/* 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, }; }); }