/* 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, AgeRestriction, AmountJson, AmountString, Amounts, DenomKeyType, Duration, TransactionAmountMode, } from "@gnu-taler/taler-util"; import test, { ExecutionContext } from "ava"; import { AvailableDenom, testing_greedySelectPeer } from "./coinSelection.js" type Tester = { deep: { equal(another: T): ReturnType; equals(another: T): ReturnType; } } function expect(t: ExecutionContext, thing: T): Tester { return { deep: { equal: (another: T) => t.deepEqual(thing, another), equals: (another: T) => t.deepEqual(thing, another), }, }; } 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("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", numAvailable: 5, depositFee: "LOCAL:0.1", fromExchange: "http://exchange.localhost/", }]), instructedAmount, tally ); expect(t, coins).deep.equal({ "hash0;32;http://exchange.localhost/": { exchangeBaseUrl: "http://exchange.localhost/", denomPubHash: "hash0", maxAge: 32, contributions: [Amounts.parseOrThrow("LOCAL:2")], } }); expect(t, tally).deep.equal({ amountAcc: Amounts.parseOrThrow("LOCAL:2"), depositFeesAcc: Amounts.parseOrThrow("LOCAL:0.1"), lastDepositFee: Amounts.parseOrThrow("LOCAL:0.1"), }); }); test("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", numAvailable: 5, depositFee: "LOCAL:0.1", fromExchange: "http://exchange.localhost/", }]), instructedAmount, tally ); expect(t, coins).deep.equal({ "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.2") ], } }); expect(t, tally).deep.equal({ amountAcc: Amounts.parseOrThrow("LOCAL:20"), depositFeesAcc: Amounts.parseOrThrow("LOCAL:0.3"), lastDepositFee: Amounts.parseOrThrow("LOCAL:0.1"), }); }); test("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", numAvailable: 5, depositFee: "LOCAL:0.1", fromExchange: "http://exchange.localhost/", }]), instructedAmount, tally ); expect(t, coins).deep.equal(undefined); expect(t, tally).deep.equal({ amountAcc: Amounts.parseOrThrow("LOCAL:49.5"), depositFeesAcc: Amounts.parseOrThrow("LOCAL:0.5"), lastDepositFee: Amounts.parseOrThrow("LOCAL:0.1"), }); }); 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", "feeRefund": "LOCAL:0", "feeWithdraw": "LOCAL:0", "stampExpireDeposit": inTheDistantFuture, "stampExpireLegal": inTheDistantFuture, "stampExpireWithdraw": inTheDistantFuture, "stampStart": inThePast, "exchangeBaseUrl": r.fromExchange, "numAvailable": r.numAvailable, "maxAge": 32, } }) }