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:
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(