commit 82a69a10cb2e98ff2d6c98afb85abb9bd30d7be9
parent 86ad465fe282ba77bf50359a7f91ebbccc5be41b
Author: Florian Dold <dold@taler.net>
Date: Thu, 3 Sep 2026 15:52:32 +0200
wallet-core: keep each coin's contribution above its deposit fee
Assigning all of the change to the smallest coin could leave it with
less than its fee, which the exchange and merchant reject after the
coins were already spent.
Diffstat:
1 file changed, 15 insertions(+), 7 deletions(-)
diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts
@@ -1344,9 +1344,9 @@ function selectGreedyDefault(
finalCoins.length > 0 || Amounts.isZero(tally.amountPayRemaining),
);
- // Account for all fees before assigning the change to the smallest coin.
+ // Account for all fees before taking the change out of the coins.
// The selection is irreducible after pass 2, so the excess is smaller than
- // that coin's value.
+ // the smallest coin's value.
for (const coin of finalCoins) {
tallyFees(
tally,
@@ -1355,7 +1355,7 @@ function selectGreedyDefault(
Amounts.parseOrThrow(coin.denom.feeDeposit),
);
}
- const excess = amountFromUnits(
+ let excess = amountFromUnits(
currency,
selection.totals.value - amountToUnits(tally.amountPayRemaining),
);
@@ -1363,12 +1363,20 @@ function selectGreedyDefault(
checkLogicInvariant(Amounts.cmp(excess, finalCoins[0].denom.value) < 0);
}
const selectedDenom: SelResult = {};
+ // The change comes out of the smallest coins first, but a coin never
+ // contributes less than its own deposit fee: the exchange rejects such a
+ // deposit. Whatever the smallest coin cannot absorb moves up to the next
+ // one; if no coin can absorb it, the customer overpays by that much.
for (let i = 0; i < finalCoins.length; i++) {
const denom = finalCoins[i].denom;
- const contribution =
- i === 0
- ? Amounts.sub(denom.value, excess).amount
- : Amounts.parseOrThrow(denom.value);
+ const value = Amounts.parseOrThrow(denom.value);
+ let contribution = value;
+ if (!Amounts.isZero(excess)) {
+ const absorbable = Amounts.sub(value, denom.feeDeposit).amount;
+ const take = Amounts.min(excess, absorbable);
+ contribution = Amounts.sub(value, take).amount;
+ excess = Amounts.sub(excess, take).amount;
+ }
applyContributions(selectedDenom, [contribution], denom);
tally.amountPayRemaining = Amounts.sub(
tally.amountPayRemaining,