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:
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