commit 2a71a72efcdf609cb73180e6b41e9898ddf2692b
parent b4cb7fc3aefe898757adcee063e34d0b96c87377
Author: Florian Dold <dold@taler.net>
Date: Thu, 3 Sep 2026 15:41:17 +0200
wallet-core: reclaim the coins of a failed push debit
A permanent error left the transaction Failed with its coins dormant;
deleting the purse first is what makes the exchange give them back.
Diffstat:
1 file changed, 26 insertions(+), 1 deletion(-)
diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
@@ -367,6 +367,14 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
await this.wex.taskScheduler.resetTask(this.taskId);
}
+ /**
+ * Give up on the transaction because of a permanent error.
+ *
+ * The coins were spent when the transaction was created, so unless the
+ * purse deletion has already run, the failure goes through it: deleting
+ * the purse (or finding it never existed) is what makes the exchange give
+ * the coins back, and the deletion state refreshes them.
+ */
async failTransaction(
fromSt: PeerPushDebitStatus,
reason?: TalerErrorDetail,
@@ -377,10 +385,23 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
if (rec?.status != fromSt) {
return;
}
- rec.status = PeerPushDebitStatus.Failed;
rec.failReason = reason;
+ switch (fromSt) {
+ case PeerPushDebitStatus.AbortingDeletePurse:
+ case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
+ case PeerPushDebitStatus.ExpiredDeletePurse:
+ case PeerPushDebitStatus.SuspendedExpiredDeletePurse:
+ rec.status = PeerPushDebitStatus.Failed;
+ break;
+ default:
+ rec.status = PeerPushDebitStatus.AbortingDeletePurse;
+ break;
+ }
await h.update(rec, "fail");
});
+ if (fromSt !== PeerPushDebitStatus.AbortingDeletePurse) {
+ await wex.taskScheduler.resetTask(this.taskId);
+ }
}
async userFailTransaction(reason?: TalerErrorDetail): Promise<void> {
@@ -1220,6 +1241,10 @@ async function processPeerPushDebitDeletePurse(
if (fromSt === PeerPushDebitStatus.ExpiredDeletePurse) {
rec.status = PeerPushDebitStatus.Expired;
await h.update(rec, "expire-purse-deleted");
+ } else if (rec.failReason) {
+ // The deletion only reclaimed the coins of a failed transaction.
+ rec.status = PeerPushDebitStatus.Failed;
+ await h.update(rec, "fail-purse-deleted");
} else {
rec.status = PeerPushDebitStatus.Aborted;
await h.update(rec, "abort-purse-deleted");