From 002ab0dab7b83c5999b0f82c430e716c718251e6 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 6 Jun 2023 17:07:09 +0200 Subject: wallet-core: try to abort withdrawals wallet-side with the bank --- packages/taler-wallet-core/src/db.ts | 2 + .../taler-wallet-core/src/operations/withdraw.ts | 216 ++++++++++++++------- 2 files changed, 147 insertions(+), 71 deletions(-) (limited to 'packages/taler-wallet-core/src') diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 195760831..9905fa370 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -210,6 +210,8 @@ export enum WithdrawalGroupStatus { * wired or not. */ AbortedExchange = 60, + + AbortedBank = 61, } /** diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 7db6dcd2a..61cab6fbb 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -316,6 +316,7 @@ export async function abortWithdrawalTransaction( case WithdrawalGroupStatus.Finished: case WithdrawalGroupStatus.FailedBankAborted: case WithdrawalGroupStatus.AbortedExchange: + case WithdrawalGroupStatus.AbortedBank: case WithdrawalGroupStatus.FailedAbortingBank: // Not allowed throw Error("abort not allowed in current state"); @@ -481,6 +482,12 @@ export function computeWithdrawalTransactionStatus( major: TransactionMajorState.Aborted, minor: TransactionMinorState.Exchange, }; + + case WithdrawalGroupStatus.AbortedBank: + return { + major: TransactionMajorState.Aborted, + minor: TransactionMinorState.Bank, + }; } } @@ -507,7 +514,7 @@ export function computeWithdrawalTransactionActions( case WithdrawalGroupStatus.SuspendedQueryingStatus: return [TransactionAction.Resume, TransactionAction.Abort]; case WithdrawalGroupStatus.SuspendedRegisteringBank: - return [TransactionAction.Resume, TransactionAction.Abort] + return [TransactionAction.Resume, TransactionAction.Abort]; case WithdrawalGroupStatus.SuspendedWaitConfirmBank: return [TransactionAction.Resume, TransactionAction.Abort]; case WithdrawalGroupStatus.SuspendedReady: @@ -519,11 +526,13 @@ export function computeWithdrawalTransactionActions( case WithdrawalGroupStatus.SuspendedAml: return [TransactionAction.Resume, TransactionAction.Abort]; case WithdrawalGroupStatus.SuspendedKyc: - return [TransactionAction.Resume, TransactionAction.Abort] + return [TransactionAction.Resume, TransactionAction.Abort]; case WithdrawalGroupStatus.FailedAbortingBank: return [TransactionAction.Delete]; case WithdrawalGroupStatus.AbortedExchange: return [TransactionAction.Delete]; + case WithdrawalGroupStatus.AbortedBank: + return [TransactionAction.Delete]; } } @@ -1270,87 +1279,61 @@ export interface WithdrawalGroupContext { wgRecord: WithdrawalGroupRecord; } -export async function processWithdrawalGroup( +async function processWithdrawalGroupAbortingBank( ws: InternalWalletState, - withdrawalGroupId: string, + withdrawalGroup: WithdrawalGroupRecord, ): Promise { - logger.trace("processing withdrawal group", withdrawalGroupId); - const withdrawalGroup = await ws.db - .mktx((x) => [x.withdrawalGroups]) - .runReadOnly(async (tx) => { - return tx.withdrawalGroups.get(withdrawalGroupId); - }); - - if (!withdrawalGroup) { - throw Error(`withdrawal group ${withdrawalGroupId} not found`); - } - - const retryTag = TaskIdentifiers.forWithdrawal(withdrawalGroup); + const { withdrawalGroupId } = withdrawalGroup; const transactionId = constructTransactionIdentifier({ tag: TransactionType.Withdrawal, withdrawalGroupId, }); - // We're already running! - if (ws.activeLongpoll[retryTag]) { - logger.info("withdrawal group already in long-polling, returning!"); - return { - type: OperationAttemptResultType.Longpoll, - }; + const wgInfo = withdrawalGroup.wgInfo; + if (wgInfo.withdrawalType != WithdrawalRecordType.BankIntegrated) { + throw Error("invalid state (aborting(bank) without bank info"); } + const abortUrl = getBankAbortUrl(wgInfo.bankInfo.talerWithdrawUri); + logger.info(`aborting withdrawal at ${abortUrl}`); + const abortResp = await ws.http.fetch(abortUrl, { + method: "POST", + body: {}, + }); + logger.info(`abort response status: ${abortResp.status}`); - 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); - case WithdrawalGroupStatus.PendingQueryingStatus: { - runLongpollAsync(ws, retryTag, (ct) => { - return queryReserve(ws, withdrawalGroupId, ct); - }); - logger.trace( - "returning early from withdrawal for long-polling in background", - ); - return { - type: OperationAttemptResultType.Longpoll, - }; - } - case WithdrawalGroupStatus.PendingWaitConfirmBank: { - const res = await processReserveBankStatus(ws, withdrawalGroupId); - switch (res.status) { - case BankStatusResultCode.Aborted: - case BankStatusResultCode.Done: - return { - type: OperationAttemptResultType.Finished, - result: undefined, - }; - case BankStatusResultCode.Waiting: { - return { - type: OperationAttemptResultType.Pending, - result: undefined, - }; - } + const transitionInfo = await ws.db + .mktx((x) => [x.withdrawalGroups]) + .runReadWrite(async (tx) => { + const wg = await tx.withdrawalGroups.get(withdrawalGroupId); + if (!wg) { + return undefined; } - break; - } - case WithdrawalGroupStatus.FailedBankAborted: { - // FIXME + const txStatusOld = computeWithdrawalTransactionStatus(wg); + wg.status = WithdrawalGroupStatus.AbortedBank; + wg.timestampFinish = TalerPreciseTimestamp.now(); + const txStatusNew = computeWithdrawalTransactionStatus(wg); + await tx.withdrawalGroups.put(wg); return { - type: OperationAttemptResultType.Pending, - result: undefined, + oldTxState: txStatusOld, + newTxState: txStatusNew, }; - } - case WithdrawalGroupStatus.Finished: - // We can try to withdraw, nothing needs to be done with the reserve. - break; - case WithdrawalGroupStatus.PendingReady: - // Continue with the actual withdrawal! - break; - default: - throw new InvariantViolatedError( - `unknown reserve record status: ${withdrawalGroup.status}`, - ); - } + }); + notifyTransition(ws, transactionId, transitionInfo); + return { + type: OperationAttemptResultType.Finished, + result: undefined, + }; +} + +async function processWithdrawalGroupPendingReady( + ws: InternalWalletState, + withdrawalGroup: WithdrawalGroupRecord, +): Promise { + const { withdrawalGroupId } = withdrawalGroup; + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.Withdrawal, + withdrawalGroupId, + }); await ws.exchangeOps.updateExchangeFromUrl( ws, @@ -1544,6 +1527,85 @@ export async function processWithdrawalGroup( }; } +export async function processWithdrawalGroup( + ws: InternalWalletState, + withdrawalGroupId: string, +): Promise { + logger.trace("processing withdrawal group", withdrawalGroupId); + const withdrawalGroup = await ws.db + .mktx((x) => [x.withdrawalGroups]) + .runReadOnly(async (tx) => { + return tx.withdrawalGroups.get(withdrawalGroupId); + }); + + if (!withdrawalGroup) { + throw Error(`withdrawal group ${withdrawalGroupId} not found`); + } + + const retryTag = TaskIdentifiers.forWithdrawal(withdrawalGroup); + + // We're already running! + if (ws.activeLongpoll[retryTag]) { + logger.info("withdrawal group already in long-polling, returning!"); + return { + type: OperationAttemptResultType.Longpoll, + }; + } + + 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); + case WithdrawalGroupStatus.PendingQueryingStatus: { + runLongpollAsync(ws, retryTag, (ct) => { + return queryReserve(ws, withdrawalGroupId, ct); + }); + logger.trace( + "returning early from withdrawal for long-polling in background", + ); + return { + type: OperationAttemptResultType.Longpoll, + }; + } + case WithdrawalGroupStatus.PendingWaitConfirmBank: { + const res = await processReserveBankStatus(ws, withdrawalGroupId); + switch (res.status) { + case BankStatusResultCode.Aborted: + case BankStatusResultCode.Done: + return { + type: OperationAttemptResultType.Finished, + result: undefined, + }; + case BankStatusResultCode.Waiting: { + return { + type: OperationAttemptResultType.Pending, + result: undefined, + }; + } + } + break; + } + case WithdrawalGroupStatus.Finished: + case WithdrawalGroupStatus.FailedBankAborted: { + // FIXME + return { + type: OperationAttemptResultType.Pending, + result: undefined, + }; + } + case WithdrawalGroupStatus.PendingReady: + // Continue with the actual withdrawal! + return await processWithdrawalGroupPendingReady(ws, withdrawalGroup); + case WithdrawalGroupStatus.AbortingBank: + return await processWithdrawalGroupAbortingBank(ws, withdrawalGroup); + default: + throw new InvariantViolatedError( + `unknown withdrawal group status: ${withdrawalGroup.status}`, + ); + } +} + export async function checkWithdrawalKycStatus( ws: InternalWalletState, exchangeUrl: string, @@ -1890,6 +1952,18 @@ export function getBankStatusUrl(talerWithdrawUri: string): string { return url.href; } +export function getBankAbortUrl(talerWithdrawUri: string): string { + const uriResult = parseWithdrawUri(talerWithdrawUri); + if (!uriResult) { + throw Error(`can't parse withdrawal URL ${talerWithdrawUri}`); + } + const url = new URL( + `withdrawal-operation/${uriResult.withdrawalOperationId}/abort`, + uriResult.bankIntegrationApiBaseUrl, + ); + return url.href; +} + async function registerReserveWithBank( ws: InternalWalletState, withdrawalGroupId: string, -- cgit v1.2.3