taler-typescript-core

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

commit 5661413726424d507c3dc8413d81ec35426cefc5
parent 32846ca06070f8a3548a9fac60122eb7ca588864
Author: Florian Dold <dold@taler.net>
Date:   Thu,  8 Oct 2026 15:58:11 +0200

wallet-core: report proposal claim retry errors to transaction waits

Expose the stored retry error before contract terms have been downloaded.
This lets waitTransactionState emit request-progress-error with the retry
counter and delay while a payment is in claim-proposal.

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

diff --git a/packages/taler-wallet-core/src/pay-merchant.test.ts b/packages/taler-wallet-core/src/pay-merchant.test.ts @@ -17,6 +17,7 @@ import { Amounts, AmountString, buildSigPS, + CancellationToken, ContractTermsUtil, decodeCrock, eddsaGetPublic, @@ -24,6 +25,7 @@ import { encodeCrock, HttpStatusCode, MerchantContractVersion, + NotificationType, RefreshReason, SelectedCoin, SignedTokenEnvelope, @@ -38,6 +40,7 @@ import { TimerHandle, TransactionMajorState, TransactionMinorState, + WalletNotification, } from "@gnu-taler/taler-util"; import assert from "node:assert"; import { test } from "node:test"; @@ -55,6 +58,7 @@ import { WalletSlate, WalletToken, timestampProtocolToDb, + timestampPreciseToDb, } from "./db/records.js"; import { WalletDbTransaction } from "./db/transaction.js"; import { @@ -94,6 +98,10 @@ import { makeIdbRunner, runnerFactories } from "./db/testing/runners.js"; import type { WalletExecutionContext } from "./wallet.js"; import { nativeCrypto } from "./crypto/cryptoImplementation.js"; import { HeadersImpl } from "@gnu-taler/taler-util/http"; +import { dispatchRequestInternal } from "./requests.js"; +import { getTransactionById } from "./transactions.js"; +import { WalletApiOperation } from "./wallet-api-types.js"; +import { TaskIdentifiers } from "./common.js"; interface ScheduledTimer { due: number; @@ -171,6 +179,111 @@ function claimPurchase( } for (const makeRunner of runnerFactories) { + test(`${makeRunner.name}: proposal claim errors reach transaction waits`, async (t) => { + const runner = await makeRunner(); + const proposalId = "claim-progress-error"; + const key = encodeCrock(new Uint8Array(32).fill(1)); + const purchase = { + ...claimPurchase(proposalId), + noncePriv: key, + noncePub: key, + secretSeed: key, + }; + const taskId = TaskIdentifiers.forPay(purchase); + const transactionId = `txn:payment:${proposalId}` as TransactionIdStr; + const notifications: WalletNotification[] = []; + const cts = CancellationToken.create(); + const wex = { + cts, + cancellationToken: cts.token, + ws: { + initCalled: true, + progressMap: new Map(), + timerGroup: new TimerGroup(new ManualTimer()), + notify: (n: WalletNotification) => notifications.push(n), + addNotificationListener: () => () => {}, + }, + taskScheduler: { async ensureRunning() {} }, + runWalletDbTx: <T>(f: (tx: WalletDbTransaction) => Promise<T>) => + runner.runReadWriteTx(f), + } as unknown as WalletExecutionContext; + const error = { + code: TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, + hint: "Unexpected HTTP status 500 in response", + requestUrl: "https://merchant.example/orders/order/claim", + requestMethod: "POST", + httpStatusCode: 500, + }; + t.mock.method(Date.prototype, "getTime", () => 1000); + try { + await runner.runReadWriteTx(async (tx) => { + await tx.upsertPurchase(purchase); + await tx.upsertOperationRetry({ + id: taskId, + lastError: error, + retryInfo: { + firstTry: timestampPreciseToDb( + TalerPreciseTimestamp.fromMilliseconds(0), + ), + nextRetry: timestampPreciseToDb( + TalerPreciseTimestamp.fromMilliseconds(6000), + ), + retryCounter: 3, + }, + }); + }); + const transaction = await getTransactionById(wex, { transactionId }); + assert.deepStrictEqual(transaction.error, error); + assert.strictEqual( + transaction.txState.minor, + TransactionMinorState.ClaimProposal, + ); + for (const operation of [ + WalletApiOperation.WaitTransactionState, + WalletApiOperation.TestingWaitTransactionState, + ]) { + const result = await dispatchRequestInternal(wex, operation, { + transactionId, + progressToken: "claim-progress", + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.ClaimProposal, + }, + }); + assert.deepStrictEqual(result, { + matched: "target", + txState: transaction.txState, + stId: PurchaseStatus.PendingDownloadingProposal, + }); + assert.deepStrictEqual( + notifications.filter( + (n) => + n.type === NotificationType.RequestProgressError && + n.operation === operation, + ), + [ + { + type: NotificationType.RequestProgressError, + operation, + progressToken: "claim-progress", + error, + nextRetryDelay: { d_us: 5_000_000 }, + retryCounter: 3, + }, + ], + ); + } + await runner.runReadWriteTx((tx) => tx.deleteOperationRetry(taskId)); + assert.strictEqual( + (await getTransactionById(wex, { transactionId })).error, + undefined, + ); + } finally { + wex.ws.timerGroup.stopCurrentAndFutureTimers(); + await runner.close(); + } + }); + for (const scenario of [ { name: "keeps monitoring before the deadline", diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -412,7 +412,7 @@ export class PayMerchantTransactionContext implements TransactionContext { transactionId: this.transactionId, abortReason: purchaseRec.abortReason, failReason: purchaseRec.failReason, - error: undefined, + error: payRetryRec?.lastError, info: undefined, contractTerms: undefined, refundQueryActive: false,