commit fc2b4c6b7baa2810571f6e308e5a97e65ab57a79
parent a5603fd32c3775b64979389c4d73bb65e17e5f5e
Author: Florian Dold <florian@dold.me>
Date: Wed, 18 Mar 2026 17:38:40 +0100
wallet-core: refactor withdrawal transitions
Diffstat:
2 files changed, 386 insertions(+), 548 deletions(-)
diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts
@@ -824,43 +824,6 @@ export namespace TaskIdentifiers {
}
/**
- * Result of a transaction transition.
- */
-export enum TransitionResultType {
- Transition = 1,
- Stay = 2,
- Delete = 3,
-}
-
-export type TransitionResult<R> =
- | { type: TransitionResultType.Stay }
- | {
- type: TransitionResultType.Transition;
- rec: R;
- balanceEffect: BalanceEffect;
- }
- | { type: TransitionResultType.Delete };
-
-export const TransitionResult = {
- stay<T>(): TransitionResult<T> {
- return { type: TransitionResultType.Stay };
- },
- delete<T>(): TransitionResult<T> {
- return { type: TransitionResultType.Delete };
- },
- transition<T>(
- rec: T,
- balanceEffect: BalanceEffect = BalanceEffect.Any,
- ): TransitionResult<T> {
- return {
- type: TransitionResultType.Transition,
- rec,
- balanceEffect,
- };
- },
-};
-
-/**
* Transaction context.
* Uniform interface to all transactions.
*/
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
@@ -116,17 +116,17 @@ import {
} from "@gnu-taler/taler-util/http";
import {
PendingTaskType,
+ RecordHandle,
TaskIdStr,
TaskRunResult,
TaskRunResultType,
TransactionContext,
- TransitionResult,
- TransitionResultType,
cancelableFetch,
cancelableLongPoll,
constructTaskIdentifier,
genericWaitForState,
genericWaitForStateVal,
+ getGenericRecordHandle,
makeCoinAvailable,
makeCoinsVisible,
requireExchangeTosAcceptedOrThrow,
@@ -144,8 +144,6 @@ import {
WalletDbAllStoresReadOnlyTransaction,
WalletDbReadOnlyTransaction,
WalletDbReadWriteTransaction,
- WalletDbStoresArr,
- WalletStoresV1,
WgInfo,
WithdrawalGroupRecord,
WithdrawalGroupStatus,
@@ -187,7 +185,6 @@ import {
isKycOperationDue,
runKycCheckAlgo,
} from "./kyc.js";
-import { DbAccess } from "./query.js";
import {
BalanceEffect,
TransitionInfo,
@@ -487,102 +484,27 @@ export class WithdrawTransactionContext implements TransactionContext {
// 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.
- */
- async transition<StoreNameArray extends WalletDbStoresArr = []>(
- opts: { extraStores?: StoreNameArray; transactionLabel?: string },
- f: (
- rec: WithdrawalGroupRecord | undefined,
- tx: WalletDbReadWriteTransaction<
- [
- "withdrawalGroups",
- "transactionsMeta",
- "operationRetries",
- "exchanges",
- "exchangeDetails",
- ...StoreNameArray,
- ]
- >,
- ) => Promise<TransitionResult<WithdrawalGroupRecord>>,
- ): Promise<boolean> {
- const baseStores = [
- "withdrawalGroups" as const,
- "transactionsMeta" as const,
- "operationRetries" as const,
- "exchanges" as const,
- "exchangeDetails" as const,
- ];
- const stores = opts.extraStores
- ? [...baseStores, ...opts.extraStores]
- : baseStores;
-
- let errorThrown: Error | undefined;
- const didTransition: boolean = await this.wex.runLegacyWalletDbTx(
- async (tx) => {
- const wgRec = await tx.withdrawalGroups.get(this.withdrawalGroupId);
- let oldTxState: TransactionState;
- let oldStId: number;
- if (wgRec) {
- oldTxState = computeWithdrawalTransactionStatus(wgRec);
- oldStId = wgRec.status;
- } else {
- oldTxState = {
- major: TransactionMajorState.None,
- };
- oldStId = 0;
- }
- let res: TransitionResult<WithdrawalGroupRecord> | undefined;
- try {
- res = await f(wgRec, tx);
- } catch (error) {
- if (error instanceof Error) {
- errorThrown = error;
- }
- return false;
- }
-
- switch (res.type) {
- case TransitionResultType.Transition: {
- await tx.withdrawalGroups.put(res.rec);
- await this.updateTransactionMeta(tx);
- const newTxState = computeWithdrawalTransactionStatus(res.rec);
- applyNotifyTransition(tx.notify, this.transactionId, {
- oldTxState,
- newTxState,
- balanceEffect: res.balanceEffect,
- oldStId,
- newStId: res.rec.status,
- });
- return true;
- }
- case TransitionResultType.Delete:
- await tx.withdrawalGroups.delete(this.withdrawalGroupId);
- await this.updateTransactionMeta(tx);
- applyNotifyTransition(tx.notify, this.transactionId, {
- oldTxState,
- newTxState: {
- major: TransactionMajorState.None,
- },
- balanceEffect: BalanceEffect.Any,
- oldStId,
- newStId: -1,
- });
- return true;
- default:
- return false;
- }
+ async getRecordHandle(
+ tx: WalletDbReadWriteTransaction<["withdrawalGroups", "transactionsMeta"]>,
+ ): Promise<
+ [WithdrawalGroupRecord | undefined, RecordHandle<WithdrawalGroupRecord>]
+ > {
+ return getGenericRecordHandle<WithdrawalGroupRecord>(
+ this,
+ tx as any,
+ async () => tx.withdrawalGroups.get(this.withdrawalGroupId),
+ async (r) => {
+ await tx.withdrawalGroups.put(r);
},
+ async () => tx.withdrawalGroups.delete(this.withdrawalGroupId),
+ (r) => computeWithdrawalTransactionStatus(r),
+ (r) => r.status,
+ () => this.updateTransactionMeta(tx),
);
- if (errorThrown) {
- throw errorThrown;
- }
- return didTransition;
}
async deleteTransaction(): Promise<void> {
- const res = await this.wex.runLegacyWalletDbTx(async (tx) => {
+ await this.wex.runLegacyWalletDbTx(async (tx) => {
return this.deleteTransactionInTx(tx);
});
}
@@ -620,178 +542,160 @@ export class WithdrawTransactionContext implements TransactionContext {
async suspendTransaction(): Promise<void> {
const { withdrawalGroupId } = this;
- await this.transition(
- {
- transactionLabel: "suspend-transaction-withdraw",
- },
- async (wg, _tx) => {
- if (!wg) {
- logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
- return TransitionResult.stay();
- }
- let newStatus: WithdrawalGroupStatus | undefined = undefined;
- switch (wg.status) {
- case WithdrawalGroupStatus.PendingReady:
- newStatus = WithdrawalGroupStatus.SuspendedReady;
- break;
- case WithdrawalGroupStatus.AbortingBank:
- newStatus = WithdrawalGroupStatus.SuspendedAbortingBank;
- break;
- case WithdrawalGroupStatus.PendingWaitConfirmBank:
- newStatus = WithdrawalGroupStatus.SuspendedWaitConfirmBank;
- break;
- case WithdrawalGroupStatus.PendingRegisteringBank:
- newStatus = WithdrawalGroupStatus.SuspendedRegisteringBank;
- break;
- case WithdrawalGroupStatus.PendingQueryingStatus:
- newStatus = WithdrawalGroupStatus.SuspendedQueryingStatus;
- break;
- case WithdrawalGroupStatus.PendingKyc:
- newStatus = WithdrawalGroupStatus.SuspendedKyc;
- break;
- default:
- logger.warn(
- `Unsupported 'suspend' on withdrawal transaction in status ${wg.status}`,
- );
- return TransitionResult.stay();
- }
- wg.status = newStatus;
- return TransitionResult.transition(wg);
- },
- );
+ await this.wex.runLegacyWalletDbTx(async (tx) => {
+ const [wg, h] = await this.getRecordHandle(tx);
+ if (!wg) {
+ logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
+ return;
+ }
+ let newStatus: WithdrawalGroupStatus | undefined = undefined;
+ switch (wg.status) {
+ case WithdrawalGroupStatus.PendingReady:
+ newStatus = WithdrawalGroupStatus.SuspendedReady;
+ break;
+ case WithdrawalGroupStatus.AbortingBank:
+ newStatus = WithdrawalGroupStatus.SuspendedAbortingBank;
+ break;
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
+ newStatus = WithdrawalGroupStatus.SuspendedWaitConfirmBank;
+ break;
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ newStatus = WithdrawalGroupStatus.SuspendedRegisteringBank;
+ break;
+ case WithdrawalGroupStatus.PendingQueryingStatus:
+ newStatus = WithdrawalGroupStatus.SuspendedQueryingStatus;
+ break;
+ case WithdrawalGroupStatus.PendingKyc:
+ newStatus = WithdrawalGroupStatus.SuspendedKyc;
+ break;
+ default:
+ logger.warn(
+ `Unsupported 'suspend' on withdrawal transaction in status ${wg.status}`,
+ );
+ return;
+ }
+ wg.status = newStatus;
+ await h.update(wg);
+ });
}
async abortTransaction(reason?: TalerErrorDetail): Promise<void> {
const { withdrawalGroupId } = this;
- await this.transition(
- {
- transactionLabel: "abort-transaction-withdraw",
- },
- async (wg, _tx) => {
- // FIXME: When aborting a partially succeeded withdrawal,
- // we need to mark already withdrawn coins as visible.
- if (!wg) {
- logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
- return TransitionResult.stay();
- }
- let newStatus: WithdrawalGroupStatus | undefined = undefined;
- switch (wg.status) {
- case WithdrawalGroupStatus.SuspendedRegisteringBank:
- case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
- case WithdrawalGroupStatus.PendingWaitConfirmBank:
- case WithdrawalGroupStatus.PendingRegisteringBank:
- newStatus = WithdrawalGroupStatus.AbortingBank;
- break;
- case WithdrawalGroupStatus.SuspendedKyc:
- case WithdrawalGroupStatus.SuspendedQueryingStatus:
- case WithdrawalGroupStatus.SuspendedReady:
- case WithdrawalGroupStatus.PendingKyc:
- case WithdrawalGroupStatus.PendingQueryingStatus:
- case WithdrawalGroupStatus.PendingBalanceKyc:
- case WithdrawalGroupStatus.SuspendedBalanceKyc:
- case WithdrawalGroupStatus.PendingBalanceKycInit:
- case WithdrawalGroupStatus.SuspendedBalanceKycInit:
- newStatus = WithdrawalGroupStatus.AbortedExchange;
- break;
- case WithdrawalGroupStatus.PendingReady:
- case WithdrawalGroupStatus.SuspendedRedenominate:
- case WithdrawalGroupStatus.PendingRedenominate:
- newStatus = WithdrawalGroupStatus.SuspendedReady;
- break;
- case WithdrawalGroupStatus.SuspendedAbortingBank:
- case WithdrawalGroupStatus.AbortingBank:
- case WithdrawalGroupStatus.AbortedUserRefused:
- // No transition needed, but not an error
- return TransitionResult.stay();
- case WithdrawalGroupStatus.DialogProposed:
- newStatus = WithdrawalGroupStatus.AbortedUserRefused;
- break;
- case WithdrawalGroupStatus.Done:
- case WithdrawalGroupStatus.FailedBankAborted:
- case WithdrawalGroupStatus.AbortedExchange:
- case WithdrawalGroupStatus.AbortedBank:
- case WithdrawalGroupStatus.FailedAbortingBank:
- case WithdrawalGroupStatus.AbortedOtherWallet:
- // Not allowed
- throw Error("abort not allowed in current state");
- default:
- assertUnreachable(wg.status);
- }
- wg.abortReason = reason;
- wg.status = newStatus;
- return TransitionResult.transition(wg);
- },
- );
+ await this.wex.runLegacyWalletDbTx(async (tx) => {
+ const [wg, h] = await this.getRecordHandle(tx);
+ if (!wg) {
+ logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
+ return;
+ }
+ let newStatus: WithdrawalGroupStatus | undefined = undefined;
+ switch (wg.status) {
+ case WithdrawalGroupStatus.SuspendedRegisteringBank:
+ case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ newStatus = WithdrawalGroupStatus.AbortingBank;
+ break;
+ case WithdrawalGroupStatus.SuspendedKyc:
+ case WithdrawalGroupStatus.SuspendedQueryingStatus:
+ case WithdrawalGroupStatus.SuspendedReady:
+ case WithdrawalGroupStatus.PendingKyc:
+ case WithdrawalGroupStatus.PendingQueryingStatus:
+ case WithdrawalGroupStatus.PendingBalanceKyc:
+ case WithdrawalGroupStatus.SuspendedBalanceKyc:
+ case WithdrawalGroupStatus.PendingBalanceKycInit:
+ case WithdrawalGroupStatus.SuspendedBalanceKycInit:
+ newStatus = WithdrawalGroupStatus.AbortedExchange;
+ break;
+ case WithdrawalGroupStatus.PendingReady:
+ case WithdrawalGroupStatus.SuspendedRedenominate:
+ case WithdrawalGroupStatus.PendingRedenominate:
+ newStatus = WithdrawalGroupStatus.SuspendedReady;
+ break;
+ case WithdrawalGroupStatus.SuspendedAbortingBank:
+ case WithdrawalGroupStatus.AbortingBank:
+ case WithdrawalGroupStatus.AbortedUserRefused:
+ // No transition needed, but not an error
+ return;
+ case WithdrawalGroupStatus.DialogProposed:
+ newStatus = WithdrawalGroupStatus.AbortedUserRefused;
+ break;
+ case WithdrawalGroupStatus.Done:
+ case WithdrawalGroupStatus.FailedBankAborted:
+ case WithdrawalGroupStatus.AbortedExchange:
+ case WithdrawalGroupStatus.AbortedBank:
+ case WithdrawalGroupStatus.FailedAbortingBank:
+ case WithdrawalGroupStatus.AbortedOtherWallet:
+ // Not allowed
+ throw Error("abort not allowed in current state");
+ default:
+ assertUnreachable(wg.status);
+ }
+ wg.abortReason = reason;
+ wg.status = newStatus;
+ await h.update(wg);
+ });
}
async resumeTransaction(): Promise<void> {
const { withdrawalGroupId } = this;
- await this.transition(
- {
- transactionLabel: "resume-transaction-withdraw",
- },
- async (wg, _tx) => {
- if (!wg) {
- logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
- return TransitionResult.stay();
- }
- let newStatus: WithdrawalGroupStatus | undefined = undefined;
- switch (wg.status) {
- case WithdrawalGroupStatus.SuspendedReady:
- newStatus = WithdrawalGroupStatus.PendingReady;
- break;
- case WithdrawalGroupStatus.SuspendedAbortingBank:
- newStatus = WithdrawalGroupStatus.AbortingBank;
- break;
- case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
- newStatus = WithdrawalGroupStatus.PendingWaitConfirmBank;
- break;
- case WithdrawalGroupStatus.SuspendedQueryingStatus:
- newStatus = WithdrawalGroupStatus.PendingQueryingStatus;
- break;
- case WithdrawalGroupStatus.SuspendedRegisteringBank:
- newStatus = WithdrawalGroupStatus.PendingRegisteringBank;
- break;
- case WithdrawalGroupStatus.SuspendedKyc:
- newStatus = WithdrawalGroupStatus.PendingKyc;
- break;
- default:
- logger.warn(
- `Unsupported 'resume' on withdrawal transaction in status ${wg.status}`,
- );
- return TransitionResult.stay();
- }
- wg.status = newStatus;
- return TransitionResult.transition(wg);
- },
- );
+ await this.wex.runLegacyWalletDbTx(async (tx) => {
+ const [wg, h] = await this.getRecordHandle(tx);
+ if (!wg) {
+ logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
+ return;
+ }
+ let newStatus: WithdrawalGroupStatus | undefined = undefined;
+ switch (wg.status) {
+ case WithdrawalGroupStatus.SuspendedReady:
+ newStatus = WithdrawalGroupStatus.PendingReady;
+ break;
+ case WithdrawalGroupStatus.SuspendedAbortingBank:
+ newStatus = WithdrawalGroupStatus.AbortingBank;
+ break;
+ case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
+ newStatus = WithdrawalGroupStatus.PendingWaitConfirmBank;
+ break;
+ case WithdrawalGroupStatus.SuspendedQueryingStatus:
+ newStatus = WithdrawalGroupStatus.PendingQueryingStatus;
+ break;
+ case WithdrawalGroupStatus.SuspendedRegisteringBank:
+ newStatus = WithdrawalGroupStatus.PendingRegisteringBank;
+ break;
+ case WithdrawalGroupStatus.SuspendedKyc:
+ newStatus = WithdrawalGroupStatus.PendingKyc;
+ break;
+ default:
+ logger.warn(
+ `Unsupported 'resume' on withdrawal transaction in status ${wg.status}`,
+ );
+ return;
+ }
+ wg.status = newStatus;
+ await h.update(wg);
+ });
}
async failTransaction(reason?: TalerErrorDetail): Promise<void> {
const { withdrawalGroupId } = this;
- await this.transition(
- {
- transactionLabel: "fail-transaction-withdraw",
- },
- async (wg, _tx) => {
- if (!wg) {
- logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
- return TransitionResult.stay();
- }
- let newStatus: WithdrawalGroupStatus | undefined = undefined;
- switch (wg.status) {
- case WithdrawalGroupStatus.SuspendedAbortingBank:
- case WithdrawalGroupStatus.AbortingBank:
- newStatus = WithdrawalGroupStatus.FailedAbortingBank;
- break;
- default:
- return TransitionResult.stay();
- }
- wg.status = newStatus;
- wg.failReason = reason;
- return TransitionResult.transition(wg);
- },
- );
+ await this.wex.runLegacyWalletDbTx(async (tx) => {
+ const [wg, h] = await this.getRecordHandle(tx);
+ if (!wg) {
+ logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
+ return;
+ }
+ let newStatus: WithdrawalGroupStatus | undefined = undefined;
+ switch (wg.status) {
+ case WithdrawalGroupStatus.SuspendedAbortingBank:
+ case WithdrawalGroupStatus.AbortingBank:
+ newStatus = WithdrawalGroupStatus.FailedAbortingBank;
+ break;
+ default:
+ return;
+ }
+ wg.status = newStatus;
+ wg.failReason = reason;
+ await h.update(wg);
+ });
}
}
@@ -1065,20 +969,18 @@ async function processWithdrawalGroupRedenominate(
exchangeBaseUrl,
withdrawalGroup.withdrawalGroupId,
);
- const didTransition = await ctx.transition({}, async (rec) => {
+ return await ctx.wex.runLegacyWalletDbTx(async (tx) => {
+ const [rec, h] = await ctx.getRecordHandle(tx);
switch (rec?.status) {
case WithdrawalGroupStatus.PendingRedenominate:
break;
default:
- return TransitionResult.stay();
+ return TaskRunResult.backoff();
}
rec.status = WithdrawalGroupStatus.PendingReady;
- return TransitionResult.transition(rec);
+ await h.update(rec);
+ return TaskRunResult.progress();
});
- if (!didTransition) {
- return TaskRunResult.backoff();
- }
- return TaskRunResult.progress();
}
async function processWithdrawalGroupBalanceKyc(
@@ -1144,43 +1046,41 @@ async function processWithdrawalGroupBalanceKyc(
});
if (ret.result === "ok") {
- const transitionInfo = await ctx.transition({}, async (wg) => {
+ return await ctx.wex.runLegacyWalletDbTx(async (tx) => {
+ const [wg, h] = await ctx.getRecordHandle(tx);
if (!wg) {
- return TransitionResult.stay();
+ return TaskRunResult.finished();
}
switch (wg.status) {
case WithdrawalGroupStatus.PendingBalanceKyc:
case WithdrawalGroupStatus.PendingBalanceKycInit: {
wg.status = WithdrawalGroupStatus.PendingReady;
- return TransitionResult.transition(wg);
+ await h.update(wg);
+ return TaskRunResult.progress();
}
default: {
- return TransitionResult.stay();
+ return TaskRunResult.backoff();
}
}
});
- if (transitionInfo) {
- return TaskRunResult.progress();
- } else {
- return TaskRunResult.backoff();
- }
} else if (
withdrawalGroup.status === WithdrawalGroupStatus.PendingBalanceKycInit &&
ret.walletKycStatus === ExchangeWalletKycStatus.Legi
) {
- await ctx.transition({}, async (wg) => {
+ return await ctx.wex.runLegacyWalletDbTx(async (tx) => {
+ const [wg, h] = await ctx.getRecordHandle(tx);
if (!wg) {
- return TransitionResult.stay();
+ return TaskRunResult.finished();
}
if (wg.status !== WithdrawalGroupStatus.PendingBalanceKycInit) {
- return TransitionResult.stay();
+ return TaskRunResult.backoff();
}
wg.status = WithdrawalGroupStatus.PendingBalanceKyc;
wg.kycAccessToken = ret.walletKycAccessToken;
delete wg.kycPaytoHash;
- return TransitionResult.transition(wg);
+ await h.update(wg);
+ return TaskRunResult.progress();
});
- return TaskRunResult.progress();
} else {
throw Error("not reached");
}
@@ -1197,14 +1097,14 @@ async function transitionSimple(
from: WithdrawalGroupStatus,
to: WithdrawalGroupStatus,
): Promise<void> {
- await ctx.transition({}, async (rec) => {
+ await ctx.wex.runLegacyWalletDbTx(async (tx) => {
+ const [rec, h] = await ctx.getRecordHandle(tx);
switch (rec?.status) {
case from: {
rec.status = to;
- return TransitionResult.transition(rec);
+ await h.update(rec);
}
}
- return TransitionResult.stay();
});
}
@@ -1598,38 +1498,34 @@ async function transitionKycRequired(
const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId);
- await ctx.transition(
- {
- extraStores: ["planchets"],
- },
- async (wg2, tx) => {
- if (!wg2) {
- return TransitionResult.stay();
- }
- for (let i = startIdx; i < requestCoinIdxs.length; i++) {
- const planchet = await tx.planchets.indexes.byGroupAndIndex.get([
- withdrawalGroup.withdrawalGroupId,
- requestCoinIdxs[i],
- ]);
- if (!planchet) {
- continue;
- }
- planchet.planchetStatus = PlanchetStatus.KycRequired;
- await tx.planchets.put(planchet);
- }
- switch (wg2.status) {
- case WithdrawalGroupStatus.PendingReady:
- case WithdrawalGroupStatus.PendingKyc:
- break;
- default:
- return TransitionResult.stay();
+ await ctx.wex.runLegacyWalletDbTx(async (tx) => {
+ const [wg2, h] = await ctx.getRecordHandle(tx);
+ if (!wg2) {
+ return;
+ }
+ for (let i = startIdx; i < requestCoinIdxs.length; i++) {
+ const planchet = await tx.planchets.indexes.byGroupAndIndex.get([
+ withdrawalGroup.withdrawalGroupId,
+ requestCoinIdxs[i],
+ ]);
+ if (!planchet) {
+ continue;
}
- wg2.kycPaytoHash = legiRequiredResp.h_payto;
- wg2.kycLastDeny = timestampPreciseToDb(TalerPreciseTimestamp.now());
- wg2.status = WithdrawalGroupStatus.PendingKyc;
- return TransitionResult.transition(wg2);
- },
- );
+ planchet.planchetStatus = PlanchetStatus.KycRequired;
+ await tx.planchets.put(planchet);
+ }
+ switch (wg2.status) {
+ case WithdrawalGroupStatus.PendingReady:
+ case WithdrawalGroupStatus.PendingKyc:
+ break;
+ default:
+ return;
+ }
+ wg2.kycPaytoHash = legiRequiredResp.h_payto;
+ wg2.kycLastDeny = timestampPreciseToDb(TalerPreciseTimestamp.now());
+ wg2.status = WithdrawalGroupStatus.PendingKyc;
+ await h.update(wg2);
+ });
}
/**
@@ -2314,64 +2210,50 @@ async function processQueryReserve(
);
}
- const transitionResult = await ctx.transition(
- {
- extraStores: [
- "denominations",
- "denominationFamilies",
- "bankAccountsV2",
- "planchets",
- ],
- },
- async (wg, tx) => {
- if (!wg) {
- logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
- return TransitionResult.stay();
- }
- if (wg.status !== WithdrawalGroupStatus.PendingQueryingStatus) {
- return TransitionResult.stay();
- }
- const lastOrigin = result.response.last_origin;
- // If the withdrawal had external confirmation, we don't store the
- // bank account details learned via the reserve here.
- const externalConfirmation =
- wg.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated &&
- wg.wgInfo.bankInfo.externalConfirmation;
- if (lastOrigin != null && !externalConfirmation) {
- await storeKnownBankAccount(tx, currency, lastOrigin);
- }
- if (amountChanged) {
- const planchetKeys = await tx.planchets.indexes.byGroup.getAllKeys(
- wg.withdrawalGroupId,
- );
- for (const pk of planchetKeys) {
- await tx.planchets.delete(pk);
- }
- const candidates = await getWithdrawableDenomsTx(
- wex,
- tx,
- exchangeBaseUrl,
- currency,
- );
- const denomsSel = selectWithdrawalDenominations(
- Amounts.parseOrThrow(result.response.balance),
- candidates,
- );
- wg.denomsSel = denomsSel;
- wg.rawWithdrawalAmount = denomsSel.totalWithdrawCost;
- wg.effectiveWithdrawalAmount = denomsSel.totalCoinValue;
+ return await ctx.wex.runLegacyWalletDbTx(async (tx) => {
+ const [wg, h] = await ctx.getRecordHandle(tx);
+ if (!wg) {
+ logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
+ return TaskRunResult.finished();
+ }
+ if (wg.status !== WithdrawalGroupStatus.PendingQueryingStatus) {
+ return TaskRunResult.backoff();
+ }
+ const lastOrigin = result.response.last_origin;
+ // If the withdrawal had external confirmation, we don't store the
+ // bank account details learned via the reserve here.
+ const externalConfirmation =
+ wg.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated &&
+ wg.wgInfo.bankInfo.externalConfirmation;
+ if (lastOrigin != null && !externalConfirmation) {
+ await storeKnownBankAccount(tx, currency, lastOrigin);
+ }
+ if (amountChanged) {
+ const planchetKeys = await tx.planchets.indexes.byGroup.getAllKeys(
+ wg.withdrawalGroupId,
+ );
+ for (const pk of planchetKeys) {
+ await tx.planchets.delete(pk);
}
- wg.status = WithdrawalGroupStatus.PendingReady;
- wg.reserveBalanceAmount = Amounts.stringify(result.response.balance);
- return TransitionResult.transition(wg);
- },
- );
-
- if (transitionResult) {
+ const candidates = await getWithdrawableDenomsTx(
+ wex,
+ tx,
+ exchangeBaseUrl,
+ currency,
+ );
+ const denomsSel = selectWithdrawalDenominations(
+ Amounts.parseOrThrow(result.response.balance),
+ candidates,
+ );
+ wg.denomsSel = denomsSel;
+ wg.rawWithdrawalAmount = denomsSel.totalWithdrawCost;
+ wg.effectiveWithdrawalAmount = denomsSel.totalCoinValue;
+ }
+ wg.status = WithdrawalGroupStatus.PendingReady;
+ wg.reserveBalanceAmount = Amounts.stringify(result.response.balance);
+ await h.update(wg);
return TaskRunResult.progress();
- } else {
- return TaskRunResult.backoff();
- }
+ });
}
/**
@@ -2407,14 +2289,16 @@ async function processWithdrawalGroupAbortingBank(
});
logger.info(`abort response status: ${abortResp.status}`);
- await ctx.transition({}, async (wg) => {
+ await ctx.wex.runLegacyWalletDbTx(async (tx) => {
+ const [wg, h] = await ctx.getRecordHandle(tx);
if (!wg) {
- return TransitionResult.stay();
+ return;
}
wg.status = WithdrawalGroupStatus.AbortedBank;
wg.timestampFinish = timestampPreciseToDb(TalerPreciseTimestamp.now());
- return TransitionResult.transition(wg);
+ await h.update(wg);
});
+
return TaskRunResult.finished();
}
@@ -2475,9 +2359,10 @@ async function processWithdrawalGroupPendingKyc(
checkProtocolInvariant(algoRes.requiresAuth != true);
- await ctx.transition({}, async (rec) => {
+ await ctx.wex.runLegacyWalletDbTx(async (tx) => {
+ const [rec, h] = await ctx.getRecordHandle(tx);
if (!rec) {
- return TransitionResult.stay();
+ return;
}
rec.kycLastAmlReview = updatedStatus.lastAmlReview;
rec.kycLastCheckStatus = updatedStatus.lastCheckStatus;
@@ -2485,7 +2370,7 @@ async function processWithdrawalGroupPendingKyc(
rec.kycLastDeny = updatedStatus.lastDeny;
rec.kycLastRuleGen = updatedStatus.lastRuleGen;
rec.kycAccessToken = updatedStatus.accessToken;
- return TransitionResult.transition(rec);
+ await h.update(rec);
});
return algoRes.taskResult;
@@ -2674,13 +2559,14 @@ async function processWithdrawalGroupPendingReady(
if (withdrawalGroup.denomsSel.selectedDenoms.length === 0) {
logger.warn("Finishing empty withdrawal group (no denoms)");
- await ctx.transition({}, async (wg) => {
- if (!wg) {
- return TransitionResult.stay();
+ await ctx.wex.runLegacyWalletDbTx(async (tx) => {
+ const [rec, h] = await ctx.getRecordHandle(tx);
+ if (!rec) {
+ return;
}
- wg.status = WithdrawalGroupStatus.Done;
- wg.timestampFinish = timestampPreciseToDb(TalerPreciseTimestamp.now());
- return TransitionResult.transition(wg);
+ rec.status = WithdrawalGroupStatus.Done;
+ rec.timestampFinish = timestampPreciseToDb(TalerPreciseTimestamp.now());
+ await h.update(rec);
});
return TaskRunResult.finished();
}
@@ -2708,12 +2594,13 @@ async function processWithdrawalGroupPendingReady(
amount: kycCheckRes.nextThreshold,
exchangeBaseUrl,
});
- await ctx.transition({}, async (wg) => {
+ await ctx.wex.runLegacyWalletDbTx(async (tx) => {
+ const [wg, h] = await ctx.getRecordHandle(tx);
if (!wg) {
- return TransitionResult.stay();
+ return;
}
wg.status = WithdrawalGroupStatus.PendingBalanceKycInit;
- return TransitionResult.transition(wg);
+ await h.update(wg);
});
return TaskRunResult.progress();
}
@@ -2839,46 +2726,43 @@ async function processWithdrawalGroupPendingReady(
let numActive = 0;
const maxReportedErrors = 5;
- const res = await ctx.transition(
- {
- extraStores: ["coins", "coinAvailability", "planchets"],
- },
- async (wg, tx) => {
- if (!wg) {
- return TransitionResult.stay();
- }
+ const res = await ctx.wex.runLegacyWalletDbTx(async (tx) => {
+ const [wg, h] = await ctx.getRecordHandle(tx);
+ if (!wg) {
+ return;
+ }
- const groupPlanchets =
- await tx.planchets.indexes.byGroup.getAll(withdrawalGroupId);
- for (const x of groupPlanchets) {
- switch (x.planchetStatus) {
- case PlanchetStatus.KycRequired:
- case PlanchetStatus.Pending:
- numActive++;
- break;
- case PlanchetStatus.WithdrawalDone:
- break;
- }
- if (x.lastError) {
- numPlanchetErrors++;
- if (numPlanchetErrors < maxReportedErrors) {
- errorsPerCoin[x.coinIdx] = x.lastError;
- }
+ const groupPlanchets =
+ await tx.planchets.indexes.byGroup.getAll(withdrawalGroupId);
+ for (const x of groupPlanchets) {
+ switch (x.planchetStatus) {
+ case PlanchetStatus.KycRequired:
+ case PlanchetStatus.Pending:
+ numActive++;
+ break;
+ case PlanchetStatus.WithdrawalDone:
+ break;
+ }
+ if (x.lastError) {
+ numPlanchetErrors++;
+ if (numPlanchetErrors < maxReportedErrors) {
+ errorsPerCoin[x.coinIdx] = x.lastError;
}
}
+ }
- if (
- (wg.timestampFinish === undefined ||
- wg.status !== WithdrawalGroupStatus.Done) &&
- numActive === 0
- ) {
- wg.timestampFinish = timestampPreciseToDb(TalerPreciseTimestamp.now());
- wg.status = WithdrawalGroupStatus.Done;
- await makeCoinsVisible(wex, tx, ctx.transactionId);
- }
- return TransitionResult.transition(wg);
- },
- );
+ if (
+ (wg.timestampFinish === undefined ||
+ wg.status !== WithdrawalGroupStatus.Done) &&
+ numActive === 0
+ ) {
+ wg.timestampFinish = timestampPreciseToDb(TalerPreciseTimestamp.now());
+ wg.status = WithdrawalGroupStatus.Done;
+ await makeCoinsVisible(wex, tx, ctx.transactionId);
+ }
+ await h.update(wg);
+ return wg;
+ });
if (!res) {
throw Error("withdrawal group does not exist anymore");
@@ -2909,20 +2793,18 @@ async function startRedenomination(
await startUpdateExchangeEntry(ctx.wex, exchangeBaseUrl, {
forceUnavailable: true,
});
- const didTransition = await ctx.transition({}, async (rec) => {
+ return await ctx.wex.runLegacyWalletDbTx(async (tx) => {
+ const [rec, h] = await ctx.getRecordHandle(tx);
switch (rec?.status) {
case WithdrawalGroupStatus.PendingReady:
break;
default:
- return TransitionResult.stay();
+ return TaskRunResult.backoff();
}
rec.status = WithdrawalGroupStatus.PendingRedenominate;
- return TransitionResult.transition(rec);
- });
- if (didTransition) {
+ await h.update(rec);
return TaskRunResult.progress();
- }
- return TaskRunResult.backoff();
+ });
}
export async function processWithdrawalGroup(
@@ -3377,16 +3259,17 @@ async function registerReserveWithBank(
codecForBankWithdrawalOperationPostResponse(),
);
- await ctx.transition({}, async (r) => {
+ return await ctx.wex.runLegacyWalletDbTx(async (tx) => {
+ const [r, h] = await ctx.getRecordHandle(tx);
if (!r) {
- return TransitionResult.stay();
+ return TaskRunResult.finished();
}
switch (r.status) {
case WithdrawalGroupStatus.PendingRegisteringBank:
case WithdrawalGroupStatus.PendingWaitConfirmBank:
break;
default:
- return TransitionResult.stay();
+ return TaskRunResult.backoff();
}
if (r.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
throw Error("invariant failed");
@@ -3396,26 +3279,26 @@ async function registerReserveWithBank(
);
r.status = WithdrawalGroupStatus.PendingWaitConfirmBank;
r.wgInfo.bankInfo.confirmUrl = status.confirm_transfer_url;
- return TransitionResult.transition(r);
+ await h.update(r);
+ return TaskRunResult.progress();
});
-
- return TaskRunResult.progress();
}
async function transitionBankAborted(
ctx: WithdrawTransactionContext,
): Promise<TaskRunResult> {
logger.info("bank aborted the withdrawal");
- await ctx.transition({}, async (r) => {
+ return await ctx.wex.runLegacyWalletDbTx(async (tx) => {
+ const [r, h] = await ctx.getRecordHandle(tx);
if (!r) {
- return TransitionResult.stay();
+ return TaskRunResult.finished();
}
switch (r.status) {
case WithdrawalGroupStatus.PendingRegisteringBank:
case WithdrawalGroupStatus.PendingWaitConfirmBank:
break;
default:
- return TransitionResult.stay();
+ return TaskRunResult.backoff();
}
if (r.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
throw Error("invariant failed");
@@ -3423,9 +3306,9 @@ async function transitionBankAborted(
const now = AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now());
r.wgInfo.bankInfo.timestampBankConfirmed = timestampPreciseToDb(now);
r.status = WithdrawalGroupStatus.FailedBankAborted;
- return TransitionResult.transition(r);
+ await h.update(r);
+ return TaskRunResult.progress();
});
- return TaskRunResult.progress();
}
async function processBankRegisterReserve(
@@ -3583,16 +3466,17 @@ async function processReserveBankStatus(
);
}
- const transitionInfo = await ctx.transition({}, async (r) => {
+ return await ctx.wex.runLegacyWalletDbTx(async (tx) => {
+ const [r, h] = await ctx.getRecordHandle(tx);
if (!r) {
- return TransitionResult.stay();
+ return TaskRunResult.finished();
}
// Re-check reserve status within transaction
switch (r.status) {
case WithdrawalGroupStatus.PendingWaitConfirmBank:
break;
default:
- return TransitionResult.stay();
+ return TaskRunResult.backoff();
}
if (r.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
throw Error("invariant failed");
@@ -3608,17 +3492,12 @@ async function processReserveBankStatus(
r.effectiveWithdrawalAmount = denomSel.totalCoinValue;
r.instructedAmount = denomSel.totalWithdrawCost;
}
- return TransitionResult.transition(r);
+ await h.update(r);
+ return TaskRunResult.progress();
} else {
- return TransitionResult.stay();
+ return TaskRunResult.backoff();
}
});
-
- if (transitionInfo) {
- return TaskRunResult.progress();
- } else {
- return TaskRunResult.backoff();
- }
}
export interface PrepareCreateWithdrawalGroupResult {
@@ -4161,76 +4040,72 @@ export async function confirmWithdrawal(
}
}
- await ctx.transition(
- {
- extraStores: ["exchanges"],
- },
- async (rec, tx) => {
- if (!rec) {
- return TransitionResult.stay();
+ await ctx.wex.runLegacyWalletDbTx(async (tx) => {
+ const [rec, h] = await ctx.getRecordHandle(tx);
+ if (!rec) {
+ return;
+ }
+ switch (rec.status) {
+ case WithdrawalGroupStatus.Done:
+ case WithdrawalGroupStatus.PendingReady:
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ case WithdrawalGroupStatus.PendingWaitConfirmBank: {
+ // Be idempotent.
+ return;
}
- switch (rec.status) {
- case WithdrawalGroupStatus.Done:
- case WithdrawalGroupStatus.PendingReady:
- case WithdrawalGroupStatus.PendingRegisteringBank:
- case WithdrawalGroupStatus.PendingWaitConfirmBank: {
- // Be idempotent.
- return TransitionResult.stay();
- }
- case WithdrawalGroupStatus.AbortedOtherWallet: {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
- {},
- );
- }
- case WithdrawalGroupStatus.DialogProposed: {
- rec.exchangeBaseUrl = exchange.exchangeBaseUrl;
- rec.instructedAmount = req.amount;
- rec.restrictAge = req.restrictAge;
- if (initialDenoms != null) {
- rec.denomsSel = initialDenoms;
- rec.rawWithdrawalAmount = initialDenoms.totalWithdrawCost;
- rec.effectiveWithdrawalAmount = initialDenoms.totalCoinValue;
- } else {
- rec.denomsSel = undefined;
- rec.rawWithdrawalAmount = Amounts.stringify(
- Amounts.zeroOfCurrency(instructedCurrency),
- );
- rec.effectiveWithdrawalAmount = Amounts.stringify(
- Amounts.zeroOfCurrency(instructedCurrency),
- );
- }
- checkDbInvariant(
- rec.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated,
- "withdrawal type mismatch",
+ case WithdrawalGroupStatus.AbortedOtherWallet: {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
+ {},
+ );
+ }
+ case WithdrawalGroupStatus.DialogProposed: {
+ rec.exchangeBaseUrl = exchange.exchangeBaseUrl;
+ rec.instructedAmount = req.amount;
+ rec.restrictAge = req.restrictAge;
+ if (initialDenoms != null) {
+ rec.denomsSel = initialDenoms;
+ rec.rawWithdrawalAmount = initialDenoms.totalWithdrawCost;
+ rec.effectiveWithdrawalAmount = initialDenoms.totalCoinValue;
+ } else {
+ rec.denomsSel = undefined;
+ rec.rawWithdrawalAmount = Amounts.stringify(
+ Amounts.zeroOfCurrency(instructedCurrency),
);
- rec.wgInfo.exchangeCreditAccounts = withdrawalAccountList;
- rec.wgInfo.bankInfo.exchangePaytoUri = exchangePaytoUri;
- rec.status = WithdrawalGroupStatus.PendingRegisteringBank;
-
- await internalPerformExchangeWasUsed(
- wex,
- tx,
- exchange.exchangeBaseUrl,
- withdrawalGroup,
+ rec.effectiveWithdrawalAmount = Amounts.stringify(
+ Amounts.zeroOfCurrency(instructedCurrency),
);
-
- return TransitionResult.transition(rec);
}
+ checkDbInvariant(
+ rec.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated,
+ "withdrawal type mismatch",
+ );
+ rec.wgInfo.exchangeCreditAccounts = withdrawalAccountList;
+ rec.wgInfo.bankInfo.exchangePaytoUri = exchangePaytoUri;
+ rec.status = WithdrawalGroupStatus.PendingRegisteringBank;
- default: {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_REQUEST_TRANSACTION_STATE_UNSUPPORTED,
- {
- message: `unable to confirm withdrawal in current state`,
- txState: computeWithdrawalTransactionStatus(rec),
- debugStateNum: rec.status,
- },
- );
- }
+ await internalPerformExchangeWasUsed(
+ wex,
+ tx,
+ exchange.exchangeBaseUrl,
+ withdrawalGroup,
+ );
+ await h.update(rec);
+ return;
}
- },
- );
+
+ default: {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_REQUEST_TRANSACTION_STATE_UNSUPPORTED,
+ {
+ message: `unable to confirm withdrawal in current state`,
+ txState: computeWithdrawalTransactionStatus(rec),
+ debugStateNum: rec.status,
+ },
+ );
+ }
+ }
+ });
await wex.taskScheduler.resetTaskRetries(ctx.taskId);