taler-typescript-core

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

commit 017c67f81cb2ee4a67144fc3bc593a2188e690b5
parent 32122aae69e5d2712f29ea0df97ec77d9ca3a87e
Author: Florian Dold <florian@dold.me>
Date:   Fri,  7 Feb 2025 14:46:49 +0100

wallet-core: don't overspend when there is still a deposit fee allowance

Thanks for Theodor Straube for finding this bug and contributing a test
case.

Diffstat:
Mpackages/taler-wallet-core/src/coinSelection.test.ts | 47+++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/coinSelection.ts | 6+++++-
2 files changed, 52 insertions(+), 1 deletion(-)

diff --git a/packages/taler-wallet-core/src/coinSelection.test.ts b/packages/taler-wallet-core/src/coinSelection.test.ts @@ -181,6 +181,7 @@ test("pay: select one coin to pay with fee", (t) => { customerWireFees: Amounts.parse("LOCAL:0.1"), wireFeeCoveredForExchange: new Set(["http://exchange.localhost/"]), lastDepositFee: Amounts.parse("LOCAL:0.1"), + totalDepositFees: Amounts.parse("LOCAL:0.1"), }); }); @@ -452,3 +453,49 @@ test("demo: deposit max after withdraw raw 13", (t) => { // current wallet impl fee 0.14 }); + +test("overpay when remaining < depositFee", (t) => { + const instructedAmount = Amounts.parseOrThrow("LOCAL:2.1"); + const tally = emptyTallyForPeerPayment({ + instructedAmount, + }); + tally.amountDepositFeeLimitRemaining = Amounts.parseOrThrow("LOCAL:0.5"); + + const coins = testing_selectGreedy( + { + wireFeesPerExchange: {}, + }, + createCandidates([ + { + amount: "LOCAL:2" as AmountString, + numAvailable: 1, + depositFee: "LOCAL:0.2" as AmountString, + fromExchange: "http://exchange.localhost/", + }, + { + amount: "LOCAL:1" as AmountString, + numAvailable: 1, + depositFee: "LOCAL:0.2" as AmountString, + fromExchange: "http://exchange.localhost/", + }, + ]), + tally, + ); + + t.assert(coins != null); + + t.deepEqual(coins, { + "hash0;32;http://exchange.localhost/": { + exchangeBaseUrl: "http://exchange.localhost/", + denomPubHash: "hash0", + maxAge: 32, + contributions: [Amounts.parseOrThrow("LOCAL:2")], + }, + "hash1;32;http://exchange.localhost/": { + exchangeBaseUrl: "http://exchange.localhost/", + denomPubHash: "hash1", + maxAge: 32, + contributions: [Amounts.parseOrThrow("LOCAL:0.1")], + }, + }); +}); diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts @@ -650,6 +650,9 @@ function selectGreedy( i < denom.numAvailable && Amounts.isNonZero(tally.amountPayRemaining); i++ ) { + // Save the allowance *before* tallying. + const depositFeeAllowance = tally.amountDepositFeeLimitRemaining; + tallyFees( tally, req.wireFeesPerExchange, @@ -659,7 +662,8 @@ function selectGreedy( const coinSpend = Amounts.max( Amounts.min(tally.amountPayRemaining, denom.value), - denom.feeDeposit, + // Underflow saturates to zero + Amounts.sub(denom.feeDeposit, depositFeeAllowance).amount, ); tally.amountPayRemaining = Amounts.sub(