diff options
Diffstat (limited to 'packages/taler-wallet-core/src/operations/withdraw.ts')
-rw-r--r-- | packages/taler-wallet-core/src/operations/withdraw.ts | 233 |
1 files changed, 90 insertions, 143 deletions
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 86f05478a..4a50e0775 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2019-2021 Taler Systems SA + (C) 2019-2024 Taler Systems SA GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -52,6 +52,7 @@ import { TalerPreciseTimestamp, TalerProtocolTimestamp, TransactionAction, + TransactionIdStr, TransactionMajorState, TransactionMinorState, TransactionState, @@ -67,7 +68,6 @@ import { codecForCashinConversionResponse, codecForConversionBankConfig, codecForExchangeWithdrawBatchResponse, - codecForIntegrationBankConfig, codecForReserveStatus, codecForWalletKycUuid, codecForWithdrawOperationStatusResponse, @@ -103,7 +103,6 @@ import { import { isWithdrawableDenom, timestampPreciseToDb } from "../index.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { - TaskIdentifiers, TaskRunResult, TaskRunResultType, TombstoneTag, @@ -111,9 +110,8 @@ import { constructTaskIdentifier, makeCoinAvailable, makeCoinsVisible, - runLongpollAsync, } from "../operations/common.js"; -import { PendingTaskType } from "../pending-types.js"; +import { PendingTaskType, TaskId } from "../pending-types.js"; import { assertUnreachable } from "../util/assertUnreachable.js"; import { selectForcedWithdrawalDenominations, @@ -141,7 +139,6 @@ import { TransitionInfo, constructTransactionIdentifier, notifyTransition, - stopLongpolling, } from "./transactions.js"; /** @@ -150,8 +147,8 @@ import { const logger = new Logger("operations/withdraw.ts"); export class WithdrawTransactionContext implements TransactionContext { - public transactionId: string; - public retryTag: string; + readonly transactionId: TransactionIdStr; + readonly taskId: TaskId; constructor( public ws: InternalWalletState, @@ -161,7 +158,7 @@ export class WithdrawTransactionContext implements TransactionContext { tag: TransactionType.Withdrawal, withdrawalGroupId, }); - this.retryTag = constructTaskIdentifier({ + this.taskId = constructTaskIdentifier({ tag: PendingTaskType.Withdraw, withdrawalGroupId, }); @@ -185,8 +182,7 @@ export class WithdrawTransactionContext implements TransactionContext { } async suspendTransaction(): Promise<void> { - const { ws, withdrawalGroupId, transactionId, retryTag } = this; - stopLongpolling(ws, retryTag); + const { ws, withdrawalGroupId, transactionId, taskId } = this; const transitionInfo = await ws.db .mktx((x) => [x.withdrawalGroups]) .runReadWrite(async (tx) => { @@ -235,13 +231,12 @@ export class WithdrawTransactionContext implements TransactionContext { } return undefined; }); - + ws.taskScheduler.stopShepherdTask(taskId); notifyTransition(ws, transactionId, transitionInfo); } async abortTransaction(): Promise<void> { - const { ws, withdrawalGroupId, transactionId } = this; - stopLongpolling(ws, this.retryTag); + const { ws, withdrawalGroupId, transactionId, taskId } = this; const transitionInfo = await ws.db .mktx((x) => [x.withdrawalGroups]) .runReadWrite(async (tx) => { @@ -297,12 +292,13 @@ export class WithdrawTransactionContext implements TransactionContext { } return undefined; }); - ws.workAvailable.trigger(); + ws.taskScheduler.stopShepherdTask(taskId); notifyTransition(ws, transactionId, transitionInfo); + ws.taskScheduler.startShepherdTask(taskId); } async resumeTransaction(): Promise<void> { - const { ws, withdrawalGroupId, transactionId } = this; + const { ws, withdrawalGroupId, transactionId, taskId: retryTag } = this; const transitionInfo = await ws.db .mktx((x) => [x.withdrawalGroups]) .runReadWrite(async (tx) => { @@ -351,13 +347,12 @@ export class WithdrawTransactionContext implements TransactionContext { } return undefined; }); - ws.workAvailable.trigger(); notifyTransition(ws, transactionId, transitionInfo); + ws.taskScheduler.startShepherdTask(retryTag); } async failTransaction(): Promise<void> { - const { ws, withdrawalGroupId, transactionId, retryTag } = this; - stopLongpolling(ws, retryTag); + const { ws, withdrawalGroupId, transactionId, taskId: retryTag } = this; const stateUpdate = await ws.db .mktx((x) => [x.withdrawalGroups]) .runReadWrite(async (tx) => { @@ -387,7 +382,9 @@ export class WithdrawTransactionContext implements TransactionContext { } return undefined; }); + ws.taskScheduler.stopShepherdTask(retryTag); notifyTransition(ws, transactionId, stateUpdate); + ws.taskScheduler.startShepherdTask(retryTag); } } @@ -744,10 +741,8 @@ async function transitionKycUrlUpdate( kycUrl: string, ): Promise<void> { let notificationKycUrl: string | undefined = undefined; - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Withdrawal, - withdrawalGroupId, - }); + const ctx = new WithdrawTransactionContext(ws, withdrawalGroupId); + const transactionId = ctx.transactionId; const transitionInfo = await ws.db .mktx((x) => [x.planchets, x.withdrawalGroups]) @@ -782,7 +777,7 @@ async function transitionKycUrlUpdate( experimentalUserData: notificationKycUrl, }); } - ws.workAvailable.trigger(); + ws.taskScheduler.startShepherdTask(ctx.taskId); } async function handleKycRequired( @@ -1273,7 +1268,7 @@ async function queryReserve( ws: InternalWalletState, withdrawalGroupId: string, cancellationToken: CancellationToken, -): Promise<{ ready: boolean }> { +): Promise<TaskRunResult> { const transactionId = constructTransactionIdentifier({ tag: TransactionType.Withdrawal, withdrawalGroupId, @@ -1283,7 +1278,7 @@ async function queryReserve( }); checkDbInvariant(!!withdrawalGroup); if (withdrawalGroup.status !== WithdrawalGroupStatus.PendingQueryingStatus) { - return { ready: true }; + return TaskRunResult.backoff(); } const reservePub = withdrawalGroup.reservePub; @@ -1312,7 +1307,7 @@ async function queryReserve( `got reserve status error, EC=${result.talerErrorResponse.code}`, ); if (resp.status === HttpStatusCode.NotFound) { - return { ready: false }; + return TaskRunResult.backoff(); } else { throwUnexpectedRequestError(resp, result.talerErrorResponse); } @@ -1341,13 +1336,7 @@ async function queryReserve( notifyTransition(ws, transactionId, transitionResult); - return { ready: true }; -} - -enum BankStatusResultCode { - Done = "done", - Waiting = "waiting", - Aborted = "aborted", + return TaskRunResult.backoff(); } /** @@ -1452,6 +1441,7 @@ async function transitionKycSatisfied( async function processWithdrawalGroupPendingKyc( ws: InternalWalletState, withdrawalGroup: WithdrawalGroupRecord, + cancellationToken: CancellationToken, ): Promise<TaskRunResult> { const userType = "individual"; const kycInfo = withdrawalGroup.kycPending; @@ -1467,45 +1457,35 @@ async function processWithdrawalGroupPendingKyc( const withdrawalGroupId = withdrawalGroup.withdrawalGroupId; - const retryTag = TaskIdentifiers.forWithdrawal(withdrawalGroup); - runLongpollAsync(ws, retryTag, async (cancellationToken) => { - logger.info(`long-polling for withdrawal KYC status via ${url.href}`); - const kycStatusRes = await ws.http.fetch(url.href, { - method: "GET", - cancellationToken, - }); - logger.info( - `kyc long-polling response status: HTTP ${kycStatusRes.status}`, - ); - if ( - kycStatusRes.status === HttpStatusCode.Ok || - //FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge - // remove after the exchange is fixed or clarified - kycStatusRes.status === HttpStatusCode.NoContent - ) { - await transitionKycSatisfied(ws, withdrawalGroup); - return { ready: true }; - } else if (kycStatusRes.status === HttpStatusCode.Accepted) { - const kycStatus = await kycStatusRes.json(); - logger.info(`kyc status: ${j2s(kycStatus)}`); - const kycUrl = kycStatus.kyc_url; - if (typeof kycUrl === "string") { - await transitionKycUrlUpdate(ws, withdrawalGroupId, kycUrl); - } - return { ready: false }; - } else if ( - kycStatusRes.status === HttpStatusCode.UnavailableForLegalReasons - ) { - const kycStatus = await kycStatusRes.json(); - logger.info(`aml status: ${j2s(kycStatus)}`); - return { ready: false }; - } else { - throw Error( - `unexpected response from kyc-check (${kycStatusRes.status})`, - ); - } + logger.info(`long-polling for withdrawal KYC status via ${url.href}`); + const kycStatusRes = await ws.http.fetch(url.href, { + method: "GET", + cancellationToken, }); - return TaskRunResult.longpoll(); + logger.info(`kyc long-polling response status: HTTP ${kycStatusRes.status}`); + if ( + kycStatusRes.status === HttpStatusCode.Ok || + //FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge + // remove after the exchange is fixed or clarified + kycStatusRes.status === HttpStatusCode.NoContent + ) { + await transitionKycSatisfied(ws, withdrawalGroup); + } else if (kycStatusRes.status === HttpStatusCode.Accepted) { + const kycStatus = await kycStatusRes.json(); + logger.info(`kyc status: ${j2s(kycStatus)}`); + const kycUrl = kycStatus.kyc_url; + if (typeof kycUrl === "string") { + await transitionKycUrlUpdate(ws, withdrawalGroupId, kycUrl); + } + } else if ( + kycStatusRes.status === HttpStatusCode.UnavailableForLegalReasons + ) { + const kycStatus = await kycStatusRes.json(); + logger.info(`aml status: ${j2s(kycStatus)}`); + } else { + throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`); + } + return TaskRunResult.backoff(); } async function processWithdrawalGroupPendingReady( @@ -1666,12 +1646,13 @@ async function processWithdrawalGroupPendingReady( }; } - return TaskRunResult.finished(); + return TaskRunResult.backoff(); } export async function processWithdrawalGroup( ws: InternalWalletState, withdrawalGroupId: string, + cancellationToken: CancellationToken, ): Promise<TaskRunResult> { logger.trace("processing withdrawal group", withdrawalGroupId); const withdrawalGroup = await ws.db @@ -1684,54 +1665,30 @@ export async function processWithdrawalGroup( 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: TaskRunResultType.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 await processWithdrawalGroup( + ws, + withdrawalGroupId, + cancellationToken, ); - return { - type: TaskRunResultType.Longpoll, - }; + case WithdrawalGroupStatus.PendingQueryingStatus: { + return queryReserve(ws, withdrawalGroupId, cancellationToken); } case WithdrawalGroupStatus.PendingWaitConfirmBank: { - const res = await processReserveBankStatus(ws, withdrawalGroupId); - switch (res.status) { - case BankStatusResultCode.Aborted: - case BankStatusResultCode.Done: - return TaskRunResult.finished(); - case BankStatusResultCode.Waiting: { - return TaskRunResult.pending(); - } - } - break; - } - case WithdrawalGroupStatus.Done: - case WithdrawalGroupStatus.FailedBankAborted: { - // FIXME - return TaskRunResult.pending(); + return await processReserveBankStatus(ws, withdrawalGroupId); } case WithdrawalGroupStatus.PendingAml: // FIXME: Handle this case, withdrawal doesn't support AML yet. - return TaskRunResult.pending(); + return TaskRunResult.backoff(); case WithdrawalGroupStatus.PendingKyc: - return processWithdrawalGroupPendingKyc(ws, withdrawalGroup); + return processWithdrawalGroupPendingKyc( + ws, + withdrawalGroup, + cancellationToken, + ); case WithdrawalGroupStatus.PendingReady: // Continue with the actual withdrawal! return await processWithdrawalGroupPendingReady(ws, withdrawalGroup); @@ -1747,6 +1704,8 @@ export async function processWithdrawalGroup( case WithdrawalGroupStatus.SuspendedReady: case WithdrawalGroupStatus.SuspendedRegisteringBank: case WithdrawalGroupStatus.SuspendedWaitConfirmBank: + case WithdrawalGroupStatus.Done: + case WithdrawalGroupStatus.FailedBankAborted: // Nothing to do. return TaskRunResult.finished(); default: @@ -2168,14 +2127,10 @@ async function registerReserveWithBank( notifyTransition(ws, transactionId, transitionInfo); } -interface BankStatusResult { - status: BankStatusResultCode; -} - async function processReserveBankStatus( ws: InternalWalletState, withdrawalGroupId: string, -): Promise<BankStatusResult> { +): Promise<TaskRunResult> { const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, { withdrawalGroupId, }); @@ -2188,9 +2143,7 @@ async function processReserveBankStatus( case WithdrawalGroupStatus.PendingRegisteringBank: break; default: - return { - status: BankStatusResultCode.Done, - }; + return TaskRunResult.backoff(); } if ( @@ -2200,9 +2153,7 @@ async function processReserveBankStatus( } const bankInfo = withdrawalGroup.wgInfo.bankInfo; if (!bankInfo) { - return { - status: BankStatusResultCode.Done, - }; + return TaskRunResult.backoff(); } const bankStatusUrl = getBankStatusUrl(bankInfo.talerWithdrawUri); @@ -2246,9 +2197,7 @@ async function processReserveBankStatus( }; }); notifyTransition(ws, transactionId, transitionInfo); - return { - status: BankStatusResultCode.Aborted, - }; + return TaskRunResult.finished(); } // Bank still needs to know our reserve info @@ -2302,15 +2251,7 @@ async function processReserveBankStatus( notifyTransition(ws, transactionId, transitionInfo); - if (status.transfer_done) { - return { - status: BankStatusResultCode.Done, - }; - } else { - return { - status: BankStatusResultCode.Waiting, - }; - } + return TaskRunResult.backoff(); } export interface PrepareCreateWithdrawalGroupResult { @@ -2492,6 +2433,13 @@ export async function internalPerformCreateWithdrawalGroup( prep.withdrawalGroup.exchangeBaseUrl, ); + const ctx = new WithdrawTransactionContext( + ws, + withdrawalGroup.withdrawalGroupId, + ); + + ws.taskScheduler.startShepherdTask(ctx.taskId); + return { withdrawalGroup, transitionInfo, @@ -2619,10 +2567,10 @@ export async function acceptWithdrawalFromUri( }); const withdrawalGroupId = withdrawalGroup.withdrawalGroupId; - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Withdrawal, - withdrawalGroupId, - }); + + const ctx = new WithdrawTransactionContext(ws, withdrawalGroupId); + + const transactionId = ctx.transactionId; // 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. @@ -2639,7 +2587,7 @@ export async function acceptWithdrawalFromUri( ); } - ws.workAvailable.trigger(); + ws.taskScheduler.startShepherdTask(ctx.taskId); return { reservePub: withdrawalGroup.reservePub, @@ -2795,10 +2743,9 @@ export async function createManualWithdrawal( }); const withdrawalGroupId = withdrawalGroup.withdrawalGroupId; - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Withdrawal, - withdrawalGroupId, - }); + const ctx = new WithdrawTransactionContext(ws, withdrawalGroupId); + + const transactionId = ctx.transactionId; const exchangePaytoUris = await ws.db .mktx((x) => [x.withdrawalGroups, x.exchanges, x.exchangeDetails]) @@ -2806,7 +2753,7 @@ export async function createManualWithdrawal( return await getFundingPaytoUris(tx, withdrawalGroup.withdrawalGroupId); }); - ws.workAvailable.trigger(); + ws.taskScheduler.startShepherdTask(ctx.taskId); return { reservePub: withdrawalGroup.reservePub, |