taler-typescript-core

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

commit 86ad465fe282ba77bf50359a7f91ebbccc5be41b
parent ab42503d8c53af369e2abb1170c68d7f7013dfa2
Author: Florian Dold <dold@taler.net>
Date:   Thu,  3 Sep 2026 15:51:25 +0200

wallet-core: test change distribution and master keys in selection

Diffstat:
Mpackages/taler-wallet-core/src/coinSelection.test.ts | 83++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
1 file changed, 68 insertions(+), 15 deletions(-)

diff --git a/packages/taler-wallet-core/src/coinSelection.test.ts b/packages/taler-wallet-core/src/coinSelection.test.ts @@ -958,7 +958,7 @@ test("demo: deposit max after withdraw raw 13", (t) => { // current wallet impl fee 0.14 }); -test("overpay when remaining < depositFee", (t) => { +test("change never drops a contribution below its deposit fee", (t) => { const instructedAmount = Amounts.parseOrThrow("LOCAL:2.1"); const tally = emptyTallyForPeerPayment({ instructedAmount, @@ -990,22 +990,75 @@ test("overpay when remaining < depositFee", (t) => { assert.ok(coins != null); - assert.deepStrictEqual(coins, { - "hash0;32;http://exchange.localhost/": { - exchangeBaseUrl: "http://exchange.localhost/", - exchangeMasterPub: "123", - denomPubHash: "hash0", - maxAge: 32, - contributions: [Amounts.parseOrThrow("LOCAL:2")], + // The excess of 0.9 over the instructed amount comes out of the smallest + // coin only down to its deposit fee; the rest comes out of the next one. + // The exchange refuses a coin whose contribution is below its fee. + let total = Amounts.zeroOfCurrency("LOCAL"); + for (const sel of Object.values(coins)) { + for (const contribution of sel.contributions) { + assert.ok( + Amounts.cmp(contribution, "LOCAL:0.2") >= 0, + `contribution ${Amounts.stringify(contribution)} below deposit fee`, + ); + total = Amounts.add(total, contribution).amount; + } + } + assert.strictEqual(Amounts.stringify(total), "LOCAL:2.1"); + assert.deepStrictEqual( + coins["hash1;32;http://exchange.localhost/"]?.contributions, + [Amounts.parseOrThrow("LOCAL:0.2")], + ); + assert.deepStrictEqual( + coins["hash0;32;http://exchange.localhost/"]?.contributions, + [Amounts.parseOrThrow("LOCAL:1.9")], + ); +}); + +test("coins of one denomination under two master keys stay separate", (t) => { + const instructedAmount = Amounts.parseOrThrow("LOCAL:2"); + const tally = emptyTallyForPeerPayment({ + instructedAmount, + }); + tally.amountDepositFeeLimitRemaining = Amounts.parseOrThrow("LOCAL:1"); + + // The same denomination, re-signed under the exchange's new master key. + const candidates = createCandidates([ + { + amount: "LOCAL:1" as AmountString, + numAvailable: 1, + depositFee: "LOCAL:0" as AmountString, + fromExchange: "http://exchange.localhost/", + fromMasterPub: "OLDKEY", }, - "hash1;32;http://exchange.localhost/": { - exchangeBaseUrl: "http://exchange.localhost/", - exchangeMasterPub: "123", - denomPubHash: "hash1", - maxAge: 32, - contributions: [Amounts.parseOrThrow("LOCAL:0.1")], + { + amount: "LOCAL:1" as AmountString, + numAvailable: 1, + depositFee: "LOCAL:0" as AmountString, + fromExchange: "http://exchange.localhost/", + fromMasterPub: "NEWKEY", }, - }); + ]); + candidates[1].denomPubHash = candidates[0].denomPubHash; + const coins = testing_selectGreedy( + { + wireFeesPerExchange: {}, + }, + candidates, + tally, + ); + + assert.ok(coins != null); + // Each entry is later looked up in the database by its master key, so + // the two coins must not be folded into one entry. + const entries = Object.values(coins); + assert.strictEqual(entries.length, 2); + assert.deepStrictEqual( + entries.map((e) => e.exchangeMasterPub).sort(), + ["NEWKEY", "OLDKEY"], + ); + for (const e of entries) { + assert.deepStrictEqual(e.contributions, [Amounts.parseOrThrow("LOCAL:1")]); + } }); test("DD91 removes redundant small coins after ascending selection", (t) => {