commit 9d1a5a06c40c63481555959ca79beee57771e0e7
parent ec559f2064a4fa8a9b2b4473d9db58ff27a54a39
Author: Florian Dold <dold@taler.net>
Date: Thu, 20 Aug 2026 19:06:45 +0200
wallet-core: respect instructed conversion inventory
Diffstat:
2 files changed, 77 insertions(+), 15 deletions(-)
diff --git a/packages/taler-wallet-core/src/instructedAmountConversion.test.ts b/packages/taler-wallet-core/src/instructedAmountConversion.test.ts
@@ -620,3 +620,42 @@ test("demo: withdraw raw 13", (t) => {
//current wallet impl: hides the left in reserve fee
//shows fee = 0.2
});
+
+test("deposit refresh search does not reuse an exhausted denomination", () => {
+ const onlyCoin = defaultFeeConfig(kudos`2`, 1);
+ const result = convertDepositAmountForAvailableCoins(
+ { list: [onlyCoin], exchanges: {} },
+ kudos`3`,
+ TransactionAmountMode.Effective,
+ );
+
+ assert.strictEqual(Amounts.stringifyValue(result.effective), "2");
+ assert.strictEqual(result.refresh, undefined);
+});
+
+test("zero-fee denominations rank ahead of fee-bearing denominations", () => {
+ const paid = {
+ ...defaultFeeConfig(kudos`2`, 1),
+ id: "paid",
+ };
+ const free = {
+ ...defaultFeeConfig(kudos`2`, 1),
+ id: "free",
+ denomDeposit: kudos`0`,
+ denomWithdraw: kudos`0`,
+ };
+
+ const deposit = convertDepositAmountForAvailableCoins(
+ { list: [paid, free], exchanges: {} },
+ kudos`2`,
+ TransactionAmountMode.Effective,
+ );
+ assert.strictEqual(Amounts.stringifyValue(deposit.raw), "2");
+
+ const withdrawal = convertWithdrawalAmountFromAvailableCoins(
+ { list: [paid, free], exchanges: {} },
+ kudos`2`,
+ TransactionAmountMode.Effective,
+ );
+ assert.strictEqual(Amounts.stringifyValue(withdrawal.raw), "2");
+});
diff --git a/packages/taler-wallet-core/src/instructedAmountConversion.ts b/packages/taler-wallet-core/src/instructedAmountConversion.ts
@@ -476,6 +476,12 @@ function searchBestRefreshCoin(
let refreshIdx = 0;
refreshIteration: while (refreshIdx < depositDenoms.length) {
const d = depositDenoms[refreshIdx];
+ if (d.total <= 0) {
+ // The initial deposit selection consumes from this same inventory.
+ // Do not manufacture another input coin for the refresh leg.
+ refreshIdx++;
+ continue refreshIteration;
+ }
const denomContribution =
mode === TransactionAmountMode.Effective
@@ -547,6 +553,26 @@ function searchBestRefreshCoin(
return choice;
}
+/** Sort higher value-per-fee ratios first, with a zero fee as the optimum. */
+function compareValuePerFee(
+ value1: AmountJson,
+ fee1: AmountJson,
+ value2: AmountJson,
+ fee2: AmountJson,
+): number {
+ const fee1Zero = Amounts.isZero(fee1);
+ const fee2Zero = Amounts.isZero(fee2);
+ if (fee1Zero !== fee2Zero) {
+ return fee1Zero ? -1 : 1;
+ }
+ if (fee1Zero) {
+ return 0;
+ }
+ const rate1 = Amounts.divmod(value1, fee1).quotient;
+ const rate2 = Amounts.divmod(value2, fee2).quotient;
+ return rate1 === rate2 ? 0 : rate1 < rate2 ? 1 : -1;
+}
+
/**
* Returns a copy of the list sorted for the best denom to withdraw first
*/
@@ -565,13 +591,12 @@ function rankDenominationForWithdrawals(
//ranking should take the relative contribution in the exchange
//which is (value - denomFee / fixedFee)
- const rate1 = Amounts.isZero(d1.denomWithdraw)
- ? Number.MIN_SAFE_INTEGER
- : Amounts.divmod(d1.value, d1.denomWithdraw).quotient;
- const rate2 = Amounts.isZero(d2.denomWithdraw)
- ? Number.MIN_SAFE_INTEGER
- : Amounts.divmod(d2.value, d2.denomWithdraw).quotient;
- const contribCmp = rate1 === rate2 ? 0 : rate1 < rate2 ? 1 : -1;
+ const contribCmp = compareValuePerFee(
+ d1.value,
+ d1.denomWithdraw,
+ d2.value,
+ d2.denomWithdraw,
+ );
return (
contribCmp ||
Duration.cmp(d1.duration, d2.duration) ||
@@ -623,14 +648,12 @@ function rankDenominationForDeposit(
// different exchanges may have different wireFee
// ranking should take the relative contribution in the exchange
// which is (value - denomFee / fixedFee)
- const rate1 = Amounts.isZero(d1.denomDeposit)
- ? Number.MIN_SAFE_INTEGER
- : Amounts.divmod(d1.value, d1.denomDeposit).quotient;
- const rate2 = Amounts.isZero(d2.denomDeposit)
- ? Number.MIN_SAFE_INTEGER
- : Amounts.divmod(d2.value, d2.denomDeposit).quotient;
-
- const contribCmp = rate1 === rate2 ? 0 : rate1 < rate2 ? 1 : -1;
+ const contribCmp = compareValuePerFee(
+ d1.value,
+ d1.denomDeposit,
+ d2.value,
+ d2.denomDeposit,
+ );
return (
contribCmp ||
Duration.cmp(d1.duration, d2.duration) ||