taler-typescript-core

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

commit 25fe7beae8fd18b008f4a014a7655be37fd0ccb9
parent 5c4f580867b6c6852d291a5e52949cedf71e995e
Author: Florian Dold <dold@taler.net>
Date:   Thu, 20 Aug 2026 19:06:47 +0200

wallet-core: do not retry template order creation

Diffstat:
Mpackages/taler-wallet-core/src/pay-template.test.ts | 61++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mpackages/taler-wallet-core/src/pay-template.ts | 17++++++++---------
2 files changed, 68 insertions(+), 10 deletions(-)

diff --git a/packages/taler-wallet-core/src/pay-template.test.ts b/packages/taler-wallet-core/src/pay-template.test.ts @@ -16,6 +16,7 @@ import { AmountString, + CancellationToken, HostPortPath, TalerPayTemplateUri, TalerUriAction, @@ -25,7 +26,12 @@ import { } from "@gnu-taler/taler-util"; import assert from "node:assert"; import { test } from "node:test"; -import { applyTemplateUriOverrides } from "./pay-template.js"; +import { + applyTemplateUriOverrides, + instantiateTemplateRaw, +} from "./pay-template.js"; +import { ProgressContext } from "./progress.js"; +import { WalletExecutionContext } from "./wallet.js"; function paivanaUri(amount: AmountString): TalerPayTemplateUri { return { @@ -84,3 +90,56 @@ test("a fixed paivana amount leaves other currencies and complex choices alone", ["EUR:5", "KUDOS:5"], ); }); + +test("template order creation is not retried after an ambiguous failure", async () => { + const cts = CancellationToken.create(); + let requestCount = 0; + const progressContext: ProgressContext = { + operation: "preparePayForTemplateV2", + progressToken: "progress-token", + finished: false, + cts, + progressContextId: 1, + }; + const wex = { + cancellationToken: cts.token, + progressContext, + http: { + async fetch(): Promise<never> { + requestCount++; + if (requestCount > 1) { + cts.cancel(); + } + throw Error("connection lost after sending request"); + }, + }, + ws: { + notify(): void {}, + timerGroup: { + async resolveAfter(): Promise<boolean> { + return true; + }, + }, + }, + } as unknown as WalletExecutionContext; + + await assert.rejects( + instantiateTemplateRaw( + wex, + { + talerPayTemplateUri: + "taler+http://pay-template/merchant.example.com:1234/template-id", + progressToken: "progress-token", + }, + { + template_contract: { + template_type: TemplateType.FIXED_ORDER, + amount: "TESTKUDOS:1", + summary: "test order", + }, + }, + ), + /connection lost/, + ); + assert.strictEqual(requestCount, 1); +}); diff --git a/packages/taler-wallet-core/src/pay-template.ts b/packages/taler-wallet-core/src/pay-template.ts @@ -325,15 +325,14 @@ export async function instantiateTemplateRaw( req.templateParams ?? {}, ); - // Retried under a progress context. Instantiating a template creates a - // fresh order server-side, so a retry after a failure just starts another - // one; the order the wallet ends up paying is the one this call returns. - const resp = await runWithProgressRetries(wex, async () => - succeedOrThrow( - await merchantApi.useTemplateCreateOrder( - parsedUri.templateId, - templateDetails, - ), + // Instantiating a template is not idempotent. In particular, a transport + // failure can happen after the merchant committed the new order. Retrying + // here would create an additional order with no way to identify the first + // one, so this POST is attempted exactly once. + const resp = succeedOrThrow( + await merchantApi.useTemplateCreateOrder( + parsedUri.templateId, + templateDetails, ), );