diff options
-rw-r--r-- | packages/taler-util/src/types-taler-wallet-transactions.ts | 1 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/balance.ts | 2 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/common.ts | 52 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/db.ts | 49 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/exchanges.ts | 89 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/pay-peer-pull-credit.ts | 126 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/pay-peer-push-credit.ts | 217 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/withdraw.ts | 116 |
8 files changed, 472 insertions, 180 deletions
diff --git a/packages/taler-util/src/types-taler-wallet-transactions.ts b/packages/taler-util/src/types-taler-wallet-transactions.ts index 018c3a041..675878e0c 100644 --- a/packages/taler-util/src/types-taler-wallet-transactions.ts +++ b/packages/taler-util/src/types-taler-wallet-transactions.ts @@ -129,6 +129,7 @@ export enum TransactionMinorState { KycRequired = "kyc", MergeKycRequired = "merge-kyc", BalanceKycRequired = "balance-kyc", + BalanceKycInit = "balance-kyc-init", Track = "track", SubmitPayment = "submit-payment", RebindSession = "rebind-session", diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts index 3b61ee4de..396efda1f 100644 --- a/packages/taler-wallet-core/src/balance.ts +++ b/packages/taler-wallet-core/src/balance.ts @@ -363,6 +363,8 @@ export async function getBalancesInsideTransaction( case WithdrawalGroupStatus.PendingBalanceKyc: case WithdrawalGroupStatus.SuspendedBalanceKyc: case WithdrawalGroupStatus.SuspendedKyc: + case WithdrawalGroupStatus.PendingBalanceKycInit: + case WithdrawalGroupStatus.SuspendedBalanceKycInit: case WithdrawalGroupStatus.PendingKyc: { checkDbInvariant( wg.denomsSel !== undefined, diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts index 668ebe5c1..02dca20e8 100644 --- a/packages/taler-wallet-core/src/common.ts +++ b/packages/taler-wallet-core/src/common.ts @@ -863,6 +863,58 @@ export async function genericWaitForState( } } +/** + * Wait until the wallet is in a particular state. + * + * Two functions must be provided: + * 1. checkState, which checks if the wallet is in the + * desired state. + * 2. filterNotification, which checks whether a notification + * might have lead to a state change. + */ +export async function genericWaitForStateVal<T>( + wex: WalletExecutionContext, + args: { + checkState: () => Promise<T | undefined>; + filterNotification: (notif: WalletNotification) => boolean; + }, +): Promise<T> { + await wex.taskScheduler.ensureRunning(); + + // FIXME: Clean up using the new JS "using" / Symbol.dispose syntax. + const flag = new AsyncFlag(); + // Raise purchaseNotifFlag whenever we get a notification + // about our refresh. + const cancelNotif = wex.ws.addNotificationListener((notif) => { + if (args.filterNotification(notif)) { + flag.raise(); + } + }); + const unregisterOnCancelled = wex.cancellationToken.onCancelled((reason) => { + cancelNotif(); + flag.raise(); + }); + + try { + while (true) { + if (wex.cancellationToken.isCancelled) { + throw Error("cancelled"); + } + const val = await args.checkState(); + if (val != null) { + return val; + } + // Wait for the next transition + await flag.wait(); + flag.reset(); + } + } catch (e) { + unregisterOnCancelled(); + cancelNotif(); + throw e; + } +} + export function requireExchangeTosAcceptedOrThrow( exchange: ReadyExchangeSummary, ): void { diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 2cee19f8e..c48a43f11 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -311,11 +311,20 @@ export enum WithdrawalGroupStatus { /** * Exchange wants KYC info from the user. + * KYC link is ready. */ PendingBalanceKyc = 0x0100_0006, SuspendedBalanceKyc = 0x0110_006, /** + * Exchange wants KYC info from the user. + * + * KYC link is not ready yet, the KYC process is still initializing. + */ + PendingBalanceKycInit = 0x0100_0007, + SuspendedBalanceKycInit = 0x0110_007, + + /** * Proposed to the user, has can choose to accept/refuse. */ DialogProposed = 0x0101_0000, @@ -1461,6 +1470,7 @@ export interface KycPendingInfo { paytoHash: string; requirementRow: number; } + /** * Group of withdrawal operations that need to be executed. * (Either for a normal withdrawal or from a reward.) @@ -1936,24 +1946,33 @@ export interface PeerPushDebitRecord { } export enum PeerPullPaymentCreditStatus { + /** + * Typically the initial state of the peer-pull-credit transaction, + * purse will be created. + */ PendingCreatePurse = 0x0100_0000, + SuspendedCreatePurse = 0x0110_0000, + /** * Purse created, waiting for the other party to accept the * invoice and deposit money into it. */ PendingReady = 0x0100_0001, - PendingMergeKycRequired = 0x0100_0002, - PendingWithdrawing = 0x0100_0003, - PendingBalanceKycRequired = 0x0100_0004, - - AbortingDeletePurse = 0x0103_0000, - - SuspendedCreatePurse = 0x0110_0000, SuspendedReady = 0x0110_0001, + + PendingMergeKycRequired = 0x0100_0002, SuspendedMergeKycRequired = 0x0110_0002, + + PendingWithdrawing = 0x0100_0003, SuspendedWithdrawing = 0x0110_0003, + + PendingBalanceKycRequired = 0x0100_0004, SuspendedBalanceKycRequired = 0x0110_0004, + PendingBalanceKycInit = 0x0100_0005, + SuspendedBalanceKycInit = 0x0110_0005, + + AbortingDeletePurse = 0x0103_0000, SuspendedAbortingDeletePurse = 0x0113_0000, Done = 0x0500_0000, @@ -2014,25 +2033,31 @@ export interface PeerPullCreditRecord { kycUrl?: string; + kycAccessToken?: string; + withdrawalGroupId: string | undefined; } export enum PeerPushCreditStatus { PendingMerge = 0x0100_0000, + SuspendedMerge = 0x0110_0000, + PendingMergeKycRequired = 0x0100_0001, + SuspendedMergeKycRequired = 0x0110_0001, + /** * Merge was successful and withdrawal group has been created, now * everything is in the hand of the withdrawal group. */ PendingWithdrawing = 0x0100_0002, + SuspendedWithdrawing = 0x0110_0002, PendingBalanceKycRequired = 0x0100_0003, - - SuspendedMerge = 0x0110_0000, - SuspendedMergeKycRequired = 0x0110_0001, - SuspendedWithdrawing = 0x0110_0002, SuspendedBalanceKycRequired = 0x0110_0003, + PendingBalanceKycInit = 0x0100_0004, + SuspendedBalanceKycInit = 0x0110_0004, + DialogProposed = 0x0101_0000, Done = 0x0500_0000, @@ -2087,6 +2112,8 @@ export interface PeerPushPaymentIncomingRecord { kycInfo?: KycPendingInfo; kycUrl?: string; + + kycAccessToken?: string; } export enum PeerPullDebitRecordStatus { diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts index fca2d4012..1820751f8 100644 --- a/packages/taler-wallet-core/src/exchanges.ts +++ b/packages/taler-wallet-core/src/exchanges.ts @@ -404,6 +404,23 @@ async function internalGetExchangeScopeInfo( }; } +function getKycStatusFromReserveStatus( + status: ReserveRecordStatus, +): ExchangeWalletKycStatus { + switch (status) { + case ReserveRecordStatus.Done: + return ExchangeWalletKycStatus.Done; + // FIXME: Do we handle the suspended state? + case ReserveRecordStatus.SuspendedLegiInit: + case ReserveRecordStatus.PendingLegiInit: + return ExchangeWalletKycStatus.LegiInit; + // FIXME: Do we handle the suspended state? + case ReserveRecordStatus.SuspendedLegi: + case ReserveRecordStatus.PendingLegi: + return ExchangeWalletKycStatus.Legi; + } +} + async function makeExchangeListItem( tx: WalletDbReadOnlyTransaction< ["globalCurrencyExchanges", "globalCurrencyAuditors"] @@ -425,24 +442,10 @@ async function makeExchangeListItem( scopeInfo = await internalGetExchangeScopeInfo(tx, exchangeDetails); } - let walletKycStatus: ExchangeWalletKycStatus | undefined = undefined; - if (reserveRec) { - switch (reserveRec.status) { - case ReserveRecordStatus.Done: - walletKycStatus = ExchangeWalletKycStatus.Done; - break; - // FIXME: Do we handle the suspended state? - case ReserveRecordStatus.SuspendedLegiInit: - case ReserveRecordStatus.PendingLegiInit: - walletKycStatus = ExchangeWalletKycStatus.LegiInit; - break; - // FIXME: Do we handle the suspended state? - case ReserveRecordStatus.SuspendedLegi: - case ReserveRecordStatus.PendingLegi: - walletKycStatus = ExchangeWalletKycStatus.Legi; - break; - } - } + let walletKycStatus: ExchangeWalletKycStatus | undefined = + reserveRec && reserveRec.status + ? getKycStatusFromReserveStatus(reserveRec.status) + : undefined; const listItem: ExchangeListItem = { exchangeBaseUrl: r.baseUrl, @@ -2970,6 +2973,8 @@ export type BalanceThresholdCheckResult = | { result: "violation"; nextThreshold: AmountString; + walletKycStatus: ExchangeWalletKycStatus | undefined; + walletKycAccessToken: string | undefined; }; export async function checkIncomingAmountLegalUnderKycBalanceThreshold( @@ -3013,8 +3018,9 @@ export async function checkIncomingAmountLegalUnderKycBalanceThreshold( // Check if we already have KYC for a sufficient threshold. const reserveId = exchangeRec.currentMergeReserveRowId; + let reserveRec: ReserveRecord | undefined; if (reserveId) { - const reserveRec = await tx.reserves.get(reserveId); + reserveRec = await tx.reserves.get(reserveId); checkDbInvariant(!!reserveRec, "reserve"); // FIXME: also consider KYC expiration! if (reserveRec.thresholdNext) { @@ -3066,6 +3072,10 @@ export async function checkIncomingAmountLegalUnderKycBalanceThreshold( return { result: "violation", nextThreshold: limNext ?? limViolated, + walletKycStatus: reserveRec?.status + ? getKycStatusFromReserveStatus(reserveRec.status) + : undefined, + walletKycAccessToken: reserveRec?.kycAccessToken, }; } }, @@ -3073,47 +3083,6 @@ export async function checkIncomingAmountLegalUnderKycBalanceThreshold( } /** - * Wait until it is allowed for the user to add the given amount - * to the wallet balance, either because the balance is low enough - * or KYC was completed. - */ -export async function waitIncomingAmountLegalUnderKycBalanceThreshold( - wex: WalletExecutionContext, - exchangeBaseUrl: string, - amountExpected: AmountLike, -): Promise<void> { - await genericWaitForState(wex, { - async checkState(): Promise<boolean> { - const checkRes = await checkIncomingAmountLegalUnderKycBalanceThreshold( - wex, - exchangeBaseUrl, - amountExpected, - ); - logger.info( - `balance check result for ${exchangeBaseUrl} +${amountExpected}: ${j2s( - checkRes, - )}`, - ); - if (checkRes.result === "ok") { - return true; - } - await handleStartExchangeWalletKyc(wex, { - amount: checkRes.nextThreshold, - exchangeBaseUrl, - }); - return false; - }, - filterNotification(notif) { - return ( - (notif.type === NotificationType.ExchangeStateTransition && - notif.exchangeBaseUrl === exchangeBaseUrl) || - notif.type === NotificationType.BalanceChange - ); - }, - }); -} - -/** * Wait until kyc has passed for the wallet. * * If passed==false, already return when legitimization diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts index ac50f2e0e..e9a3970c6 100644 --- a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts +++ b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts @@ -24,6 +24,7 @@ import { CheckPeerPullCreditResponse, ContractTermsUtil, ExchangeReservePurseRequest, + ExchangeWalletKycStatus, HttpStatusCode, InitiatePeerPullCreditRequest, InitiatePeerPullCreditResponse, @@ -65,6 +66,7 @@ import { TransitionResult, TransitionResultType, constructTaskIdentifier, + genericWaitForStateVal, requireExchangeTosAcceptedOrThrow, runWithClientCancellation, } from "./common.js"; @@ -84,11 +86,11 @@ import { timestampPreciseToDb, } from "./db.js"; import { + BalanceThresholdCheckResult, checkIncomingAmountLegalUnderKycBalanceThreshold, fetchFreshExchange, getScopeForAllExchanges, handleStartExchangeWalletKyc, - waitIncomingAmountLegalUnderKycBalanceThreshold, } from "./exchanges.js"; import { codecForExchangePurseStatus, @@ -415,16 +417,20 @@ export class PeerPullCreditTransactionContext implements TransactionContext { case PeerPullPaymentCreditStatus.PendingBalanceKycRequired: newStatus = PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired; break; - case PeerPullPaymentCreditStatus.Done: + case PeerPullPaymentCreditStatus.PendingBalanceKycInit: + newStatus = PeerPullPaymentCreditStatus.SuspendedBalanceKycInit; + break; case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired: case PeerPullPaymentCreditStatus.SuspendedCreatePurse: case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: case PeerPullPaymentCreditStatus.SuspendedReady: case PeerPullPaymentCreditStatus.SuspendedWithdrawing: + case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit: + case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: + case PeerPullPaymentCreditStatus.Done: case PeerPullPaymentCreditStatus.Aborted: case PeerPullPaymentCreditStatus.Failed: case PeerPullPaymentCreditStatus.Expired: - case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: break; default: assertUnreachable(pullCreditRec.status); @@ -475,6 +481,8 @@ export class PeerPullCreditTransactionContext implements TransactionContext { case PeerPullPaymentCreditStatus.Expired: case PeerPullPaymentCreditStatus.PendingBalanceKycRequired: case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired: + case PeerPullPaymentCreditStatus.PendingBalanceKycInit: + case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit: break; case PeerPullPaymentCreditStatus.AbortingDeletePurse: case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: @@ -520,12 +528,16 @@ export class PeerPullCreditTransactionContext implements TransactionContext { case PeerPullPaymentCreditStatus.PendingWithdrawing: case PeerPullPaymentCreditStatus.PendingReady: case PeerPullPaymentCreditStatus.PendingBalanceKycRequired: + case PeerPullPaymentCreditStatus.PendingBalanceKycInit: case PeerPullPaymentCreditStatus.AbortingDeletePurse: case PeerPullPaymentCreditStatus.Done: case PeerPullPaymentCreditStatus.Failed: case PeerPullPaymentCreditStatus.Expired: case PeerPullPaymentCreditStatus.Aborted: break; + case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit: + newStatus = PeerPullPaymentCreditStatus.PendingBalanceKycInit; + break; case PeerPullPaymentCreditStatus.SuspendedCreatePurse: newStatus = PeerPullPaymentCreditStatus.PendingCreatePurse; break; @@ -581,6 +593,8 @@ export class PeerPullCreditTransactionContext implements TransactionContext { switch (pullCreditRec.status) { case PeerPullPaymentCreditStatus.PendingBalanceKycRequired: case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired: + case PeerPullPaymentCreditStatus.PendingBalanceKycInit: + case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit: case PeerPullPaymentCreditStatus.PendingCreatePurse: case PeerPullPaymentCreditStatus.PendingMergeKycRequired: newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse; @@ -942,7 +956,7 @@ async function handlePeerPullCreditCreatePurse( if (rec.status !== PeerPullPaymentCreditStatus.PendingCreatePurse) { return TransitionResult.stay(); } - rec.status = PeerPullPaymentCreditStatus.PendingBalanceKycRequired; + rec.status = PeerPullPaymentCreditStatus.PendingBalanceKycInit; return TransitionResult.transition(rec); }); return TaskRunResult.progress(); @@ -1114,6 +1128,7 @@ export async function processPeerPullCredit( case PeerPullPaymentCreditStatus.PendingWithdrawing: return handlePeerPullCreditWithdrawing(wex, pullIni); case PeerPullPaymentCreditStatus.PendingBalanceKycRequired: + case PeerPullPaymentCreditStatus.PendingBalanceKycInit: return processPeerPullCreditBalanceKyc(ctx, pullIni); case PeerPullPaymentCreditStatus.Aborted: case PeerPullPaymentCreditStatus.Failed: @@ -1124,6 +1139,7 @@ export async function processPeerPullCredit( case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: case PeerPullPaymentCreditStatus.SuspendedReady: case PeerPullPaymentCreditStatus.SuspendedWithdrawing: + case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit: break; default: assertUnreachable(pullIni.status); @@ -1139,22 +1155,80 @@ async function processPeerPullCreditBalanceKyc( const exchangeBaseUrl = peerInc.exchangeBaseUrl; const amount = peerInc.estimatedAmountEffective; - await waitIncomingAmountLegalUnderKycBalanceThreshold( - ctx.wex, - exchangeBaseUrl, - amount, - ); - await ctx.transition({}, async (rec) => { - if (!rec) { - return TransitionResult.stay(); - } - if (rec.status !== PeerPullPaymentCreditStatus.PendingBalanceKycRequired) { - return TransitionResult.stay(); - } - rec.status = PeerPullPaymentCreditStatus.PendingCreatePurse; - return TransitionResult.transition(rec); + const ret = await genericWaitForStateVal(ctx.wex, { + async checkState(): Promise<BalanceThresholdCheckResult | undefined> { + const checkRes = await checkIncomingAmountLegalUnderKycBalanceThreshold( + ctx.wex, + exchangeBaseUrl, + amount, + ); + logger.info( + `balance check result for ${exchangeBaseUrl} +${amount}: ${j2s( + checkRes, + )}`, + ); + if (checkRes.result === "ok") { + return checkRes; + } + if ( + peerInc.status === PeerPullPaymentCreditStatus.PendingBalanceKycInit && + checkRes.walletKycStatus === ExchangeWalletKycStatus.Legi + ) { + return checkRes; + } + await handleStartExchangeWalletKyc(ctx.wex, { + amount: checkRes.nextThreshold, + exchangeBaseUrl, + }); + return undefined; + }, + filterNotification(notif) { + return ( + (notif.type === NotificationType.ExchangeStateTransition && + notif.exchangeBaseUrl === exchangeBaseUrl) || + notif.type === NotificationType.BalanceChange + ); + }, }); - return TaskRunResult.progress(); + + if (ret.result === "ok") { + await ctx.transition({}, async (rec) => { + if (!rec) { + return TransitionResult.stay(); + } + if ( + rec.status !== PeerPullPaymentCreditStatus.PendingBalanceKycRequired + ) { + return TransitionResult.stay(); + } + rec.status = PeerPullPaymentCreditStatus.PendingCreatePurse; + return TransitionResult.transition(rec); + }); + return TaskRunResult.progress(); + } else if ( + peerInc.status === PeerPullPaymentCreditStatus.PendingBalanceKycInit && + ret.walletKycStatus === ExchangeWalletKycStatus.Legi + ) { + await ctx.transition({}, async (rec) => { + if (!rec) { + return TransitionResult.stay(); + } + if (rec.status !== PeerPullPaymentCreditStatus.PendingBalanceKycInit) { + return TransitionResult.stay(); + } + rec.status = PeerPullPaymentCreditStatus.PendingBalanceKycRequired; + delete rec.kycInfo; + rec.kycAccessToken = ret.walletKycAccessToken; + rec.kycUrl = new URL( + `kyc-spa/${ret.walletKycAccessToken}`, + exchangeBaseUrl, + ).href; + return TransitionResult.transition(rec); + }); + return TaskRunResult.progress(); + } else { + throw Error("not reached"); + } } async function processPeerPullCreditKycRequired( @@ -1547,6 +1621,16 @@ export function computePeerPullCreditTransactionState( major: TransactionMajorState.Suspended, minor: TransactionMinorState.BalanceKycRequired, }; + case PeerPullPaymentCreditStatus.PendingBalanceKycInit: + return { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.BalanceKycInit, + }; + case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit: + return { + major: TransactionMajorState.Suspended, + minor: TransactionMinorState.BalanceKycInit, + }; } } @@ -1606,5 +1690,9 @@ export function computePeerPullCreditTransactionActions( return [TransactionAction.Suspend, TransactionAction.Abort]; case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired: return [TransactionAction.Resume, TransactionAction.Abort]; + case PeerPullPaymentCreditStatus.PendingBalanceKycInit: + return [TransactionAction.Suspend, TransactionAction.Abort]; + case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit: + return [TransactionAction.Resume, TransactionAction.Abort]; } } diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts index 6f2b15d20..ed59f0d4d 100644 --- a/packages/taler-wallet-core/src/pay-peer-push-credit.ts +++ b/packages/taler-wallet-core/src/pay-peer-push-credit.ts @@ -21,6 +21,7 @@ import { ConfirmPeerPushCreditRequest, ContractTermsUtil, ExchangePurseMergeRequest, + ExchangeWalletKycStatus, HttpStatusCode, Logger, NotificationType, @@ -62,6 +63,7 @@ import { TransitionResult, TransitionResultType, constructTaskIdentifier, + genericWaitForStateVal, requireExchangeTosAcceptedOrThrow, } from "./common.js"; import { @@ -79,11 +81,11 @@ import { timestampPreciseToDb, } from "./db.js"; import { + BalanceThresholdCheckResult, checkIncomingAmountLegalUnderKycBalanceThreshold, fetchFreshExchange, getScopeForAllExchanges, handleStartExchangeWalletKyc, - waitIncomingAmountLegalUnderKycBalanceThreshold, } from "./exchanges.js"; import { codecForExchangePurseStatus, @@ -362,60 +364,48 @@ export class PeerPushCreditTransactionContext implements TransactionContext { } async suspendTransaction(): Promise<void> { - const { wex, peerPushCreditId, taskId: retryTag, transactionId } = this; - const transitionInfo = await wex.db.runReadWriteTx( - { storeNames: ["peerPushCredit", "transactionsMeta"] }, - async (tx) => { - const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId); - if (!pushCreditRec) { - logger.warn(`peer push credit ${peerPushCreditId} not found`); - return; - } - let newStatus: PeerPushCreditStatus | undefined = undefined; - switch (pushCreditRec.status) { - case PeerPushCreditStatus.DialogProposed: - case PeerPushCreditStatus.Done: - case PeerPushCreditStatus.SuspendedMerge: - case PeerPushCreditStatus.SuspendedMergeKycRequired: - case PeerPushCreditStatus.SuspendedWithdrawing: - case PeerPushCreditStatus.SuspendedBalanceKycRequired: - case PeerPushCreditStatus.Failed: - case PeerPushCreditStatus.Aborted: - break; - case PeerPushCreditStatus.PendingBalanceKycRequired: - newStatus = PeerPushCreditStatus.SuspendedBalanceKycRequired; - break; - case PeerPushCreditStatus.PendingMergeKycRequired: - newStatus = PeerPushCreditStatus.SuspendedMergeKycRequired; - break; - case PeerPushCreditStatus.PendingMerge: - newStatus = PeerPushCreditStatus.SuspendedMerge; - break; - case PeerPushCreditStatus.PendingWithdrawing: - // FIXME: Suspend internal withdrawal transaction! - newStatus = PeerPushCreditStatus.SuspendedWithdrawing; - break; - default: - assertUnreachable(pushCreditRec.status); - } - if (newStatus != null) { - const oldTxState = - computePeerPushCreditTransactionState(pushCreditRec); - pushCreditRec.status = newStatus; - const newTxState = - computePeerPushCreditTransactionState(pushCreditRec); - await tx.peerPushCredit.put(pushCreditRec); - await this.updateTransactionMeta(tx); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }, - ); - notifyTransition(wex, transactionId, transitionInfo); - wex.taskScheduler.stopShepherdTask(retryTag); + await this.transition({}, async (pushCreditRec) => { + if (!pushCreditRec) { + return TransitionResult.stay(); + } + let newStatus: PeerPushCreditStatus | undefined = undefined; + switch (pushCreditRec.status) { + case PeerPushCreditStatus.DialogProposed: + case PeerPushCreditStatus.Done: + case PeerPushCreditStatus.SuspendedMerge: + case PeerPushCreditStatus.SuspendedMergeKycRequired: + case PeerPushCreditStatus.SuspendedWithdrawing: + case PeerPushCreditStatus.SuspendedBalanceKycRequired: + case PeerPushCreditStatus.SuspendedBalanceKycInit: + case PeerPushCreditStatus.Failed: + case PeerPushCreditStatus.Aborted: + break; + case PeerPushCreditStatus.PendingBalanceKycRequired: + newStatus = PeerPushCreditStatus.SuspendedBalanceKycRequired; + break; + case PeerPushCreditStatus.PendingBalanceKycInit: + newStatus = PeerPushCreditStatus.SuspendedBalanceKycInit; + break; + case PeerPushCreditStatus.PendingMergeKycRequired: + newStatus = PeerPushCreditStatus.SuspendedMergeKycRequired; + break; + case PeerPushCreditStatus.PendingMerge: + newStatus = PeerPushCreditStatus.SuspendedMerge; + break; + case PeerPushCreditStatus.PendingWithdrawing: + // FIXME: Suspend internal withdrawal transaction! + newStatus = PeerPushCreditStatus.SuspendedWithdrawing; + break; + default: + assertUnreachable(pushCreditRec.status); + } + if (newStatus != null) { + pushCreditRec.status = newStatus; + return TransitionResult.transition(pushCreditRec); + } else { + return TransitionResult.stay(); + } + }); } async abortTransaction(): Promise<void> { @@ -443,6 +433,8 @@ export class PeerPushCreditTransactionContext implements TransactionContext { case PeerPushCreditStatus.PendingWithdrawing: case PeerPushCreditStatus.PendingMergeKycRequired: case PeerPushCreditStatus.PendingMerge: + case PeerPushCreditStatus.PendingBalanceKycInit: + case PeerPushCreditStatus.SuspendedBalanceKycInit: newStatus = PeerPushCreditStatus.Aborted; break; default: @@ -481,13 +473,14 @@ export class PeerPushCreditTransactionContext implements TransactionContext { let newStatus: PeerPushCreditStatus | undefined = undefined; switch (pushCreditRec.status) { case PeerPushCreditStatus.DialogProposed: - case PeerPushCreditStatus.Done: case PeerPushCreditStatus.PendingMergeKycRequired: case PeerPushCreditStatus.PendingMerge: case PeerPushCreditStatus.PendingWithdrawing: + case PeerPushCreditStatus.PendingBalanceKycRequired: + case PeerPushCreditStatus.PendingBalanceKycInit: + case PeerPushCreditStatus.Done: case PeerPushCreditStatus.Aborted: case PeerPushCreditStatus.Failed: - case PeerPushCreditStatus.PendingBalanceKycRequired: break; case PeerPushCreditStatus.SuspendedMerge: newStatus = PeerPushCreditStatus.PendingMerge; @@ -502,6 +495,9 @@ export class PeerPushCreditTransactionContext implements TransactionContext { case PeerPushCreditStatus.SuspendedBalanceKycRequired: newStatus = PeerPushCreditStatus.PendingBalanceKycRequired; break; + case PeerPushCreditStatus.SuspendedBalanceKycInit: + newStatus = PeerPushCreditStatus.PendingBalanceKycInit; + break; default: assertUnreachable(pushCreditRec.status); } @@ -551,6 +547,8 @@ export class PeerPushCreditTransactionContext implements TransactionContext { case PeerPushCreditStatus.SuspendedWithdrawing: case PeerPushCreditStatus.PendingBalanceKycRequired: case PeerPushCreditStatus.SuspendedBalanceKycRequired: + case PeerPushCreditStatus.PendingBalanceKycInit: + case PeerPushCreditStatus.SuspendedBalanceKycInit: newStatus = PeerPushCreditStatus.Failed; break; default: @@ -925,7 +923,7 @@ async function handlePendingMerge( if (rec.status !== PeerPushCreditStatus.PendingMerge) { return TransitionResult.stay(); } - rec.status = PeerPushCreditStatus.PendingBalanceKycRequired; + rec.status = PeerPushCreditStatus.PendingBalanceKycInit; return TransitionResult.transition(rec); }); return TaskRunResult.progress(); @@ -1181,6 +1179,7 @@ export async function processPeerPushCredit( case PeerPushCreditStatus.PendingWithdrawing: { return handlePendingWithdrawing(wex, peerInc); } + case PeerPushCreditStatus.PendingBalanceKycInit: case PeerPushCreditStatus.PendingBalanceKycRequired: { return await processPeerPushCreditBalanceKyc(ctx, peerInc); } @@ -1196,22 +1195,80 @@ async function processPeerPushCreditBalanceKyc( const exchangeBaseUrl = peerInc.exchangeBaseUrl; const amount = peerInc.estimatedAmountEffective; - await waitIncomingAmountLegalUnderKycBalanceThreshold( - ctx.wex, - exchangeBaseUrl, - amount, - ); - await ctx.transition({}, async (rec) => { - if (!rec) { - return TransitionResult.stay(); - } - if (rec.status !== PeerPushCreditStatus.PendingBalanceKycRequired) { - return TransitionResult.stay(); - } - rec.status = PeerPushCreditStatus.PendingMerge; - return TransitionResult.transition(rec); + const ret = await genericWaitForStateVal(ctx.wex, { + async checkState(): Promise<BalanceThresholdCheckResult | undefined> { + const checkRes = await checkIncomingAmountLegalUnderKycBalanceThreshold( + ctx.wex, + exchangeBaseUrl, + amount, + ); + logger.info( + `balance check result for ${exchangeBaseUrl} +${amount}: ${j2s( + checkRes, + )}`, + ); + if (checkRes.result === "ok") { + return checkRes; + } + if ( + peerInc.status === PeerPushCreditStatus.PendingBalanceKycInit && + checkRes.walletKycStatus === ExchangeWalletKycStatus.Legi + ) { + return checkRes; + } + await handleStartExchangeWalletKyc(ctx.wex, { + amount: checkRes.nextThreshold, + exchangeBaseUrl, + }); + return undefined; + }, + filterNotification(notif) { + return ( + (notif.type === NotificationType.ExchangeStateTransition && + notif.exchangeBaseUrl === exchangeBaseUrl) || + notif.type === NotificationType.BalanceChange + ); + }, }); - return TaskRunResult.progress(); + + if (ret.result === "ok") { + await ctx.transition({}, async (rec) => { + if (!rec) { + return TransitionResult.stay(); + } + if ( + rec.status !== PeerPushCreditStatus.PendingBalanceKycRequired + ) { + return TransitionResult.stay(); + } + rec.status = PeerPushCreditStatus.PendingMerge; + return TransitionResult.transition(rec); + }); + return TaskRunResult.progress(); + } else if ( + peerInc.status === PeerPushCreditStatus.PendingBalanceKycInit && + ret.walletKycStatus === ExchangeWalletKycStatus.Legi + ) { + await ctx.transition({}, async (rec) => { + if (!rec) { + return TransitionResult.stay(); + } + if (rec.status !== PeerPushCreditStatus.PendingBalanceKycInit) { + return TransitionResult.stay(); + } + rec.status = PeerPushCreditStatus.PendingBalanceKycRequired; + delete rec.kycInfo; + rec.kycAccessToken = ret.walletKycAccessToken; + rec.kycUrl = new URL( + `kyc-spa/${ret.walletKycAccessToken}`, + exchangeBaseUrl, + ).href; + return TransitionResult.transition(rec); + }); + return TaskRunResult.progress(); + } else { + throw Error("not reached"); + } } export async function confirmPeerPushCredit( @@ -1325,6 +1382,16 @@ export function computePeerPushCreditTransactionState( major: TransactionMajorState.Suspended, minor: TransactionMinorState.BalanceKycRequired, }; + case PeerPushCreditStatus.PendingBalanceKycInit: + return { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.BalanceKycInit, + }; + case PeerPushCreditStatus.SuspendedBalanceKycInit: + return { + major: TransactionMajorState.Suspended, + minor: TransactionMinorState.BalanceKycInit, + }; default: assertUnreachable(pushCreditRecord.status); } @@ -1366,6 +1433,10 @@ export function computePeerPushCreditTransactionActions( return [TransactionAction.Suspend, TransactionAction.Abort]; case PeerPushCreditStatus.SuspendedBalanceKycRequired: return [TransactionAction.Resume, TransactionAction.Abort]; + case PeerPushCreditStatus.PendingBalanceKycInit: + return [TransactionAction.Suspend, TransactionAction.Abort]; + case PeerPushCreditStatus.SuspendedBalanceKycInit: + return [TransactionAction.Resume, TransactionAction.Abort]; case PeerPushCreditStatus.Aborted: return [TransactionAction.Delete]; case PeerPushCreditStatus.Failed: diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts index d4038f008..8c56adb49 100644 --- a/packages/taler-wallet-core/src/withdraw.ts +++ b/packages/taler-wallet-core/src/withdraw.ts @@ -47,6 +47,7 @@ import { ExchangeBatchWithdrawRequest, ExchangeListItem, ExchangeUpdateStatus, + ExchangeWalletKycStatus, ExchangeWireAccount, ExchangeWithdrawBatchResponse, ExchangeWithdrawRequest, @@ -122,6 +123,7 @@ import { TransitionResultType, constructTaskIdentifier, genericWaitForState, + genericWaitForStateVal, makeCoinAvailable, makeCoinsVisible, requireExchangeTosAcceptedOrThrow, @@ -157,6 +159,7 @@ import { } from "./denomSelection.js"; import { isWithdrawableDenom } from "./denominations.js"; import { + BalanceThresholdCheckResult, ExchangeWireDetails, ReadyExchangeSummary, checkIncomingAmountLegalUnderKycBalanceThreshold, @@ -168,7 +171,6 @@ import { listExchanges, lookupExchangeByUri, markExchangeUsed, - waitIncomingAmountLegalUnderKycBalanceThreshold, } from "./exchanges.js"; import { DbAccess } from "./query.js"; import { @@ -629,6 +631,8 @@ export class WithdrawTransactionContext implements TransactionContext { case WithdrawalGroupStatus.PendingQueryingStatus: case WithdrawalGroupStatus.PendingBalanceKyc: case WithdrawalGroupStatus.SuspendedBalanceKyc: + case WithdrawalGroupStatus.PendingBalanceKycInit: + case WithdrawalGroupStatus.SuspendedBalanceKycInit: newStatus = WithdrawalGroupStatus.AbortedExchange; break; case WithdrawalGroupStatus.PendingReady: @@ -845,6 +849,16 @@ export function computeWithdrawalTransactionStatus( major: TransactionMajorState.Suspended, minor: TransactionMinorState.BalanceKycRequired, }; + case WithdrawalGroupStatus.PendingBalanceKycInit: + return { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.BalanceKycInit, + }; + case WithdrawalGroupStatus.SuspendedBalanceKycInit: + return { + major: TransactionMajorState.Suspended, + minor: TransactionMinorState.BalanceKycInit, + }; } } @@ -924,6 +938,14 @@ export function computeWithdrawalTransactionActions( ]; case WithdrawalGroupStatus.SuspendedBalanceKyc: return [TransactionAction.Resume, TransactionAction.Abort]; + case WithdrawalGroupStatus.PendingBalanceKycInit: + return [ + TransactionAction.Suspend, + TransactionAction.Retry, + TransactionAction.Abort, + ]; + case WithdrawalGroupStatus.SuspendedBalanceKycInit: + return [TransactionAction.Resume, TransactionAction.Abort]; } } @@ -943,22 +965,80 @@ async function processWithdrawalGroupBalanceKyc( "invalid state (expected withdrawal group to have effective withdrawal amount)", ); } - await waitIncomingAmountLegalUnderKycBalanceThreshold( - ctx.wex, - exchangeBaseUrl, - amount, - ); - await ctx.transition({}, async (wg) => { - if (!wg) { - return TransitionResult.stay(); - } - if (wg.status !== WithdrawalGroupStatus.PendingBalanceKyc) { - return TransitionResult.stay(); - } - wg.status = WithdrawalGroupStatus.PendingReady; - return TransitionResult.transition(wg); + + const ret = await genericWaitForStateVal(ctx.wex, { + async checkState(): Promise<BalanceThresholdCheckResult | undefined> { + const checkRes = await checkIncomingAmountLegalUnderKycBalanceThreshold( + ctx.wex, + exchangeBaseUrl, + amount, + ); + logger.info( + `balance check result for ${exchangeBaseUrl} +${amount}: ${j2s( + checkRes, + )}`, + ); + if (checkRes.result === "ok") { + return checkRes; + } + if ( + withdrawalGroup.status === + WithdrawalGroupStatus.PendingBalanceKycInit && + checkRes.walletKycStatus === ExchangeWalletKycStatus.Legi + ) { + return checkRes; + } + await handleStartExchangeWalletKyc(ctx.wex, { + amount: checkRes.nextThreshold, + exchangeBaseUrl, + }); + return undefined; + }, + filterNotification(notif) { + return ( + (notif.type === NotificationType.ExchangeStateTransition && + notif.exchangeBaseUrl === exchangeBaseUrl) || + notif.type === NotificationType.BalanceChange + ); + }, }); - return TaskRunResult.progress(); + + if (ret.result === "ok") { + await ctx.transition({}, async (wg) => { + if (!wg) { + return TransitionResult.stay(); + } + if (wg.status !== WithdrawalGroupStatus.PendingBalanceKyc) { + return TransitionResult.stay(); + } + wg.status = WithdrawalGroupStatus.PendingReady; + return TransitionResult.transition(wg); + }); + return TaskRunResult.progress(); + } else if ( + withdrawalGroup.status === WithdrawalGroupStatus.PendingBalanceKycInit && + ret.walletKycStatus === ExchangeWalletKycStatus.Legi + ) { + await ctx.transition({}, async (wg) => { + if (!wg) { + return TransitionResult.stay(); + } + if (wg.status !== WithdrawalGroupStatus.PendingBalanceKycInit) { + return TransitionResult.stay(); + } + wg.status = WithdrawalGroupStatus.PendingBalanceKyc; + wg.kycPending = undefined; + wg.kycAccessToken = ret.walletKycAccessToken; + wg.kycUrl = new URL( + `kyc-spa/${ret.walletKycAccessToken}`, + exchangeBaseUrl, + ).href; + return TransitionResult.transition(wg); + }); + return TaskRunResult.progress(); + } else { + throw Error("not reached"); + } } async function processWithdrawalGroupDialogProposed( @@ -2194,7 +2274,7 @@ async function processWithdrawalGroupPendingReady( if (!wg) { return TransitionResult.stay(); } - wg.status = WithdrawalGroupStatus.PendingBalanceKyc; + wg.status = WithdrawalGroupStatus.PendingBalanceKycInit; return TransitionResult.transition(wg); }); return TaskRunResult.progress(); @@ -2389,6 +2469,7 @@ export async function processWithdrawalGroup( case WithdrawalGroupStatus.DialogProposed: return await processWithdrawalGroupDialogProposed(ctx, withdrawalGroup); case WithdrawalGroupStatus.PendingBalanceKyc: + case WithdrawalGroupStatus.PendingBalanceKycInit: return await processWithdrawalGroupBalanceKyc(ctx, withdrawalGroup); case WithdrawalGroupStatus.AbortedBank: case WithdrawalGroupStatus.AbortedExchange: @@ -2400,6 +2481,7 @@ export async function processWithdrawalGroup( case WithdrawalGroupStatus.SuspendedRegisteringBank: case WithdrawalGroupStatus.SuspendedWaitConfirmBank: case WithdrawalGroupStatus.SuspendedBalanceKyc: + case WithdrawalGroupStatus.SuspendedBalanceKycInit: case WithdrawalGroupStatus.Done: case WithdrawalGroupStatus.FailedBankAborted: case WithdrawalGroupStatus.AbortedUserRefused: |