From 612b85c18fc17af412d08e075e1fddaa67aa7bf0 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 21 Feb 2024 13:01:23 +0100 Subject: move helpers to util --- .../taler-wallet-core/src/coinSelection.test.ts | 248 +++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 packages/taler-wallet-core/src/coinSelection.test.ts (limited to 'packages/taler-wallet-core/src/coinSelection.test.ts') diff --git a/packages/taler-wallet-core/src/coinSelection.test.ts b/packages/taler-wallet-core/src/coinSelection.test.ts new file mode 100644 index 000000000..839cd22fb --- /dev/null +++ b/packages/taler-wallet-core/src/coinSelection.test.ts @@ -0,0 +1,248 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ +import { + AbsoluteTime, + AmountString, + Amounts, + DenomKeyType, + Duration, + j2s, +} from "@gnu-taler/taler-util"; +import test from "ava"; +import { + AvailableDenom, + testing_greedySelectPeer, + testing_selectGreedy, +} from "./coinSelection.js"; + +const inTheDistantFuture = AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration(AbsoluteTime.now(), Duration.fromSpec({ hours: 1 })), +); + +const inThePast = AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.subtractDuraction( + AbsoluteTime.now(), + Duration.fromSpec({ hours: 1 }), + ), +); + +test("p2p: should select the coin", (t) => { + const instructedAmount = Amounts.parseOrThrow("LOCAL:2"); + const tally = { + amountAcc: Amounts.zeroOfCurrency(instructedAmount.currency), + depositFeesAcc: Amounts.zeroOfCurrency(instructedAmount.currency), + lastDepositFee: Amounts.zeroOfCurrency(instructedAmount.currency), + }; + const coins = testing_greedySelectPeer( + createCandidates([ + { + amount: "LOCAL:10" as AmountString, + numAvailable: 5, + depositFee: "LOCAL:0.1" as AmountString, + fromExchange: "http://exchange.localhost/", + }, + ]), + instructedAmount, + tally, + ); + + t.log(j2s(coins)); + + 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.1")], + expireDeposit: inTheDistantFuture, + expireWithdraw: inTheDistantFuture, + }, + }); + + t.deepEqual(tally, { + amountAcc: Amounts.parseOrThrow("LOCAL:2"), + depositFeesAcc: Amounts.parseOrThrow("LOCAL:0.1"), + lastDepositFee: Amounts.parseOrThrow("LOCAL:0.1"), + }); +}); + +test("p2p: should select 3 coins", (t) => { + const instructedAmount = Amounts.parseOrThrow("LOCAL:20"); + const tally = { + amountAcc: Amounts.zeroOfCurrency(instructedAmount.currency), + depositFeesAcc: Amounts.zeroOfCurrency(instructedAmount.currency), + lastDepositFee: Amounts.zeroOfCurrency(instructedAmount.currency), + }; + const coins = testing_greedySelectPeer( + createCandidates([ + { + amount: "LOCAL:10" as AmountString, + numAvailable: 5, + depositFee: "LOCAL:0.1" as AmountString, + fromExchange: "http://exchange.localhost/", + }, + ]), + instructedAmount, + tally, + ); + + t.deepEqual(coins, { + "hash0;32;http://exchange.localhost/": { + exchangeBaseUrl: "http://exchange.localhost/", + denomPubHash: "hash0", + maxAge: 32, + contributions: [ + Amounts.parseOrThrow("LOCAL:9.9"), + Amounts.parseOrThrow("LOCAL:9.9"), + Amounts.parseOrThrow("LOCAL:0.5"), + ], + expireDeposit: inTheDistantFuture, + expireWithdraw: inTheDistantFuture, + }, + }); + + t.deepEqual(tally, { + amountAcc: Amounts.parseOrThrow("LOCAL:20"), + depositFeesAcc: Amounts.parseOrThrow("LOCAL:0.3"), + lastDepositFee: Amounts.parseOrThrow("LOCAL:0.1"), + }); +}); + +test("p2p: can't select since the instructed amount is too high", (t) => { + const instructedAmount = Amounts.parseOrThrow("LOCAL:60"); + const tally = { + amountAcc: Amounts.zeroOfCurrency(instructedAmount.currency), + depositFeesAcc: Amounts.zeroOfCurrency(instructedAmount.currency), + lastDepositFee: Amounts.zeroOfCurrency(instructedAmount.currency), + }; + const coins = testing_greedySelectPeer( + createCandidates([ + { + amount: "LOCAL:10" as AmountString, + numAvailable: 5, + depositFee: "LOCAL:0.1" as AmountString, + fromExchange: "http://exchange.localhost/", + }, + ]), + instructedAmount, + tally, + ); + + t.is(coins, undefined); + + t.deepEqual(tally, { + amountAcc: Amounts.parseOrThrow("LOCAL:49"), + depositFeesAcc: Amounts.parseOrThrow("LOCAL:0.5"), + lastDepositFee: Amounts.parseOrThrow("LOCAL:0.1"), + }); +}); + +test("pay: select one coin to pay with fee", (t) => { + const payment = Amounts.parseOrThrow("LOCAL:2"); + const exchangeWireFee = Amounts.parseOrThrow("LOCAL:0.1"); + const zero = Amounts.zeroOfCurrency(payment.currency); + const tally = { + amountPayRemaining: payment, + amountWireFeeLimitRemaining: zero, + amountDepositFeeLimitRemaining: zero, + customerDepositFees: zero, + customerWireFees: zero, + wireFeeCoveredForExchange: new Set(), + lastDepositFee: zero, + }; + const coins = testing_selectGreedy( + { + auditors: [], + exchanges: [ + { + exchangeBaseUrl: "http://exchange.localhost/", + exchangePub: "E5M8CGRDHXF1RCVP3B8TQCTDYNQ7T4XHWR5SVEQRGVVMVME41VJ0", + }, + ], + contractTermsAmount: payment, + depositFeeLimit: zero, + wireFeeAmortization: 1, + wireFeeLimit: zero, + prevPayCoins: [], + wireMethod: "x-taler-bank", + }, + createCandidates([ + { + amount: "LOCAL:10" as AmountString, + numAvailable: 5, + depositFee: "LOCAL:0.1" as AmountString, + fromExchange: "http://exchange.localhost/", + }, + ]), + { "http://exchange.localhost/": exchangeWireFee }, + tally, + ); + + t.deepEqual(coins, { + "hash0;32;http://exchange.localhost/": { + exchangeBaseUrl: "http://exchange.localhost/", + denomPubHash: "hash0", + maxAge: 32, + contributions: [Amounts.parseOrThrow("LOCAL:2.2")], + expireDeposit: inTheDistantFuture, + expireWithdraw: inTheDistantFuture, + }, + }); + + t.deepEqual(tally, { + amountPayRemaining: Amounts.parseOrThrow("LOCAL:2"), + amountWireFeeLimitRemaining: zero, + amountDepositFeeLimitRemaining: zero, + customerDepositFees: zero, + customerWireFees: zero, + wireFeeCoveredForExchange: new Set(), + lastDepositFee: zero, + }); +}); + +function createCandidates( + ar: { + amount: AmountString; + depositFee: AmountString; + numAvailable: number; + fromExchange: string; + }[], +): AvailableDenom[] { + return ar.map((r, idx) => { + return { + denomPub: { + age_mask: 0, + cipher: DenomKeyType.Rsa, + rsa_public_key: "PPP", + }, + denomPubHash: `hash${idx}`, + value: r.amount, + feeDeposit: r.depositFee, + feeRefresh: "LOCAL:0" as AmountString, + feeRefund: "LOCAL:0" as AmountString, + feeWithdraw: "LOCAL:0" as AmountString, + stampExpireDeposit: inTheDistantFuture, + stampExpireLegal: inTheDistantFuture, + stampExpireWithdraw: inTheDistantFuture, + stampStart: inThePast, + exchangeBaseUrl: r.fromExchange, + numAvailable: r.numAvailable, + maxAge: 32, + }; + }); +} -- cgit v1.2.3