taler-typescript-core

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

commit 6affddc50527a8a4176e334166bff6b63c646ca6
parent 054af1c13e7f09ebb25437e9a38b36db7c0cfc5e
Author: Florian Dold <dold@taler.net>
Date:   Thu, 20 Aug 2026 19:06:45 +0200

wallet-core: reject incomplete withdrawal batches

Diffstat:
Mpackages/taler-wallet-core/src/withdraw.test.ts | 17++++++++++++++++-
Mpackages/taler-wallet-core/src/withdraw.ts | 22++++++++++++++++++++++
2 files changed, 38 insertions(+), 1 deletion(-)

diff --git a/packages/taler-wallet-core/src/withdraw.test.ts b/packages/taler-wallet-core/src/withdraw.test.ts @@ -30,7 +30,10 @@ import { } from "./db-common.js"; import { WalletDenomination } from "./db-common.js"; import { selectWithdrawalDenominations } from "./denomSelection.js"; -import { isWithdrawalFinalStatus } from "./withdraw.js"; +import { + isWithdrawalFinalStatus, + requireWithdrawalBatchCardinality, +} from "./withdraw.js"; test("every terminal withdrawal status ends final-state waits", () => { for (const status of [ @@ -54,6 +57,18 @@ test("every terminal withdrawal status ends final-state waits", () => { ); }); +test("withdrawal batches require one signature per planchet", () => { + assert.doesNotThrow(() => requireWithdrawalBatchCardinality(2, 2)); + assert.throws( + () => requireWithdrawalBatchCardinality(2, 1), + /returned 1 withdrawal signatures for 2 planchets/, + ); + assert.throws( + () => requireWithdrawalBatchCardinality(2, 3), + /returned 3 withdrawal signatures for 2 planchets/, + ); +}); + test("withdrawal selection bug repro", (t) => { const amount = { currency: "KUDOS", diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -1535,6 +1535,20 @@ interface WithdrawalBatchResult { batchResp: ExchangeWithdrawResponse; } +export function requireWithdrawalBatchCardinality( + requestedCount: number, + responseCount: number, +): void { + if (requestedCount === responseCount) { + return; + } + throw TalerError.fromDetail( + TalerErrorCode.WALLET_TRANSACTION_PROTOCOL_VIOLATION, + {}, + `exchange returned ${responseCount} withdrawal signatures for ${requestedCount} planchets`, + ); +} + /** * Transition a transaction from pending(ready) * into a pending(kyc|aml) state, in case KYC is required. @@ -1717,6 +1731,10 @@ async function processPlanchetExchangeLegacyBatchRequest( resp, codecForExchangeLegacyWithdrawBatchResponse(), ); + requireWithdrawalBatchCardinality( + requestCoinIdxs.length, + r.ev_sigs.length, + ); return { coinIdxs: requestCoinIdxs, batchResp: { ev_sigs: r.ev_sigs.map((x) => x.ev_sig) }, @@ -1910,6 +1928,10 @@ async function processPlanchetExchangeBatchRequest( // Forbidden or any unexpected status: treated as an error below. throwUnexpectedRequestError(resp.response, resp.detail!); } + requireWithdrawalBatchCardinality( + requestCoinIdxs.length, + resp.body.ev_sigs.length, + ); return { coinIdxs: requestCoinIdxs, batchResp: resp.body,