taler-typescript-core

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

commit 195c58648cc736ffe9e4be0dcfd235d96b2a5e66
parent d0775fae165d66ed22f21a688b00a4d62508022c
Author: Florian Dold <dold@taler.net>
Date:   Tue, 21 Jul 2026 11:05:13 +0200

util: validate currencies up front in sub, normalize in divide

sub() returned early on underflow, so a mismatched currency in a later
operand was never reported; divide() was the only arithmetic entry point
not going through jsonifyAmount, and never normalized its dividend.

Diffstat:
Mpackages/taler-util/src/amounts.ts | 28+++++++++++++++++++---------
1 file changed, 19 insertions(+), 9 deletions(-)

diff --git a/packages/taler-util/src/amounts.ts b/packages/taler-util/src/amounts.ts @@ -447,11 +447,17 @@ export class Amounts { let value = aJ.value + Math.floor(aJ.fraction / amountFractionalBase); let fraction = aJ.fraction % amountFractionalBase; - for (const b of rest) { - const bJ = Amounts.jsonifyAmount(b); + // Validate every operand before subtracting any of them, as add() does. + // Checking inside the loop made a mismatched currency invisible once an + // earlier operand had already saturated the result. + const restJ = rest.map((b) => Amounts.jsonifyAmount(b)); + for (const bJ of restJ) { if (bJ.currency.toUpperCase() !== aJ.currency.toUpperCase()) { throw Error(`Mismatched currency: ${bJ.currency} and ${currency}`); } + } + + for (const bJ of restJ) { if (fraction < bJ.fraction) { if (value < 1) { return { @@ -512,23 +518,27 @@ export class Amounts { /** * Divide an amount. Throws on division by zero. */ - static divide(a: AmountJson, n: number): AmountJson { + static divide(a: AmountLike, n: number): AmountJson { if (!Number.isInteger(n) || n <= 0) { throw Error(`Amount can only be divided by a positive integer`); } + const aJ = Amounts.jsonifyAmount(a); + // Normalize the dividend before dividing, as the C reference does; an + // out-of-range fraction would otherwise survive into the quotient. + const value = aJ.value + Math.floor(aJ.fraction / amountFractionalBase); + const fraction = aJ.fraction % amountFractionalBase; if (n === 1) { - return { value: a.value, fraction: a.fraction, currency: a.currency }; + return { value, fraction, currency: aJ.currency }; } - const r = a.value % n; + const r = value % n; // Scale in BigInt: r can be as large as n-1, so r * amountFractionalBase // exceeds 2^53 and float64 would round the fraction's low bits away. const scaled = - (BigInt(r) * BigInt(amountFractionalBase) + BigInt(a.fraction)) / - BigInt(n); + (BigInt(r) * BigInt(amountFractionalBase) + BigInt(fraction)) / BigInt(n); return { - currency: a.currency, + currency: aJ.currency, fraction: Number(scaled), - value: Math.floor(a.value / n), + value: Math.floor(value / n), }; }