aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/withdraw.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/withdraw.ts')
-rw-r--r--packages/taler-wallet-core/src/withdraw.ts220
1 files changed, 153 insertions, 67 deletions
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
index 7e9b295bd..4979b2623 100644
--- a/packages/taler-wallet-core/src/withdraw.ts
+++ b/packages/taler-wallet-core/src/withdraw.ts
@@ -15,6 +15,11 @@
*/
/**
+ * @fileoverview Implementation of Taler withdrawals, both
+ * bank-integrated and manual.
+ */
+
+/**
* Imports.
*/
import {
@@ -26,6 +31,7 @@ import {
AmountLike,
AmountString,
Amounts,
+ AsyncFlag,
BankWithdrawDetails,
CancellationToken,
CoinStatus,
@@ -105,11 +111,14 @@ import {
KycPendingInfo,
PlanchetRecord,
PlanchetStatus,
+ WalletDbReadOnlyTransaction,
+ WalletDbReadWriteTransaction,
WalletStoresV1,
WgInfo,
WithdrawalGroupRecord,
WithdrawalGroupStatus,
WithdrawalRecordType,
+ timestampPreciseToDb,
} from "./db.js";
import {
ReadyExchangeSummary,
@@ -119,12 +128,6 @@ import {
listExchanges,
markExchangeUsed,
} from "./exchanges.js";
-import {
- WalletDbReadOnlyTransaction,
- WalletDbReadWriteTransaction,
- isWithdrawableDenom,
- timestampPreciseToDb,
-} from "./index.js";
import { InternalWalletState } from "./internal-wallet-state.js";
import { DbAccess } from "./query.js";
import {
@@ -137,6 +140,7 @@ import {
selectForcedWithdrawalDenominations,
selectWithdrawalDenominations,
} from "./util/coinSelection.js";
+import { isWithdrawableDenom } from "./util/denominations.js";
import { checkDbInvariant, checkLogicInvariant } from "./util/invariants.js";
import {
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
@@ -628,7 +632,7 @@ export async function getCandidateWithdrawalDenomsTx(
exchangeBaseUrl: string,
currency: string,
): Promise<DenominationRecord[]> {
- // FIXME: Use denom groups instead of querying all denominations!
+ // FIXME(https://bugs.taler.net/n/8446): Use denom groups instead of querying all denominations!
const allDenoms =
await tx.denominations.indexes.byExchangeBaseUrl.getAll(exchangeBaseUrl);
return allDenoms
@@ -730,10 +734,11 @@ interface WithdrawalBatchResult {
batchResp: ExchangeWithdrawBatchResponse;
}
-enum AmlStatus {
- normal = 0,
- pending = 1,
- fronzen = 2,
+// FIXME: Move to exchange API types
+enum ExchangeAmlStatus {
+ Normal = 0,
+ Pending = 1,
+ Frozen = 2,
}
/**
@@ -818,7 +823,7 @@ async function handleKycRequired(
method: "GET",
});
let kycUrl: string;
- let amlStatus: AmlStatus | undefined;
+ let amlStatus: ExchangeAmlStatus | undefined;
if (
kycStatusRes.status === HttpStatusCode.Ok ||
// FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
@@ -872,11 +877,11 @@ async function handleKycRequired(
};
wg2.kycUrl = kycUrl;
wg2.status =
- amlStatus === AmlStatus.normal || amlStatus === undefined
+ amlStatus === ExchangeAmlStatus.Normal || amlStatus === undefined
? WithdrawalGroupStatus.PendingKyc
- : amlStatus === AmlStatus.pending
+ : amlStatus === ExchangeAmlStatus.Pending
? WithdrawalGroupStatus.PendingAml
- : amlStatus === AmlStatus.fronzen
+ : amlStatus === ExchangeAmlStatus.Frozen
? WithdrawalGroupStatus.SuspendedAml
: assertUnreachable(amlStatus);
@@ -1333,7 +1338,7 @@ async function queryReserve(
*
* Used to store some cached info during a withdrawal operation.
*/
-export interface WithdrawalGroupContext {
+interface WithdrawalGroupContext {
numPlanchets: number;
planchetsFinished: Set<string>;
@@ -1659,19 +1664,11 @@ export async function processWithdrawalGroup(
switch (withdrawalGroup.status) {
case WithdrawalGroupStatus.PendingRegisteringBank:
- await processReserveBankStatus(ws, withdrawalGroupId);
- // FIXME: This will get called by the main task loop, why call it here?!
- return await processWithdrawalGroup(
- ws,
- withdrawalGroupId,
- cancellationToken,
- );
- case WithdrawalGroupStatus.PendingQueryingStatus: {
+ return await processReserveBankStatus(ws, withdrawalGroupId);
+ case WithdrawalGroupStatus.PendingQueryingStatus:
return queryReserve(ws, withdrawalGroupId, cancellationToken);
- }
- case WithdrawalGroupStatus.PendingWaitConfirmBank: {
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
return await processReserveBankStatus(ws, withdrawalGroupId);
- }
case WithdrawalGroupStatus.PendingAml:
// FIXME: Handle this case, withdrawal doesn't support AML yet.
return TaskRunResult.backoff();
@@ -1768,29 +1765,34 @@ export async function getExchangeWithdrawalInfo(
logger.trace("computing earliest deposit expiration");
let earliestDepositExpiration: TalerProtocolTimestamp | undefined;
- for (let i = 0; i < selectedDenoms.selectedDenoms.length; i++) {
- const ds = selectedDenoms.selectedDenoms[i];
- // FIXME: Do in one transaction!
- const denom = await ws.db.runReadOnlyTx(["denominations"], async (tx) => {
- return ws.getDenomInfo(ws, tx, exchangeBaseUrl, ds.denomPubHash);
- });
- checkDbInvariant(!!denom);
- hasDenomWithAgeRestriction =
- hasDenomWithAgeRestriction || denom.denomPub.age_mask > 0;
- const expireDeposit = denom.stampExpireDeposit;
- if (!earliestDepositExpiration) {
- earliestDepositExpiration = expireDeposit;
- continue;
- }
- if (
- AbsoluteTime.cmp(
- AbsoluteTime.fromProtocolTimestamp(expireDeposit),
- AbsoluteTime.fromProtocolTimestamp(earliestDepositExpiration),
- ) < 0
- ) {
- earliestDepositExpiration = expireDeposit;
+
+ await ws.db.runReadOnlyTx(["denominations"], async (tx) => {
+ for (let i = 0; i < selectedDenoms.selectedDenoms.length; i++) {
+ const ds = selectedDenoms.selectedDenoms[i];
+ const denom = await ws.getDenomInfo(
+ ws,
+ tx,
+ exchangeBaseUrl,
+ ds.denomPubHash,
+ );
+ checkDbInvariant(!!denom);
+ hasDenomWithAgeRestriction =
+ hasDenomWithAgeRestriction || denom.denomPub.age_mask > 0;
+ const expireDeposit = denom.stampExpireDeposit;
+ if (!earliestDepositExpiration) {
+ earliestDepositExpiration = expireDeposit;
+ continue;
+ }
+ if (
+ AbsoluteTime.cmp(
+ AbsoluteTime.fromProtocolTimestamp(expireDeposit),
+ AbsoluteTime.fromProtocolTimestamp(earliestDepositExpiration),
+ ) < 0
+ ) {
+ earliestDepositExpiration = expireDeposit;
+ }
}
- }
+ });
checkLogicInvariant(!!earliestDepositExpiration);
@@ -2192,13 +2194,7 @@ async function processReserveBankStatus(
// Bank still needs to know our reserve info
if (!status.selection_done) {
await registerReserveWithBank(ws, withdrawalGroupId);
- return await processReserveBankStatus(ws, withdrawalGroupId);
- }
-
- // FIXME: Why do we do this?!
- if (withdrawalGroup.status === WithdrawalGroupStatus.PendingRegisteringBank) {
- await registerReserveWithBank(ws, withdrawalGroupId);
- return await processReserveBankStatus(ws, withdrawalGroupId);
+ return TaskRunResult.progress();
}
const transitionInfo = await ws.db.runReadWriteTx(
@@ -2479,6 +2475,14 @@ export async function internalCreateWithdrawalGroup(
return res.withdrawalGroup;
}
+/**
+ * Accept a bank-integrated withdrawal.
+ *
+ * Before returning, the wallet tries to register the reserve with the bank.
+ *
+ * Thus after this call returns, the withdrawal operation can be confirmed
+ * with the bank.
+ */
export async function acceptWithdrawalFromUri(
ws: InternalWalletState,
req: {
@@ -2560,11 +2564,10 @@ export async function acceptWithdrawalFromUri(
const ctx = new WithdrawTransactionContext(ws, withdrawalGroupId);
- const transactionId = ctx.transactionId;
+ // FIXME: Do we wait here until the reserve is registered with the bank?
+
+ await waitWithdrawalRegistered(ws, ctx);
- // We do this here, as the reserve should be registered before we return,
- // so that we can redirect the user to the bank's status page.
- await processReserveBankStatus(ws, withdrawalGroupId);
const processedWithdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
withdrawalGroupId,
});
@@ -2582,10 +2585,93 @@ export async function acceptWithdrawalFromUri(
return {
reservePub: withdrawalGroup.reservePub,
confirmTransferUrl: withdrawInfo.confirmTransferUrl,
- transactionId,
+ transactionId: ctx.transactionId,
};
}
+async function internalWaitWithdrawalRegistered(
+ ws: InternalWalletState,
+ ctx: WithdrawTransactionContext,
+ withdrawalNotifFlag: AsyncFlag,
+): Promise<void> {
+ while (true) {
+ const { withdrawalRec, retryRec } = await ws.db.runReadOnlyTx(
+ ["withdrawalGroups", "operationRetries"],
+ async (tx) => {
+ return {
+ withdrawalRec: await tx.withdrawalGroups.get(ctx.withdrawalGroupId),
+ retryRec: await tx.operationRetries.get(ctx.taskId),
+ };
+ },
+ );
+
+ if (!withdrawalRec) {
+ throw Error("withdrawal not found anymore");
+ }
+
+ switch (withdrawalRec.status) {
+ case WithdrawalGroupStatus.FailedBankAborted:
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
+ {},
+ );
+ case WithdrawalGroupStatus.PendingKyc:
+ case WithdrawalGroupStatus.PendingAml:
+ case WithdrawalGroupStatus.PendingQueryingStatus:
+ case WithdrawalGroupStatus.PendingReady:
+ case WithdrawalGroupStatus.Done:
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
+ return;
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ break;
+ default: {
+ if (retryRec) {
+ if (retryRec.lastError) {
+ throw TalerError.fromUncheckedDetail(retryRec.lastError);
+ } else {
+ throw Error("withdrawal unexpectedly pending");
+ }
+ }
+ }
+ }
+
+ await withdrawalNotifFlag.wait();
+ withdrawalNotifFlag.reset();
+ }
+}
+
+async function waitWithdrawalRegistered(
+ ws: InternalWalletState,
+ ctx: WithdrawTransactionContext,
+): Promise<void> {
+ // FIXME: We should use Symbol.dispose magic here for cleanup!
+
+ const withdrawalNotifFlag = new AsyncFlag();
+ // Raise exchangeNotifFlag whenever we get a notification
+ // about our exchange.
+ const cancelNotif = ws.addNotificationListener((notif) => {
+ if (
+ notif.type === NotificationType.TransactionStateTransition &&
+ notif.transactionId === ctx.transactionId
+ ) {
+ logger.info(`raising update notification: ${j2s(notif)}`);
+ withdrawalNotifFlag.raise();
+ }
+ });
+
+ try {
+ const res = await internalWaitWithdrawalRegistered(
+ ws,
+ ctx,
+ withdrawalNotifFlag,
+ );
+ logger.info("done waiting for ready exchange");
+ return res;
+ } finally {
+ cancelNotif();
+ }
+}
+
async function fetchAccount(
ws: InternalWalletState,
instructedAmount: AmountJson,
@@ -2669,7 +2755,7 @@ async function fetchWithdrawalAccountInfo(
reservePub?: string;
},
): Promise<WithdrawalExchangeAccountDetails[]> {
- const { exchange, instructedAmount } = req;
+ const { exchange } = req;
const withdrawalAccounts: WithdrawalExchangeAccountDetails[] = [];
for (let acct of exchange.wireInfo.accounts) {
const acctInfo = await fetchAccount(
@@ -2732,10 +2818,10 @@ export async function createManualWithdrawal(
reserveKeyPair,
});
- const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
- const ctx = new WithdrawTransactionContext(ws, withdrawalGroupId);
-
- const transactionId = ctx.transactionId;
+ const ctx = new WithdrawTransactionContext(
+ ws,
+ withdrawalGroup.withdrawalGroupId,
+ );
const exchangePaytoUris = await ws.db.runReadOnlyTx(
["withdrawalGroups", "exchanges", "exchangeDetails"],
@@ -2750,6 +2836,6 @@ export async function createManualWithdrawal(
reservePub: withdrawalGroup.reservePub,
exchangePaytoUris: exchangePaytoUris,
withdrawalAccountsList: withdrawalAccountsList,
- transactionId,
+ transactionId: ctx.transactionId,
};
}