From c4f5c83b8e8614ead5f48952ea8b60b5b3a3971c Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 2 May 2023 10:04:58 +0200 Subject: wallet-core: implement withdrawal tx transitions --- packages/taler-wallet-core/src/db.ts | 15 +- .../taler-wallet-core/src/operations/withdraw.ts | 309 ++++++++++++++++++++- 2 files changed, 322 insertions(+), 2 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 cff874508..febc5f8fa 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -171,6 +171,16 @@ export enum WithdrawalGroupStatus { */ AbortingBank = 14, + /** + * Exchange wants KYC info from the user. + */ + Kyc = 16, + + /** + * Exchange is doing AML checks. + */ + Aml = 17, + /** * The corresponding withdraw record has been created. * No further processing is done, unless explicitly requested @@ -187,7 +197,10 @@ export enum WithdrawalGroupStatus { SuspendedWaitConfirmBank = 53, SuspendedQueryingStatus = 54, SuspendedReady = 55, - SuspendedAbortingBank = 55, + SuspendedAbortingBank = 56, + SuspendedKyc = 57, + SuspendedAml = 58, + FailedAbortingBank = 59, } /** diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 6d5644b06..3f3eb3784 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -112,6 +112,7 @@ import { OperationAttemptResult, OperationAttemptResultType, TaskIdentifiers, + constructTaskIdentifier, } from "../util/retries.js"; import { WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, @@ -128,13 +129,284 @@ import { selectForcedWithdrawalDenominations, selectWithdrawalDenominations, } from "../util/coinSelection.js"; -import { isWithdrawableDenom } from "../index.js"; +import { PendingTaskType, isWithdrawableDenom } from "../index.js"; +import { + constructTransactionIdentifier, + stopLongpolling, +} from "./transactions.js"; /** * Logger for this file. */ const logger = new Logger("operations/withdraw.ts"); +export async function suspendWithdrawalTransaction( + ws: InternalWalletState, + withdrawalGroupId: string, +) { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.Withdraw, + withdrawalGroupId, + }); + stopLongpolling(ws, taskId); + const stateUpdate = await ws.db + .mktx((x) => [x.withdrawalGroups]) + .runReadWrite(async (tx) => { + const wg = await tx.withdrawalGroups.get(withdrawalGroupId); + if (!wg) { + logger.warn(`withdrawal group ${withdrawalGroupId} not found`); + return; + } + let newStatus: WithdrawalGroupStatus | undefined = undefined; + switch (wg.status) { + case WithdrawalGroupStatus.Ready: + newStatus = WithdrawalGroupStatus.SuspendedReady; + break; + case WithdrawalGroupStatus.AbortingBank: + newStatus = WithdrawalGroupStatus.SuspendedAbortingBank; + break; + case WithdrawalGroupStatus.WaitConfirmBank: + newStatus = WithdrawalGroupStatus.SuspendedWaitConfirmBank; + break; + case WithdrawalGroupStatus.RegisteringBank: + newStatus = WithdrawalGroupStatus.SuspendedRegisteringBank; + break; + case WithdrawalGroupStatus.QueryingStatus: + newStatus = WithdrawalGroupStatus.QueryingStatus; + break; + case WithdrawalGroupStatus.Kyc: + newStatus = WithdrawalGroupStatus.SuspendedKyc; + break; + case WithdrawalGroupStatus.Aml: + newStatus = WithdrawalGroupStatus.SuspendedAml; + break; + default: + logger.warn( + `Unsupported 'suspend' on withdrawal transaction in status ${wg.status}`, + ); + } + if (newStatus != null) { + const oldTxState = computeWithdrawalTransactionStatus(wg); + wg.status = newStatus; + const newTxState = computeWithdrawalTransactionStatus(wg); + await tx.withdrawalGroups.put(wg); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + + if (stateUpdate) { + ws.notify({ + type: NotificationType.TransactionStateTransition, + transactionId: constructTransactionIdentifier({ + tag: TransactionType.Withdrawal, + withdrawalGroupId, + }), + oldTxState: stateUpdate.oldTxState, + newTxState: stateUpdate.newTxState, + }); + } +} + +export async function resumeWithdrawalTransaction( + ws: InternalWalletState, + withdrawalGroupId: string, +) { + const stateUpdate = await ws.db + .mktx((x) => [x.withdrawalGroups]) + .runReadWrite(async (tx) => { + const wg = await tx.withdrawalGroups.get(withdrawalGroupId); + if (!wg) { + logger.warn(`withdrawal group ${withdrawalGroupId} not found`); + return; + } + let newStatus: WithdrawalGroupStatus | undefined = undefined; + switch (wg.status) { + case WithdrawalGroupStatus.SuspendedReady: + newStatus = WithdrawalGroupStatus.Ready; + break; + case WithdrawalGroupStatus.SuspendedAbortingBank: + newStatus = WithdrawalGroupStatus.AbortingBank; + break; + case WithdrawalGroupStatus.SuspendedWaitConfirmBank: + newStatus = WithdrawalGroupStatus.WaitConfirmBank; + break; + case WithdrawalGroupStatus.SuspendedQueryingStatus: + newStatus = WithdrawalGroupStatus.QueryingStatus; + break; + case WithdrawalGroupStatus.SuspendedRegisteringBank: + newStatus = WithdrawalGroupStatus.RegisteringBank; + break; + case WithdrawalGroupStatus.SuspendedAml: + newStatus = WithdrawalGroupStatus.Aml; + break; + case WithdrawalGroupStatus.SuspendedKyc: + newStatus = WithdrawalGroupStatus.Kyc; + break; + default: + logger.warn( + `Unsupported 'resume' on withdrawal transaction in status ${wg.status}`, + ); + } + if (newStatus != null) { + const oldTxState = computeWithdrawalTransactionStatus(wg); + wg.status = newStatus; + const newTxState = computeWithdrawalTransactionStatus(wg); + await tx.withdrawalGroups.put(wg); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + + if (stateUpdate) { + ws.notify({ + type: NotificationType.TransactionStateTransition, + transactionId: constructTransactionIdentifier({ + tag: TransactionType.Withdrawal, + withdrawalGroupId, + }), + oldTxState: stateUpdate.oldTxState, + newTxState: stateUpdate.newTxState, + }); + } +} + +export async function abortWithdrawalTransaction( + ws: InternalWalletState, + withdrawalGroupId: string, +) { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.Withdraw, + withdrawalGroupId, + }); + stopLongpolling(ws, taskId); + const stateUpdate = await ws.db + .mktx((x) => [x.withdrawalGroups]) + .runReadWrite(async (tx) => { + const wg = await tx.withdrawalGroups.get(withdrawalGroupId); + if (!wg) { + logger.warn(`withdrawal group ${withdrawalGroupId} not found`); + return; + } + let newStatus: WithdrawalGroupStatus | undefined = undefined; + switch (wg.status) { + case WithdrawalGroupStatus.WaitConfirmBank: + case WithdrawalGroupStatus.RegisteringBank: + case WithdrawalGroupStatus.AbortingBank: + newStatus = WithdrawalGroupStatus.AbortingBank; + break; + case WithdrawalGroupStatus.Aml: + newStatus = WithdrawalGroupStatus.SuspendedAml; + break; + case WithdrawalGroupStatus.Kyc: + newStatus = WithdrawalGroupStatus.SuspendedKyc; + break; + case WithdrawalGroupStatus.QueryingStatus: + newStatus = WithdrawalGroupStatus.SuspendedQueryingStatus; + break; + case WithdrawalGroupStatus.Ready: + newStatus = WithdrawalGroupStatus.SuspendedReady; + break; + case WithdrawalGroupStatus.SuspendedAbortingBank: + case WithdrawalGroupStatus.SuspendedQueryingStatus: + case WithdrawalGroupStatus.SuspendedAml: + case WithdrawalGroupStatus.SuspendedKyc: + case WithdrawalGroupStatus.SuspendedReady: + // No transition needed + break; + case WithdrawalGroupStatus.SuspendedRegisteringBank: + case WithdrawalGroupStatus.SuspendedWaitConfirmBank: + case WithdrawalGroupStatus.Finished: + case WithdrawalGroupStatus.BankAborted: + // Not allowed + break; + } + if (newStatus != null) { + const oldTxState = computeWithdrawalTransactionStatus(wg); + wg.status = newStatus; + const newTxState = computeWithdrawalTransactionStatus(wg); + await tx.withdrawalGroups.put(wg); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + + if (stateUpdate) { + ws.notify({ + type: NotificationType.TransactionStateTransition, + transactionId: constructTransactionIdentifier({ + tag: TransactionType.Withdrawal, + withdrawalGroupId, + }), + oldTxState: stateUpdate.oldTxState, + newTxState: stateUpdate.newTxState, + }); + } +} + +// Called "cancel" in the spec right now, +// from suspended-aborting. +export async function cancelAbortingWithdrawalTransaction( + ws: InternalWalletState, + withdrawalGroupId: string, +) { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.Withdraw, + withdrawalGroupId, + }); + stopLongpolling(ws, taskId); + const stateUpdate = await ws.db + .mktx((x) => [x.withdrawalGroups]) + .runReadWrite(async (tx) => { + const wg = await tx.withdrawalGroups.get(withdrawalGroupId); + if (!wg) { + logger.warn(`withdrawal group ${withdrawalGroupId} not found`); + return; + } + let newStatus: WithdrawalGroupStatus | undefined = undefined; + switch (wg.status) { + case WithdrawalGroupStatus.AbortingBank: + newStatus = WithdrawalGroupStatus.FailedAbortingBank; + break; + default: + break; + } + if (newStatus != null) { + const oldTxState = computeWithdrawalTransactionStatus(wg); + wg.status = newStatus; + const newTxState = computeWithdrawalTransactionStatus(wg); + await tx.withdrawalGroups.put(wg); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + + if (stateUpdate) { + ws.notify({ + type: NotificationType.TransactionStateTransition, + transactionId: constructTransactionIdentifier({ + tag: TransactionType.Withdrawal, + withdrawalGroupId, + }), + oldTxState: stateUpdate.oldTxState, + newTxState: stateUpdate.newTxState, + }); + } +} + + export function computeWithdrawalTransactionStatus( wgRecord: WithdrawalGroupRecord, ): TransactionState { @@ -192,6 +464,41 @@ export function computeWithdrawalTransactionStatus( major: TransactionMajorState.Suspended, minor: TransactionMinorState.BankConfirmTransfer, }; + case WithdrawalGroupStatus.SuspendedReady: { + return { + major: TransactionMajorState.Suspended, + minor: TransactionMinorState.WithdrawCoins, + }; + } + case WithdrawalGroupStatus.Aml: { + return { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.AmlRequired, + }; + } + case WithdrawalGroupStatus.Kyc: { + return { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.KycRequired, + }; + } + case WithdrawalGroupStatus.SuspendedAml: { + return { + major: TransactionMajorState.Suspended, + minor: TransactionMinorState.AmlRequired, + }; + } + case WithdrawalGroupStatus.SuspendedKyc: { + return { + major: TransactionMajorState.Suspended, + minor: TransactionMinorState.KycRequired, + }; + } + case WithdrawalGroupStatus.FailedAbortingBank: + return { + major: TransactionMajorState.Failed, + minor: TransactionMinorState.AbortingBank, + }; } } -- cgit v1.2.3