taler-typescript-core

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

commit 444785e37db60943b0634e532aa9e0758167a1e7
parent ed8cd5e545ec8a035c309aad8fedf9343c6ff362
Author: Florian Dold <florian@dold.me>
Date:   Fri, 20 Mar 2026 14:56:07 +0100

wallet-core: fix some transitions to failed

Diffstat:
Mpackages/taler-util/src/types-taler-exchange.ts | 1+
Mpackages/taler-wallet-core/src/db.ts | 9+++++++--
Mpackages/taler-wallet-core/src/pay-merchant.ts | 38++++++++++++++++++++++++++++++++------
Mpackages/taler-wallet-core/src/pay-peer-pull-credit.ts | 27++++++++++++++++++++++-----
Mpackages/taler-wallet-core/src/pay-peer-pull-debit.ts | 33+++++++++++++++++++++++----------
Mpackages/taler-wallet-core/src/pay-peer-push-credit.ts | 28++++++++++++++++++++--------
Mpackages/taler-wallet-core/src/pay-peer-push-debit.ts | 34+++++++++++++++++++++++-----------
7 files changed, 128 insertions(+), 42 deletions(-)

diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts @@ -3254,6 +3254,7 @@ export type PurseConflict = | PurseCreateConflict | PurseDepositConflict | PurseContractConflict; + export type PurseConflictPartial = | PurseCreateConflict | PurseDepositConflict diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts @@ -1443,9 +1443,9 @@ export enum PurchaseStatus { DialogShared = 0x0101_0001, /** - * Downloading or processing the proposal has failed permanently. + * Generic failure, check error code. */ - FailedClaim = 0x0501_0000, + Failed = 0x0501_0000, /** * Tried to abort, but aborting failed or was cancelled. @@ -1455,6 +1455,11 @@ export enum PurchaseStatus { FailedPaidByOther = 0x0501_0002, /** + * Downloading or processing the proposal has failed permanently. + */ + FailedClaim = 0x0501_0003, + + /** * Payment was successful. */ Done = 0x0500_0000, diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -435,6 +435,22 @@ export class PayMerchantTransactionContext implements TransactionContext { }); } + async failTransaction( + fromSt: PurchaseStatus, + reason?: TalerErrorDetail, + ): Promise<void> { + const { wex } = this; + await wex.runLegacyWalletDbTx(async (tx) => { + const [purchase, h] = await this.getRecordHandle(tx); + if (purchase?.purchaseStatus != fromSt) { + return; + } + purchase.purchaseStatus = PurchaseStatus.Failed; + purchase.failReason = reason; + await h.update(purchase); + }); + } + async userAbortTransaction(reason?: TalerErrorDetail): Promise<void> { const { wex } = this; await wex.runLegacyWalletDbTx(async (tx) => { @@ -1668,7 +1684,7 @@ async function handleInsufficientFunds( logger.trace("got exchange error reply (see below)"); logger.trace(j2s(exchangeReply)); } - await ctx.userAbortTransaction({ + await ctx.failTransaction(proposal.purchaseStatus, { code: TalerErrorCode.WALLET_TRANSACTION_PROTOCOL_VIOLATION, message: `unable to handle /pay exchange error response (${exchangeReply.code})`, exchangeReply, @@ -1680,7 +1696,7 @@ async function handleInsufficientFunds( logger.trace(`excluded broken coin pub=${brokenCoinPub}`); if (!brokenCoinPub) { - await ctx.userAbortTransaction({ + await ctx.failTransaction(proposal.purchaseStatus, { code: TalerErrorCode.WALLET_TRANSACTION_PROTOCOL_VIOLATION, message: "Exchange claimed bad coin, but coin was not used.", brokenCoinPub, @@ -1692,7 +1708,7 @@ async function handleInsufficientFunds( TalerErrorCode.MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND ) { // We might support this in the future. - await ctx.userAbortTransaction({ + await ctx.failTransaction(proposal.purchaseStatus, { code: TalerErrorCode.WALLET_TRANSACTION_PROTOCOL_VIOLATION, message: "Denomination used in payment became invalid.", errorDetails: err, @@ -2016,6 +2032,7 @@ async function checkPaymentByProposalId( case PurchaseStatus.AbortingWithRefund: case PurchaseStatus.FailedAbort: case PurchaseStatus.FailedClaim: + case PurchaseStatus.Failed: case PurchaseStatus.FinalizingQueryingAutoRefund: case PurchaseStatus.PendingAcceptRefund: case PurchaseStatus.PendingPaying: @@ -3014,6 +3031,7 @@ export async function processPurchase( case PurchaseStatus.SuspendedQueryingAutoRefund: case PurchaseStatus.SuspendedQueryingRefund: case PurchaseStatus.SuspendedFinalizingQueryingAutoRefund: + case PurchaseStatus.Failed: case PurchaseStatus.FailedAbort: case PurchaseStatus.FailedPaidByOther: case PurchaseStatus.Expired: @@ -3317,7 +3335,7 @@ async function processPurchasePay( case TalerErrorCode.EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN: // We might want to handle this in the future by re-denomination, // for now we just abort. - await ctx.userAbortTransaction({ + await ctx.failTransaction(purchase.purchaseStatus, { code: TalerErrorCode.WALLET_TRANSACTION_PROTOCOL_VIOLATION, message: "Denomination used in payment became invalid.", errorDetails: err, @@ -3333,7 +3351,8 @@ async function processPurchasePay( if (resp.status === HttpStatusCode.UnavailableForLegalReasons) { logger.warn(`pay transaction aborted, merchant has KYC problems`); - await ctx.userAbortTransaction( + await ctx.failTransaction( + purchase.purchaseStatus, makeTalerErrorDetail(TalerErrorCode.WALLET_PAY_MERCHANT_KYC_MISSING, { exchangeResponse: await resp.json(), }), @@ -3343,7 +3362,8 @@ async function processPurchasePay( if (resp.status === HttpStatusCode.Gone) { logger.warn(`pay transaction aborted, order expired`); - await ctx.userAbortTransaction( + await ctx.failTransaction( + purchase.purchaseStatus, makeTalerErrorDetail(TalerErrorCode.WALLET_PAY_MERCHANT_ORDER_GONE, {}), ); return TaskRunResult.progress(); @@ -3789,6 +3809,10 @@ export function computePayMerchantTransactionState( return { major: TransactionMajorState.Expired, }; + case PurchaseStatus.Failed: + return { + major: TransactionMajorState.Failed, + }; case PurchaseStatus.FailedClaim: return { major: TransactionMajorState.Failed, @@ -3909,6 +3933,8 @@ export function computePayMerchantTransactionActions( return [TransactionAction.Delete]; case PurchaseStatus.AbortedIncompletePayment: return [TransactionAction.Delete]; + case PurchaseStatus.Failed: + return [TransactionAction.Delete]; case PurchaseStatus.FailedClaim: return [TransactionAction.Delete]; case PurchaseStatus.FailedAbort: diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts @@ -357,6 +357,22 @@ export class PeerPullCreditTransactionContext implements TransactionContext { this.wex.taskScheduler.stopShepherdTask(this.taskId); } + async failTransaction( + fromSt: PeerPullPaymentCreditStatus, + reason?: TalerErrorDetail, + ): Promise<void> { + const { wex } = this; + await wex.runLegacyWalletDbTx(async (tx) => { + const [rec, h] = await this.getRecordHandle(tx); + if (rec?.status != fromSt) { + return; + } + rec.status = PeerPullPaymentCreditStatus.Failed; + rec.failReason = reason; + await h.update(rec); + }); + } + async userFailTransaction(reason?: TalerErrorDetail): Promise<void> { await this.wex.runLegacyWalletDbTx(async (tx) => { const [rec, h] = await this.getRecordHandle(tx); @@ -517,7 +533,7 @@ async function queryPurseForPeerPullCredit( }); return TaskRunResult.finished(); case HttpStatusCode.NotFound: - await ctx.userFailTransaction(resp.detail); + await ctx.failTransaction(pullIni.status, resp.detail); return TaskRunResult.finished(); default: assertUnreachable(resp); @@ -663,7 +679,7 @@ async function processPeerPullCreditAbortingDeletePurse( }); return TaskRunResult.finished(); case HttpStatusCode.Forbidden: - await ctx.userFailTransaction(resp.detail); + await ctx.failTransaction(peerPullIni.status, resp.detail); return TaskRunResult.finished(); case HttpStatusCode.Conflict: // FIXME check if done ? @@ -848,15 +864,16 @@ async function processPeerPullCreditCreatePurse( } case HttpStatusCode.Forbidden: case HttpStatusCode.NotFound: - await ctx.userFailTransaction(resp.detail); + await ctx.failTransaction(pullIni.status, resp.detail); return TaskRunResult.finished(); case HttpStatusCode.Conflict: - await ctx.userFailTransaction({ code: resp.body.code }); + await ctx.failTransaction(pullIni.status, { code: resp.body.code }); return TaskRunResult.finished(); case HttpStatusCode.PaymentRequired: throw Error(`unexpected reserve merge response ${resp.case}`); case TalerErrorCode.EXCHANGE_RESERVES_PURSE_EXPIRATION_BEFORE_NOW: - await ctx.userFailTransaction( + await ctx.failTransaction( + pullIni.status, resp.detail ? { code: resp.detail?.code } : undefined, ); return TaskRunResult.finished(); diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts @@ -118,9 +118,7 @@ export class PeerPullDebitTransactionContext implements TransactionContext { }); } - async updateTransactionMeta( - tx: WalletIndexedDbTransaction, - ): Promise<void> { + async updateTransactionMeta(tx: WalletIndexedDbTransaction): Promise<void> { const rec = await tx.peerPullDebit.get(this.peerPullDebitId); if (rec == null) { await tx.transactionsMeta.delete(this.peerPullDebitId); @@ -201,15 +199,29 @@ export class PeerPullDebitTransactionContext implements TransactionContext { ); } + async failTransaction( + fromSt: PeerPullDebitRecordStatus, + reason?: TalerErrorDetail, + ): Promise<void> { + const { wex } = this; + await wex.runLegacyWalletDbTx(async (tx) => { + const [rec, h] = await this.getRecordHandle(tx); + if (rec?.status != fromSt) { + return; + } + rec.status = PeerPullDebitRecordStatus.Failed; + rec.failReason = reason; + await h.update(rec); + }); + } + async userDeleteTransaction(): Promise<void> { await this.wex.runLegacyWalletDbTx(async (tx) => { await this.deleteTransactionInTx(tx); }); } - async deleteTransactionInTx( - tx: WalletIndexedDbTransaction, - ): Promise<void> { + async deleteTransactionInTx(tx: WalletIndexedDbTransaction): Promise<void> { const [rec, h] = await this.getRecordHandle(tx); if (!rec) { return; @@ -356,7 +368,7 @@ async function handlePurseCreationConflict( conflict: PurseConflict, ): Promise<TaskRunResult> { if (conflict.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) { - await ctx.userFailTransaction(); + await ctx.failTransaction(peerPullInc.status, { ...conflict }); return TaskRunResult.finished(); } @@ -470,7 +482,7 @@ async function processPeerPullDebitDialogProposed( }); return TaskRunResult.finished(); case HttpStatusCode.NotFound: - await ctx.userFailTransaction(resp.detail); + await ctx.failTransaction(pullIni.status, resp.detail); return TaskRunResult.finished(); default: assertUnreachable(resp); @@ -612,7 +624,8 @@ async function processPeerPullDebitPendingDeposit( case "ok": continue; case HttpStatusCode.Gone: { - await ctx.userAbortTransaction( + await ctx.failTransaction( + peerPullInc.status, makeTalerErrorDetail( TalerErrorCode.WALLET_PEER_PULL_DEBIT_PURSE_GONE, {}, @@ -624,7 +637,7 @@ async function processPeerPullDebitPendingDeposit( return handlePurseCreationConflict(ctx, peerPullInc, resp.body); case HttpStatusCode.Forbidden: case HttpStatusCode.NotFound: - await ctx.userFailTransaction(resp.detail); + await ctx.failTransaction(peerPullInc.status, resp.detail); return TaskRunResult.finished(); default: assertUnreachable(resp); diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts @@ -126,9 +126,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext { }); } - async updateTransactionMeta( - tx: WalletIndexedDbTransaction, - ): Promise<void> { + async updateTransactionMeta(tx: WalletIndexedDbTransaction): Promise<void> { const rec = await tx.peerPushCredit.get(this.peerPushCreditId); if (rec == null) { await tx.transactionsMeta.delete(this.peerPushCreditId); @@ -283,9 +281,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext { ); } - async deleteTransactionInTx( - tx: WalletIndexedDbTransaction, - ): Promise<void> { + async deleteTransactionInTx(tx: WalletIndexedDbTransaction): Promise<void> { const [rec, h] = await this.getRecordHandle(tx); if (!rec) { return; @@ -418,6 +414,22 @@ export class PeerPushCreditTransactionContext implements TransactionContext { this.wex.taskScheduler.startShepherdTask(this.taskId); } + async failTransaction( + fromSt: PeerPushCreditStatus, + reason?: TalerErrorDetail, + ): Promise<void> { + const { wex } = this; + await wex.runLegacyWalletDbTx(async (tx) => { + const [rec, h] = await this.getRecordHandle(tx); + if (rec?.status != fromSt) { + return; + } + rec.status = PeerPushCreditStatus.Failed; + rec.failReason = reason; + await h.update(rec); + }); + } + async userFailTransaction(reason?: TalerErrorDetail): Promise<void> { await this.wex.runLegacyWalletDbTx(async (tx) => { const [rec, h] = await this.getRecordHandle(tx); @@ -888,7 +900,7 @@ async function processPendingMerge( return TaskRunResult.finished(); case HttpStatusCode.Forbidden: case HttpStatusCode.NotFound: - await ctx.userFailTransaction(mergeResp.detail); + await ctx.failTransaction(peerInc.status, mergeResp.detail); return TaskRunResult.finished(); default: assertUnreachable(mergeResp); @@ -1039,7 +1051,7 @@ async function processPeerPushDebitDialogProposed( }); return TaskRunResult.finished(); case HttpStatusCode.NotFound: - await ctx.userFailTransaction(resp.detail); + await ctx.failTransaction(pullIni.status, resp.detail); return TaskRunResult.finished(); default: assertUnreachable(resp); diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts @@ -116,9 +116,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext { }); } - async updateTransactionMeta( - tx: WalletIndexedDbTransaction, - ): Promise<void> { + async updateTransactionMeta(tx: WalletIndexedDbTransaction): Promise<void> { const rec = await tx.peerPushDebit.get(this.pursePub); if (rec == null) { await tx.transactionsMeta.delete(this.pursePub); @@ -219,9 +217,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext { }); } - async deleteTransactionInTx( - tx: WalletIndexedDbTransaction, - ): Promise<void> { + async deleteTransactionInTx(tx: WalletIndexedDbTransaction): Promise<void> { const [rec, h] = await this.getRecordHandle(tx); if (!rec) { return; @@ -330,6 +326,22 @@ export class PeerPushDebitTransactionContext implements TransactionContext { this.wex.taskScheduler.startShepherdTask(this.taskId); } + async failTransaction( + fromSt: PeerPushDebitStatus, + reason?: TalerErrorDetail, + ): Promise<void> { + const { wex } = this; + await wex.runLegacyWalletDbTx(async (tx) => { + const [rec, h] = await this.getRecordHandle(tx); + if (rec?.status != fromSt) { + return; + } + rec.status = PeerPushDebitStatus.Failed; + rec.failReason = reason; + await h.update(rec); + }); + } + async userFailTransaction(reason?: TalerErrorDetail): Promise<void> { await this.wex.runLegacyWalletDbTx(async (tx) => { const [rec, h] = await this.getRecordHandle(tx); @@ -477,7 +489,7 @@ async function handlePurseCreationConflict( const pursePub = peerPushInitiation.pursePub; const ctx = new PeerPushDebitTransactionContext(wex, pursePub); if (conflict.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) { - await ctx.userFailTransaction(); + await ctx.failTransaction(peerPushInitiation.status, { ...conflict }); return TaskRunResult.finished(); } @@ -700,7 +712,7 @@ async function processPeerPushDebitCreateReserve( continue; case HttpStatusCode.Forbidden: case HttpStatusCode.NotFound: - await ctx.userFailTransaction(resp.detail); + await ctx.failTransaction(peerPushInitiation.status, resp.detail); return TaskRunResult.finished(); case HttpStatusCode.Conflict: return handlePurseCreationConflict( @@ -752,7 +764,7 @@ async function processPeerPushDebitCreateReserve( ); case HttpStatusCode.Forbidden: case HttpStatusCode.NotFound: - await ctx.userFailTransaction(resp.detail); + await ctx.failTransaction(peerPushInitiation.status, resp.detail); return TaskRunResult.finished(); default: assertUnreachable(resp); @@ -799,7 +811,7 @@ async function processPeerPushDebitCreateReserve( }); return TaskRunResult.progress(); case HttpStatusCode.NotFound: - await ctx.userFailTransaction(resp.detail); + await ctx.failTransaction(peerPushInitiation.status, resp.detail); return TaskRunResult.finished(); default: assertUnreachable(resp); @@ -825,7 +837,7 @@ async function processPeerPushDebitAbortingDeletePurse( case HttpStatusCode.Conflict: throw Error("purse deletion conflict"); case HttpStatusCode.Forbidden: - await ctx.userFailTransaction(resp.detail); + await ctx.failTransaction(peerPushInitiation.status, resp.detail); return TaskRunResult.finished(); }