diff options
author | Florian Dold <florian@dold.me> | 2024-02-19 19:45:26 +0100 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2024-02-19 20:07:45 +0100 |
commit | e1a7bf4825162b4b95669ae6b4552589f4a64217 (patch) | |
tree | c8bdfe19ed9ed120e44d7c4be04a10bcfd1e67ff /packages/taler-wallet-core/src/withdraw.ts | |
parent | 0b044475a96ae140754ce478af41fc24424d7cc3 (diff) | |
download | wallet-core-e1a7bf4825162b4b95669ae6b4552589f4a64217.tar.xz |
wallet-core: better import hygiene, cleanup withdrawals
Diffstat (limited to 'packages/taler-wallet-core/src/withdraw.ts')
-rw-r--r-- | packages/taler-wallet-core/src/withdraw.ts | 220 |
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, }; } |