taler-typescript-core

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

commit f554c3a1abccd9cc04aca4a233f5a7065431b85f
parent 9d1a5a06c40c63481555959ca79beee57771e0e7
Author: Florian Dold <dold@taler.net>
Date:   Thu, 20 Aug 2026 19:06:45 +0200

wallet-core: require exact donation unit selection

Diffstat:
Apackages/taler-wallet-core/src/donau.test.ts | 43+++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/donau.ts | 57++++++++++++++++++++++++++++++++++++---------------------
2 files changed, 79 insertions(+), 21 deletions(-)

diff --git a/packages/taler-wallet-core/src/donau.test.ts b/packages/taler-wallet-core/src/donau.test.ts @@ -0,0 +1,43 @@ +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { Amounts } from "@gnu-taler/taler-util"; +import assert from "node:assert"; +import { test } from "node:test"; +import { + CandidateDonationUnit, + selectDonationUnitsExact, +} from "./donau.js"; + +function candidate(value: string): CandidateDonationUnit { + return { + value: `EUR:${value}`, + unitHash: `hash-${value}`, + unitKey: { cipher: "RSA" }, + } as unknown as CandidateDonationUnit; +} + +test("donation units must represent the requested amount exactly", () => { + const two = candidate("2"); + + assert.strictEqual( + selectDonationUnitsExact([two], Amounts.parseOrThrow("EUR:3")), + undefined, + ); + assert.deepStrictEqual( + selectDonationUnitsExact([two], Amounts.parseOrThrow("EUR:4")), + [two, two], + ); +}); diff --git a/packages/taler-wallet-core/src/donau.ts b/packages/taler-wallet-core/src/donau.ts @@ -333,12 +333,40 @@ export async function handleGetDonau( /** * Info about a donation unit key from the donau. */ -interface CandidateDonationUnit { +export interface CandidateDonationUnit { value: AmountString; unitHash: HashCodeString; unitKey: DonauUnitPubKey; } +export function selectDonationUnitsExact( + candidates: CandidateDonationUnit[], + amount: AmountLike, +): CandidateDonationUnit[] | undefined { + let remaining = Amount.from(amount); + const selection: CandidateDonationUnit[] = []; + + let i = 0; + while (i < candidates.length) { + if (remaining.isZero()) { + return selection; + } + const cand = candidates[i]; + if (Amounts.isZero(cand.value)) { + // Would never reduce the remaining amount. + i++; + continue; + } + if (Amounts.cmp(remaining, cand.value) >= 0) { + selection.push(cand); + remaining = remaining.sub(cand.value); + } else { + i++; + } + } + return remaining.isZero() ? selection : undefined; +} + /** * Filter out applicable donation units * from the donau keys response. @@ -456,26 +484,13 @@ export async function generateDonauPlanchets( logger.info(`created ${candidates.length} donau candidates`); - let remaining = Amount.from(res.donauAmount); - const selection: CandidateDonationUnit[] = []; - - let i = 0; - while (i < candidates.length) { - if (remaining.isZero()) { - break; - } - const cand = candidates[i]; - if (Amounts.isZero(cand.value)) { - // Would never reduce the remaining amount. - i++; - continue; - } - if (Amounts.cmp(remaining, cand.value) >= 0) { - selection.push(cand); - remaining = remaining.sub(cand.value); - } else { - i++; - } + const selection = selectDonationUnitsExact(candidates, res.donauAmount); + if (!selection) { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_TRANSACTION_PROTOCOL_VIOLATION, + {}, + `donation amount ${res.donauAmount} cannot be represented by the offered donation units`, + ); } const donauPlanchets: WalletDonationPlanchet[] = [];