taler-typescript-core

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

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:
Mpackages/taler-wallet-core/src/instructedAmountConversion.test.ts | 39+++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/instructedAmountConversion.ts | 53++++++++++++++++++++++++++++++++++++++---------------
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) ||