taler-typescript-core

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

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:
Mpackages/taler-wallet-core/src/pay-peer-push-debit.ts | 27++++++++++++++++++++++++++-
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");