taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/taler-wallet-core/src/deposits.ts | 148++++++++++++++++++++++---------------------------------------------------------
Mpackages/taler-wallet-core/src/pay-merchant.ts | 106++++++++++++-------------------------------------------------------------------
Mpackages/taler-wallet-core/src/withdraw.ts | 89++++++++++++++++++++-----------------------------------------------------------
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; }