commit 31bcff25a7359f90db1ee469640c2c7665c79667
parent fc2b4c6b7baa2810571f6e308e5a97e65ab57a79
Author: Florian Dold <florian@dold.me>
Date: Wed, 18 Mar 2026 18:24:01 +0100
wallet-core: refactor transitions to use helper
Diffstat:
3 files changed, 78 insertions(+), 265 deletions(-)
diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts
@@ -92,6 +92,7 @@ import {
} from "./coinSelection.js";
import {
PendingTaskType,
+ RecordHandle,
TaskIdStr,
TaskRunResult,
TransactionContext,
@@ -99,6 +100,7 @@ import {
cancelableLongPoll,
constructTaskIdentifier,
genericWaitForState,
+ getGenericRecordHandle,
runWithClientCancellation,
spendCoins,
} from "./common.js";
@@ -331,6 +333,25 @@ export class DepositTransactionContext implements TransactionContext {
});
}
+ async getRecordHandle(
+ tx: WalletDbReadWriteTransaction<["depositGroups", "transactionsMeta"]>,
+ ): Promise<
+ [DepositGroupRecord | undefined, RecordHandle<DepositGroupRecord>]
+ > {
+ return getGenericRecordHandle<DepositGroupRecord>(
+ this,
+ tx as any,
+ async () => tx.depositGroups.get(this.depositGroupId),
+ async (r) => {
+ await tx.depositGroups.put(r);
+ },
+ async () => tx.depositGroups.delete(this.depositGroupId),
+ (r) => computeDepositTransactionStatus(r),
+ (r) => r.operationStatus,
+ () => this.updateTransactionMeta(tx),
+ );
+ }
+
async deleteTransaction(): Promise<void> {
await this.wex.runLegacyWalletDbTx(async (tx) => {
await this.deleteTransactionInTx(tx);
@@ -342,36 +363,23 @@ export class DepositTransactionContext implements TransactionContext {
["depositGroups", "tombstones", "transactionsMeta"]
>,
): Promise<void> {
- const rec = await tx.depositGroups.get(this.depositGroupId);
+ const [rec, h] = await this.getRecordHandle(tx);
if (!rec) {
return;
}
- const oldTxState = computeDepositTransactionStatus(rec);
- await tx.depositGroups.delete(rec.depositGroupId);
- await this.updateTransactionMeta(tx);
- tx.notify({
- type: NotificationType.TransactionStateTransition,
- transactionId: this.transactionId,
- oldTxState,
- newTxState: {
- major: TransactionMajorState.Deleted,
- },
- newStId: -1,
- });
+ await h.update(undefined);
}
async suspendTransaction(): Promise<void> {
const { wex, depositGroupId, transactionId, taskId: retryTag } = this;
await wex.runLegacyWalletDbTx(async (tx) => {
- const dg = await tx.depositGroups.get(depositGroupId);
+ const [dg, h] = await this.getRecordHandle(tx);
if (!dg) {
logger.warn(
`can't suspend deposit group, depositGroupId=${depositGroupId} not found`,
);
return undefined;
}
- const oldState = computeDepositTransactionStatus(dg);
- const oldStId = dg.operationStatus;
let newOpStatus: DepositOperationStatus | undefined;
switch (dg.operationStatus) {
case DepositOperationStatus.AbortedDeposit:
@@ -414,15 +422,7 @@ export class DepositTransactionContext implements TransactionContext {
return undefined;
}
dg.operationStatus = newOpStatus;
- await tx.depositGroups.put(dg);
- await this.updateTransactionMeta(tx);
- applyNotifyTransition(tx.notify, transactionId, {
- oldTxState: oldState,
- newTxState: computeDepositTransactionStatus(dg),
- balanceEffect: BalanceEffect.None,
- newStId: dg.operationStatus,
- oldStId,
- });
+ await h.update(dg);
});
wex.taskScheduler.stopShepherdTask(retryTag);
}
@@ -1017,24 +1017,12 @@ async function waitForRefreshOnDepositGroup(
default:
return false;
}
- const newDg = await tx.depositGroups.get(depositGroup.depositGroupId);
+ const [newDg, h] = await ctx.getRecordHandle(tx);
if (!newDg) {
return false;
}
- const oldTxState = computeDepositTransactionStatus(newDg);
- const oldStId = newDg.operationStatus;
newDg.operationStatus = newOpState;
- const newTxState = computeDepositTransactionStatus(newDg);
- const newStId = newDg.operationStatus;
- await tx.depositGroups.put(newDg);
- await ctx.updateTransactionMeta(tx);
- applyNotifyTransition(tx.notify, ctx.transactionId, {
- oldTxState,
- newTxState,
- balanceEffect: BalanceEffect.Any,
- newStId,
- oldStId,
- });
+ await h.update(newDg);
return true;
});
if (didTransition) {
@@ -1141,12 +1129,10 @@ async function processDepositGroupPendingKyc(
// Now store the result.
return await wex.runLegacyWalletDbTx(async (tx) => {
- const newDg = await tx.depositGroups.get(depositGroupId);
+ const [newDg, h] = await ctx.getRecordHandle(tx);
if (!newDg) {
return TaskRunResult.finished();
}
- const oldTxState = computeDepositTransactionStatus(newDg);
- const oldStId = newDg.operationStatus;
switch (newDg.operationStatus) {
case DepositOperationStatus.PendingAggregateKyc:
if (requiresAuth) {
@@ -1167,17 +1153,7 @@ async function processDepositGroupPendingKyc(
return TaskRunResult.backoff();
}
newDg.kycInfo = kycInfo;
- await tx.depositGroups.put(newDg);
- await ctx.updateTransactionMeta(tx);
- const newTxState = computeDepositTransactionStatus(newDg);
- const newStId = newDg.operationStatus;
- applyNotifyTransition(tx.notify, ctx.transactionId, {
- oldTxState,
- newTxState,
- balanceEffect: BalanceEffect.Any,
- oldStId,
- newStId,
- });
+ await h.update(newDg);
return algoRes.taskResult;
});
}
@@ -1274,12 +1250,10 @@ async function transitionToKycRequired(
const ctx = new DepositTransactionContext(wex, depositGroupId);
await wex.runLegacyWalletDbTx(async (tx) => {
- const dg = await tx.depositGroups.get(depositGroupId);
+ const [dg, h] = await ctx.getRecordHandle(tx);
if (!dg) {
return undefined;
}
- const oldTxState = computeDepositTransactionStatus(dg);
- const oldStId = dg.operationStatus;
switch (dg.operationStatus) {
case DepositOperationStatus.LegacyPendingTrack:
case DepositOperationStatus.FinalizingTrack:
@@ -1326,17 +1300,7 @@ async function transitionToKycRequired(
lastBadKycAuth: args.badKycAuth,
};
}
- await tx.depositGroups.put(dg);
- await ctx.updateTransactionMeta(tx);
- const newTxState = computeDepositTransactionStatus(dg);
- const newStId = dg.operationStatus;
- applyNotifyTransition(tx.notify, ctx.transactionId, {
- oldTxState,
- newTxState,
- balanceEffect: BalanceEffect.Any,
- newStId,
- oldStId,
- });
+ await h.update(dg);
});
return TaskRunResult.progress();
}
@@ -1468,15 +1432,13 @@ async function processDepositGroupTrack(
let allWired = true;
await wex.runLegacyWalletDbTx(async (tx) => {
- const dg = await tx.depositGroups.get(depositGroupId);
+ const [dg, h] = await ctx.getRecordHandle(tx);
if (!dg) {
return undefined;
}
if (!dg.statusPerCoin) {
return undefined;
}
- const oldTxState = computeDepositTransactionStatus(dg);
- const oldStId = dg.operationStatus;
for (let i = 0; i < dg.statusPerCoin.length; i++) {
if (dg.statusPerCoin[i] !== DepositElementStatus.Wired) {
allWired = false;
@@ -1489,15 +1451,7 @@ async function processDepositGroupTrack(
await tx.depositGroups.put(dg);
await ctx.updateTransactionMeta(tx);
}
- const newTxState = computeDepositTransactionStatus(dg);
- const newStId = dg.operationStatus;
- applyNotifyTransition(tx.notify, ctx.transactionId, {
- oldTxState,
- newTxState,
- balanceEffect: BalanceEffect.Any,
- oldStId,
- newStId,
- });
+ await h.update(dg);
});
if (allWired) {
return TaskRunResult.finished();
@@ -1815,24 +1769,12 @@ async function processDepositGroupPendingDeposit(
}
await wex.runLegacyWalletDbTx(async (tx) => {
- const dg = await tx.depositGroups.get(depositGroupId);
+ const [dg, h] = await ctx.getRecordHandle(tx);
if (!dg) {
return undefined;
}
- const oldTxState = computeDepositTransactionStatus(dg);
- const oldStId = dg.operationStatus;
dg.operationStatus = DepositOperationStatus.FinalizingTrack;
- await tx.depositGroups.put(dg);
- await ctx.updateTransactionMeta(tx);
- const newTxState = computeDepositTransactionStatus(dg);
- const newStId = dg.operationStatus;
- applyNotifyTransition(tx.notify, ctx.transactionId, {
- oldTxState,
- newTxState,
- balanceEffect: BalanceEffect.None,
- oldStId,
- newStId,
- });
+ await h.update(dg);
});
return TaskRunResult.progress();
}
@@ -2290,6 +2232,10 @@ export async function createDepositGroup(
const transactionId = ctx.transactionId;
const newTxState = await wex.runLegacyWalletDbTx(async (tx) => {
+ const [oldDg, h] = await ctx.getRecordHandle(tx);
+ if (oldDg != null) {
+ throw Error("deposit group already exists");
+ }
if (depositGroup.payCoinSelection) {
await spendCoins(wex, tx, {
transactionId,
@@ -2300,24 +2246,12 @@ export async function createDepositGroup(
refreshReason: RefreshReason.PayDeposit,
});
}
- await tx.depositGroups.put(depositGroup);
await tx.contractTerms.put({
contractTermsRaw: contractTerms,
h: contractTermsHash,
});
- await ctx.updateTransactionMeta(tx);
- const oldTxState = { major: TransactionMajorState.None };
- const oldStId = 0;
- const newTxState = computeDepositTransactionStatus(depositGroup);
- const newStId = depositGroup.operationStatus;
- applyNotifyTransition(tx.notify, transactionId, {
- oldTxState,
- newTxState,
- balanceEffect: BalanceEffect.Any,
- oldStId,
- newStId,
- });
- return newTxState;
+ await h.update(depositGroup);
+ return computeDepositTransactionStatus(depositGroup);
});
wex.taskScheduler.startShepherdTask(ctx.taskId);
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -427,30 +427,19 @@ export class PayMerchantTransactionContext implements TransactionContext {
}
async suspendTransaction(): Promise<void> {
- const { wex, proposalId, transactionId } = this;
+ const { wex } = this;
wex.taskScheduler.stopShepherdTask(this.taskId);
await wex.runLegacyWalletDbTx(async (tx) => {
- const purchase = await tx.purchases.get(proposalId);
+ const [purchase, h] = await this.getRecordHandle(tx);
if (!purchase) {
throw Error("purchase not found");
}
- const oldTxState = computePayMerchantTransactionState(purchase);
- const oldStId = purchase.purchaseStatus;
let newStatus = transitionSuspend[purchase.purchaseStatus];
- if (!newStatus) {
+ if (!newStatus?.next) {
return;
}
- await tx.purchases.put(purchase);
- await this.updateTransactionMeta(tx);
- const newTxState = computePayMerchantTransactionState(purchase);
- const newStId = purchase.purchaseStatus;
- applyNotifyTransition(tx.notify, transactionId, {
- oldTxState,
- newTxState,
- balanceEffect: BalanceEffect.None,
- oldStId,
- newStId,
- });
+ purchase.purchaseStatus = newStatus.next;
+ await h.update(purchase);
});
}
@@ -1135,15 +1124,13 @@ async function processDownloadProposal(
logger.trace(`extracted contract data: ${j2s(contractData)}`);
await wex.runLegacyWalletDbTx(async (tx) => {
- const p = await tx.purchases.get(proposalId);
+ const [p, h] = await ctx.getRecordHandle(tx);
if (!p) {
return;
}
if (p.purchaseStatus !== PurchaseStatus.PendingDownloadingProposal) {
return;
}
- const oldTxState = computePayMerchantTransactionState(p);
- const oldStId = p.purchaseStatus;
const secretSeed = encodeCrock(getRandomBytes(32));
p.secretSeed = secretSeed;
@@ -1212,23 +1199,12 @@ async function processDownloadProposal(
logger.warn("repurchase detected");
p.purchaseStatus = PurchaseStatus.DoneRepurchaseDetected;
p.repurchaseProposalId = repurchase.proposalId;
- await tx.purchases.put(p);
} else {
p.purchaseStatus = p.shared
? PurchaseStatus.DialogShared
: PurchaseStatus.DialogProposed;
- await tx.purchases.put(p);
}
- await ctx.updateTransactionMeta(tx);
- const newTxState = computePayMerchantTransactionState(p);
- const newStId = p.purchaseStatus;
- applyNotifyTransition(tx.notify, transactionId, {
- oldTxState,
- newTxState,
- balanceEffect: BalanceEffect.None,
- oldStId,
- newStId,
- });
+ await h.update(p);
});
return TaskRunResult.progress();
@@ -2804,7 +2780,7 @@ export async function confirmPay(
);
await wex.runLegacyWalletDbTx(async (tx) => {
- const p = await tx.purchases.get(proposal.proposalId);
+ const [p, h] = await ctx.getRecordHandle(tx);
if (!p) {
return;
}
@@ -2921,8 +2897,6 @@ export async function confirmPay(
}
}
- const oldTxState = computePayMerchantTransactionState(p);
- const oldStId = p.purchaseStatus;
switch (p.purchaseStatus) {
case PurchaseStatus.DialogShared:
case PurchaseStatus.DialogProposed:
@@ -2941,8 +2915,7 @@ export async function confirmPay(
p.lastSessionId = sessionId;
p.timestampAccept = timestampPreciseToDb(TalerPreciseTimestamp.now());
p.purchaseStatus = PurchaseStatus.PendingPaying;
- await tx.purchases.put(p);
- await ctx.updateTransactionMeta(tx);
+ await h.update(p);
if (p.payInfo.payTokenSelection) {
await spendTokens(tx, {
tokenPubs: p.payInfo.payTokenSelection.tokenPubs,
@@ -2966,14 +2939,6 @@ export async function confirmPay(
default:
break;
}
- const newTxState = computePayMerchantTransactionState(p);
- applyNotifyTransition(tx.notify, transactionId, {
- oldTxState,
- newTxState,
- balanceEffect: BalanceEffect.Any,
- oldStId,
- newStId: p.purchaseStatus,
- });
});
// TODO: pre-generate slates based on choice priority!
@@ -4009,7 +3974,7 @@ export async function sharePayment(
const ctx = new PayMerchantTransactionContext(wex, proposalId);
const result = await wex.runLegacyWalletDbTx(async (tx) => {
- const p = await tx.purchases.get(proposalId);
+ const [p, h] = await ctx.getRecordHandle(tx);
if (!p) {
logger.warn("purchase does not exist anymore");
return undefined;
@@ -4021,26 +3986,11 @@ export async function sharePayment(
// FIXME: purchase can be shared before being paid
return undefined;
}
- const oldTxState = computePayMerchantTransactionState(p);
- const oldStId = p.purchaseStatus;
if (p.purchaseStatus === PurchaseStatus.DialogProposed) {
p.purchaseStatus = PurchaseStatus.DialogShared;
p.shared = true;
- await tx.purchases.put(p);
+ await h.update(p);
}
-
- await ctx.updateTransactionMeta(tx);
-
- const newTxState = computePayMerchantTransactionState(p);
-
- applyNotifyTransition(tx.notify, ctx.transactionId, {
- oldTxState,
- newTxState,
- balanceEffect: BalanceEffect.Any,
- oldStId,
- newStId: p.purchaseStatus,
- });
-
return {
proposalId: p.proposalId,
nonce: p.noncePriv,
@@ -4541,7 +4491,7 @@ export async function startQueryRefund(
): Promise<void> {
const ctx = new PayMerchantTransactionContext(wex, proposalId);
await wex.runLegacyWalletDbTx(async (tx) => {
- const p = await tx.purchases.get(proposalId);
+ const [p, h] = await ctx.getRecordHandle(tx);
if (!p) {
logger.warn(`purchase ${proposalId} does not exist anymore`);
return;
@@ -4549,20 +4499,8 @@ export async function startQueryRefund(
if (p.purchaseStatus !== PurchaseStatus.Done) {
return;
}
- const oldTxState = computePayMerchantTransactionState(p);
- const oldStId = p.purchaseStatus;
p.purchaseStatus = PurchaseStatus.PendingQueryingRefund;
- const newTxState = computePayMerchantTransactionState(p);
- const newStId = p.purchaseStatus;
- await tx.purchases.put(p);
- await ctx.updateTransactionMeta(tx);
- applyNotifyTransition(tx.notify, ctx.transactionId, {
- oldTxState,
- newTxState,
- balanceEffect: BalanceEffect.Any,
- newStId,
- oldStId,
- });
+ await h.update(p);
});
wex.taskScheduler.startShepherdTask(ctx.taskId);
}
@@ -4644,7 +4582,7 @@ async function storeRefunds(
const currency = Amounts.currencyOf(amountRaw);
const result = await wex.db.runAllStoresReadWriteTx({}, async (tx) => {
- const myPurchase = await tx.purchases.get(purchase.proposalId);
+ const [myPurchase, h] = await ctx.getRecordHandle(tx);
if (!myPurchase) {
logger.warn("purchase group not found anymore");
return;
@@ -4827,10 +4765,6 @@ async function storeRefunds(
);
}
}
-
- const oldTxState = computePayMerchantTransactionState(myPurchase);
- const oldStId = myPurchase.purchaseStatus;
-
const shouldCheckAutoRefund =
myPurchase.autoRefundDeadline &&
!AbsoluteTime.isExpired(
@@ -4849,17 +4783,7 @@ async function storeRefunds(
}
myPurchase.refundAmountAwaiting = undefined;
}
- await tx.purchases.put(myPurchase);
- await ctx.updateTransactionMeta(tx);
- const newTxState = computePayMerchantTransactionState(myPurchase);
-
- applyNotifyTransition(tx.notify, ctx.transactionId, {
- oldTxState,
- newTxState,
- balanceEffect: BalanceEffect.Any,
- newStId: myPurchase.purchaseStatus,
- oldStId,
- });
+ await h.update(myPurchase);
return {
numPendingItemsTotal,
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
@@ -77,7 +77,6 @@ import {
TransactionWithdrawal,
URL,
UnblindedDenominationSignature,
- WalletNotification,
WithdrawUriInfoResponse,
WithdrawalDetailsForAmount,
WithdrawalExchangeAccountDetails,
@@ -142,6 +141,7 @@ import {
PlanchetRecord,
PlanchetStatus,
WalletDbAllStoresReadOnlyTransaction,
+ WalletDbAllStoresReadWriteTransaction,
WalletDbReadOnlyTransaction,
WalletDbReadWriteTransaction,
WgInfo,
@@ -186,9 +186,6 @@ import {
runKycCheckAlgo,
} from "./kyc.js";
import {
- BalanceEffect,
- TransitionInfo,
- applyNotifyTransition,
constructTransactionIdentifier,
isUnsuccessfulTransaction,
parseTransactionIdentifier,
@@ -514,30 +511,17 @@ export class WithdrawTransactionContext implements TransactionContext {
["withdrawalGroups", "planchets", "tombstones", "transactionsMeta"]
>,
): Promise<void> {
- const notifs: WalletNotification[] = [];
- const rec = await tx.withdrawalGroups.get(this.withdrawalGroupId);
+ const [rec, h] = await this.getRecordHandle(tx);
if (!rec) {
return;
}
- const oldTxState = computeWithdrawalTransactionStatus(rec);
- await tx.withdrawalGroups.delete(rec.withdrawalGroupId);
const planchets = await tx.planchets.indexes.byGroup.getAll(
rec.withdrawalGroupId,
);
for (const p of planchets) {
await tx.planchets.delete(p.coinPub);
}
- await this.updateTransactionMeta(tx);
- notifs.push({
- type: NotificationType.TransactionStateTransition,
- transactionId: this.transactionId,
- oldTxState,
- newTxState: {
- major: TransactionMajorState.Deleted,
- },
- newStId: -1,
- });
- return;
+ await h.update(undefined);
}
async suspendTransaction(): Promise<void> {
@@ -3631,15 +3615,15 @@ export interface PerformCreateWithdrawalGroupResult {
export async function internalPerformCreateWithdrawalGroup(
wex: WalletExecutionContext,
- tx: WalletDbReadWriteTransaction<
- ["withdrawalGroups", "reserves", "exchanges"]
- >,
+ tx: WalletDbAllStoresReadWriteTransaction,
prep: PrepareCreateWithdrawalGroupResult,
): Promise<PerformCreateWithdrawalGroupResult> {
const { withdrawalGroup } = prep;
- const existingWg = await tx.withdrawalGroups.get(
+ const ctx = new WithdrawTransactionContext(
+ wex,
withdrawalGroup.withdrawalGroupId,
);
+ const [existingWg, h] = await ctx.getRecordHandle(tx);
if (existingWg) {
return {
withdrawalGroup: existingWg,
@@ -3651,59 +3635,35 @@ export async function internalPerformCreateWithdrawalGroup(
reservePriv: withdrawalGroup.reservePriv,
});
- if (!prep.creationInfo) {
- return {
- withdrawalGroup,
- };
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+
+ if (prep.creationInfo) {
+ await internalPerformExchangeWasUsed(
+ wex,
+ tx,
+ prep.creationInfo.canonExchange,
+ );
}
- return internalPerformExchangeWasUsed(
- wex,
- tx,
- prep.creationInfo.canonExchange,
+ return {
withdrawalGroup,
- );
+ };
}
+/**
+ * Mark an exchange as used for a withdrawal operation.
+ */
async function internalPerformExchangeWasUsed(
wex: WalletExecutionContext,
tx: WalletDbReadWriteTransaction<["exchanges"]>,
canonExchange: string,
- withdrawalGroup: WithdrawalGroupRecord,
-): Promise<PerformCreateWithdrawalGroupResult> {
+): Promise<void> {
const exchange = await tx.exchanges.get(canonExchange);
if (exchange) {
exchange.lastWithdrawal = timestampPreciseToDb(TalerPreciseTimestamp.now());
await tx.exchanges.put(exchange);
}
- const oldTxState = {
- major: TransactionMajorState.None,
- minor: undefined,
- internalId: 0,
- };
- const newTxState = computeWithdrawalTransactionStatus(withdrawalGroup);
- const transitionInfo: TransitionInfo = {
- oldTxState,
- newTxState,
- balanceEffect: BalanceEffect.Any,
- oldStId: 0,
- newStId: withdrawalGroup.status,
- };
-
await markExchangeUsed(tx, canonExchange);
-
- const ctx = new WithdrawTransactionContext(
- wex,
- withdrawalGroup.withdrawalGroupId,
- );
-
- wex.taskScheduler.startShepherdTask(ctx.taskId);
-
- applyNotifyTransition(tx.notify, ctx.transactionId, transitionInfo);
-
- return {
- withdrawalGroup,
- };
}
/**
@@ -4084,12 +4044,7 @@ export async function confirmWithdrawal(
rec.wgInfo.bankInfo.exchangePaytoUri = exchangePaytoUri;
rec.status = WithdrawalGroupStatus.PendingRegisteringBank;
- await internalPerformExchangeWasUsed(
- wex,
- tx,
- exchange.exchangeBaseUrl,
- withdrawalGroup,
- );
+ await internalPerformExchangeWasUsed(wex, tx, exchange.exchangeBaseUrl);
await h.update(rec);
return;
}