taler-typescript-core

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

commit e540862ac5ecf5b55ae9129f1b61e7ee01894f2f
parent 886d86262fb909f0939dfd95118846760da5d57d
Author: Florian Dold <florian@dold.me>
Date:   Thu, 19 Mar 2026 23:41:17 +0100

wallet-core: automatically expire pay-merchant transactions

Diffstat:
Mpackages/taler-wallet-core/src/db.ts | 2++
Mpackages/taler-wallet-core/src/pay-merchant.ts | 52+++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 53 insertions(+), 1 deletion(-)

diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts @@ -1464,6 +1464,8 @@ export enum PurchaseStatus { */ DoneRepurchaseDetected = 0x0500_0001, + Expired = 0x0502_0000, + /** * The user has rejected the proposal. */ diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -2028,6 +2028,7 @@ async function checkPaymentByProposalId( case PurchaseStatus.SuspendedPendingAcceptRefund: case PurchaseStatus.SuspendedQueryingAutoRefund: case PurchaseStatus.SuspendedQueryingRefund: + case PurchaseStatus.Expired: return handleConfirmed(); case PurchaseStatus.FailedPaidByOther: return handlePaidByOther(); @@ -2996,10 +2997,11 @@ export async function processPurchase( return processPurchaseAcceptRefund(wex, purchase); case PurchaseStatus.DialogShared: return processPurchaseDialogShared(wex, purchase); + case PurchaseStatus.DialogProposed: + return processPurchaseDialogProposed(wex, purchase); case PurchaseStatus.FailedClaim: case PurchaseStatus.Done: case PurchaseStatus.DoneRepurchaseDetected: - case PurchaseStatus.DialogProposed: case PurchaseStatus.AbortedProposalRefused: case PurchaseStatus.AbortedIncompletePayment: case PurchaseStatus.AbortedOrderDeleted: @@ -3014,6 +3016,7 @@ export async function processPurchase( case PurchaseStatus.SuspendedFinalizingQueryingAutoRefund: case PurchaseStatus.FailedAbort: case PurchaseStatus.FailedPaidByOther: + case PurchaseStatus.Expired: return TaskRunResult.finished(); default: assertUnreachable(purchase.purchaseStatus); @@ -3782,6 +3785,10 @@ export function computePayMerchantTransactionState( return { major: TransactionMajorState.Aborted, }; + case PurchaseStatus.Expired: + return { + major: TransactionMajorState.Expired, + }; case PurchaseStatus.FailedClaim: return { major: TransactionMajorState.Failed, @@ -3896,6 +3903,8 @@ export function computePayMerchantTransactionActions( return [TransactionAction.Delete]; case PurchaseStatus.Done: return [TransactionAction.Delete]; + case PurchaseStatus.Expired: + return [TransactionAction.Delete]; case PurchaseStatus.DoneRepurchaseDetected: return [TransactionAction.Delete]; case PurchaseStatus.AbortedIncompletePayment: @@ -4015,6 +4024,47 @@ async function checkIfOrderIsAlreadyPaid( throw Error(`this order cant be paid: ${resp.status}`); } +async function processPurchaseDialogProposed( + wex: WalletExecutionContext, + purchase: PurchaseRecord, +): Promise<TaskRunResult> { + const proposalId = purchase.proposalId; + const ctx = new PayMerchantTransactionContext(wex, proposalId); + const txRes = await wex.runLegacyWalletDbTx(async (tx) => { + const rec = await tx.purchases.get(proposalId); + if (!rec) { + return undefined; + } + const download = await expectProposalDownloadByIdInTx(wex, tx, proposalId); + return { download, rec }; + }); + if (!txRes) { + // Transaction longer exists. + return TaskRunResult.finished(); + } + const payDeadline = AbsoluteTime.fromProtocolTimestamp( + txRes.download.contractTerms.pay_deadline, + ); + + const expiry = AbsoluteTime.addDuration( + payDeadline, + Duration.fromSpec({ seconds: 10 }), + ); + + if (AbsoluteTime.isExpired(expiry)) { + await wex.runLegacyWalletDbTx(async (tx) => { + const [r2, h] = await ctx.getRecordHandle(tx); + if (r2?.purchaseStatus !== PurchaseStatus.DialogProposed) { + return; + } + r2.purchaseStatus = PurchaseStatus.Expired; + await h.update(r2); + }); + return TaskRunResult.progress(); + } + return TaskRunResult.runAgainAt(expiry); +} + /** * While the transaction is in the dialog(shared) state, * we long-poll the merchant. We do this to find out if