taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit d4442df30a6bafd9c00423888f4edfd88e0e5c90
parent 9686ed3428d64ba8480bf6632fd08adde47cbbe8
Author: Florian Dold <florian@dold.me>
Date:   Wed, 17 Jul 2024 21:48:38 +0200

wallet-core: tolerate withdrawal with missing exchange entry

Diffstat:
Mpackages/taler-wallet-core/src/withdraw.ts | 155+++++++++++++++++++++++++++++++++++++++----------------------------------------
1 file changed, 77 insertions(+), 78 deletions(-)

diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -178,68 +178,8 @@ import { WalletExecutionContext, getDenomInfo } from "./wallet.js"; */ const logger = new Logger("withdraw.ts"); -/** - * Update the materialized withdrawal transaction based - * on the withdrawal group record. - */ -async function updateWithdrawalTransaction( - ctx: WithdrawTransactionContext, - tx: WalletDbReadWriteTransaction< - [ - "withdrawalGroups", - "transactionsMeta", - "operationRetries", - "exchanges", - "exchangeDetails", - ] - >, -): Promise<void> { - const wgRecord = await tx.withdrawalGroups.get(ctx.withdrawalGroupId); - if (!wgRecord) { - await tx.transactionsMeta.delete(ctx.transactionId); - return; - } - - if ( - !wgRecord.instructedAmount || - !wgRecord.denomsSel || - !wgRecord.exchangeBaseUrl - ) { - // withdrawal group is in preparation, nothing to update - return; - } - - if (wgRecord.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated) { - } else if ( - wgRecord.wgInfo.withdrawalType === WithdrawalRecordType.BankManual - ) { - checkDbInvariant( - wgRecord.instructedAmount !== undefined, - "manual withdrawal without amount can't be created", - ); - checkDbInvariant( - wgRecord.denomsSel !== undefined, - "manual withdrawal without denoms can't be created", - ); - } else { - // FIXME: If this is an orphaned withdrawal for a p2p transaction, we - // still might want to report the withdrawal. - return; - } - await tx.transactionsMeta.put({ - transactionId: ctx.transactionId, - status: wgRecord.status, - timestamp: wgRecord.timestampStart, - currency: Amounts.currencyOf(wgRecord.instructedAmount), - exchanges: [wgRecord.exchangeBaseUrl], - }); - - // FIXME: Handle orphaned withdrawals where the p2p or recoup tx was deleted? -} - function buildTransactionForBankIntegratedWithdraw( wg: WithdrawalGroupRecord, - exchangeDetails: ExchangeWireDetails | undefined, ort?: OperationRetryRecord, ): TransactionWithdrawal { if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) { @@ -294,14 +234,14 @@ function buildTransactionForBankIntegratedWithdraw( function buildTransactionForManualWithdraw( wg: WithdrawalGroupRecord, - exchangeDetails: ExchangeWireDetails, + exchangeDetails: ExchangeWireDetails | undefined, ort?: OperationRetryRecord, ): TransactionWithdrawal { if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.BankManual) throw Error(""); const plainPaytoUris = - exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? []; + exchangeDetails?.wireInfo?.accounts.map((x) => x.payto_uri) ?? []; checkDbInvariant(wg.instructedAmount !== undefined, "wg uninitialized"); checkDbInvariant(wg.denomsSel !== undefined, "wg uninitialized"); @@ -376,27 +316,27 @@ export class WithdrawTransactionContext implements TransactionContext { return undefined; } const ort = await tx.operationRetries.get(this.taskId); - const exchangeDetails = - withdrawalGroupRecord.exchangeBaseUrl === undefined - ? undefined - : await getExchangeWireDetailsInTx( - tx, - withdrawalGroupRecord.exchangeBaseUrl, - ); if ( withdrawalGroupRecord.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated ) { return buildTransactionForBankIntegratedWithdraw( withdrawalGroupRecord, - exchangeDetails, ort, ); } - checkDbInvariant( - exchangeDetails !== undefined, - "manual withdrawal without exchange", - ); + const exchangeDetails = + withdrawalGroupRecord.exchangeBaseUrl === undefined + ? undefined + : await getExchangeWireDetailsInTx( + tx, + withdrawalGroupRecord.exchangeBaseUrl, + ); + if (!exchangeDetails) { + logger.warn( + `transaction ${this.transactionId} is a manual withdrawal, but no exchange wire details found`, + ); + } return buildTransactionForManualWithdraw( withdrawalGroupRecord, exchangeDetails, @@ -405,6 +345,66 @@ export class WithdrawTransactionContext implements TransactionContext { } /** + * Update the metadata of the transaction in the database. + */ + async updateTransactionMeta( + tx: WalletDbReadWriteTransaction< + [ + "withdrawalGroups", + "transactionsMeta", + "operationRetries", + "exchanges", + "exchangeDetails", + ] + >, + ): Promise<void> { + const ctx = this; + const wgRecord = await tx.withdrawalGroups.get(ctx.withdrawalGroupId); + if (!wgRecord) { + await tx.transactionsMeta.delete(ctx.transactionId); + return; + } + + if ( + !wgRecord.instructedAmount || + !wgRecord.denomsSel || + !wgRecord.exchangeBaseUrl + ) { + // withdrawal group is in preparation, nothing to update + return; + } + + if ( + wgRecord.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated + ) { + } else if ( + wgRecord.wgInfo.withdrawalType === WithdrawalRecordType.BankManual + ) { + checkDbInvariant( + wgRecord.instructedAmount !== undefined, + "manual withdrawal without amount can't be created", + ); + checkDbInvariant( + wgRecord.denomsSel !== undefined, + "manual withdrawal without denoms can't be created", + ); + } else { + // FIXME: If this is an orphaned withdrawal for a p2p transaction, we + // still might want to report the withdrawal. + return; + } + await tx.transactionsMeta.put({ + transactionId: ctx.transactionId, + status: wgRecord.status, + timestamp: wgRecord.timestampStart, + currency: Amounts.currencyOf(wgRecord.instructedAmount), + exchanges: [wgRecord.exchangeBaseUrl], + }); + + // FIXME: Handle orphaned withdrawals where the p2p or recoup tx was deleted? + } + + /** * Transition a withdrawal transaction. * Extra object stores may be accessed during the transition. */ @@ -458,11 +458,10 @@ export class WithdrawTransactionContext implements TransactionContext { return undefined; } - // const res = await f(wgRec, tx); switch (res.type) { case TransitionResultType.Transition: { await tx.withdrawalGroups.put(res.rec); - await updateWithdrawalTransaction(this, tx); + await this.updateTransactionMeta(tx); const newTxState = computeWithdrawalTransactionStatus(res.rec); return { oldTxState, @@ -471,7 +470,7 @@ export class WithdrawTransactionContext implements TransactionContext { } case TransitionResultType.Delete: await tx.withdrawalGroups.delete(this.withdrawalGroupId); - await updateWithdrawalTransaction(this, tx); + await this.updateTransactionMeta(tx); return { oldTxState, newTxState: { @@ -3129,7 +3128,7 @@ export async function internalCreateWithdrawalGroup( }, async (tx) => { const res = await internalPerformCreateWithdrawalGroup(wex, tx, prep); - await updateWithdrawalTransaction(ctx, tx); + await ctx.updateTransactionMeta(tx); return res; }, );