taler-typescript-core

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

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

wallet-core: map payment signatures by output index

Diffstat:
Mpackages/taler-wallet-core/src/pay-merchant.test.ts | 40++++++++++++++++++++++++++++++++++++----
Mpackages/taler-wallet-core/src/pay-merchant.ts | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++----
2 files changed, 91 insertions(+), 8 deletions(-)

diff --git a/packages/taler-wallet-core/src/pay-merchant.test.ts b/packages/taler-wallet-core/src/pay-merchant.test.ts @@ -128,18 +128,24 @@ test("payment output signatures are mandatory and partitioned exactly", () => { const donauSig = outputSig("donau"); assert.deepStrictEqual( - splitPaymentOutputTokenSignatures([slateSig, donauSig], 1, 1), + splitPaymentOutputTokenSignatures([slateSig, donauSig], [0], 1, 1), { slateTokenSigs: [slateSig], donauTokenSigs: [donauSig], }, ); assert.throws( - () => splitPaymentOutputTokenSignatures(undefined, 1, 0), + () => splitPaymentOutputTokenSignatures(undefined, [0], undefined, 0), /returned 0 token signatures, expected 1/, ); assert.throws( - () => splitPaymentOutputTokenSignatures([slateSig, donauSig], 1, 0), + () => + splitPaymentOutputTokenSignatures( + [slateSig, donauSig], + [0], + undefined, + 0, + ), /returned 2 token signatures, expected 1/, ); }); @@ -147,7 +153,13 @@ test("payment output signatures are mandatory and partitioned exactly", () => { test("cached output signatures recover an older partial finalization", () => { const cached = outputSig("cached"); assert.deepStrictEqual( - splitPaymentOutputTokenSignatures(undefined, 1, 0, [cached]), + splitPaymentOutputTokenSignatures( + undefined, + [0], + undefined, + 0, + [cached], + ), { slateTokenSigs: [cached], donauTokenSigs: [], @@ -155,6 +167,26 @@ test("cached output signatures recover an older partial finalization", () => { ); }); +test("payment output signatures follow interleaved contract outputs", () => { + const firstSlate = outputSig("slate-0"); + const firstDonau = outputSig("donau-0"); + const secondDonau = outputSig("donau-1"); + const secondSlate = outputSig("slate-2"); + + assert.deepStrictEqual( + splitPaymentOutputTokenSignatures( + [firstSlate, firstDonau, secondDonau, secondSlate], + [0, 2], + 1, + 2, + ), + { + slateTokenSigs: [firstSlate, secondSlate], + donauTokenSigs: [firstDonau, secondDonau], + }, + ); +}); + function refundGroup( id: string, raw: AmountString, diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -1809,7 +1809,8 @@ async function storeFirstPaySuccess( export function splitPaymentOutputTokenSignatures( responseTokenSigs: SignedTokenEnvelope[] | undefined, - slateCount: number, + slateOutputIndexes: number[], + donauOutputIndex: number | undefined, donauCount: number, cachedSlateSigs?: SignedTokenEnvelope[], ): { @@ -1817,6 +1818,7 @@ export function splitPaymentOutputTokenSignatures( donauTokenSigs: SignedTokenEnvelope[]; } { const tokenSigs = responseTokenSigs ?? []; + const slateCount = slateOutputIndexes.length; const expectedCount = slateCount + donauCount; if ( @@ -1840,9 +1842,45 @@ export function splitPaymentOutputTokenSignatures( `merchant returned ${tokenSigs.length} token signatures, expected ${expectedCount}`, ); } + + if (donauCount > 0 && donauOutputIndex === undefined) { + throw Error("donation signatures require a contract output index"); + } + + const responsePositions: Array< + | { outputIndex: number; subIndex: number; kind: "slate"; index: number } + | { outputIndex: number; subIndex: number; kind: "donau"; index: number } + > = slateOutputIndexes.map((outputIndex, index) => ({ + outputIndex, + subIndex: index, + kind: "slate", + index, + })); + for (let index = 0; index < donauCount; index++) { + responsePositions.push({ + outputIndex: donauOutputIndex!, + subIndex: index, + kind: "donau", + index, + }); + } + responsePositions.sort( + (a, b) => a.outputIndex - b.outputIndex || a.subIndex - b.subIndex, + ); + + const slateTokenSigs: SignedTokenEnvelope[] = new Array(slateCount); + const donauTokenSigs: SignedTokenEnvelope[] = new Array(donauCount); + for (let responseIndex = 0; responseIndex < tokenSigs.length; responseIndex++) { + const position = responsePositions[responseIndex]; + if (position.kind === "slate") { + slateTokenSigs[position.index] = tokenSigs[responseIndex]; + } else { + donauTokenSigs[position.index] = tokenSigs[responseIndex]; + } + } return { - slateTokenSigs: tokenSigs.slice(0, slateCount), - donauTokenSigs: tokenSigs.slice(slateCount), + slateTokenSigs, + donauTokenSigs, }; } @@ -3271,6 +3309,11 @@ async function processPurchasePay( index, ]}`, ); + slateRes.sort( + (a, b) => + (a.outputIndex ?? 0) - (b.outputIndex ?? 0) || + (a.repeatIndex ?? 0) - (b.repeatIndex ?? 0), + ); slateRes.forEach((s) => { slates?.push(s); wallet_data?.tokens_evs.push(s.tokenEv); @@ -3284,6 +3327,7 @@ async function processPurchasePay( // FIXME: Merge with transaction above const res = await wex.runWalletDbTx(async (tx) => { const recs = await tx.getDonationPlanchetsByProposal(proposalId); + recs.sort((a, b) => a.udiIndex - b.udiIndex); for (const rec of recs) { budikeypairs.push({ blinded_udi: rec.blindedUdi, @@ -3455,7 +3499,14 @@ async function processPurchasePay( const { slateTokenSigs, donauTokenSigs } = splitPaymentOutputTokenSignatures( merchantResp.token_sigs, - slateList.length, + slateList.map((slate) => { + checkDbInvariant( + slate.outputIndex !== undefined, + `slate ${slate.tokenUsePub} has no output index`, + ); + return slate.outputIndex; + }), + purchase.donauOutputIndex, donauPlanchetList.length, payInfo.slateTokenSigs, );