256 lines
7.1 KiB
TypeScript
256 lines
7.1 KiB
TypeScript
/*
|
|
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 <http://www.gnu.org/licenses/>
|
|
*/
|
|
import {
|
|
AbsoluteTime,
|
|
AmountString,
|
|
Amounts,
|
|
DenomKeyType,
|
|
Duration,
|
|
j2s,
|
|
} from "@gnu-taler/taler-util";
|
|
import test, { ExecutionContext } 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",
|
|
numAvailable: 5,
|
|
depositFee: "LOCAL:0.1",
|
|
fromExchange: "http://exchange.localhost/",
|
|
},
|
|
]),
|
|
instructedAmount,
|
|
tally,
|
|
);
|
|
|
|
t.log(j2s(coins));
|
|
|
|
expect(t, coins).deep.equal({
|
|
"hash0;32;http://exchange.localhost/": {
|
|
exchangeBaseUrl: "http://exchange.localhost/",
|
|
denomPubHash: "hash0",
|
|
maxAge: 32,
|
|
contributions: [Amounts.parseOrThrow("LOCAL:2.1")],
|
|
},
|
|
});
|
|
|
|
expect(t, tally).deep.equal({
|
|
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",
|
|
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.5"),
|
|
],
|
|
},
|
|
});
|
|
|
|
expect(t, tally).deep.equal({
|
|
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",
|
|
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"),
|
|
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<string>(),
|
|
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",
|
|
numAvailable: 5,
|
|
depositFee: "LOCAL:0.1",
|
|
fromExchange: "http://exchange.localhost/",
|
|
},
|
|
]),
|
|
{ "http://exchange.localhost/": exchangeWireFee },
|
|
tally,
|
|
);
|
|
|
|
expect(t, coins).deep.equal({
|
|
"hash0;32;http://exchange.localhost/": {
|
|
exchangeBaseUrl: "http://exchange.localhost/",
|
|
denomPubHash: "hash0",
|
|
maxAge: 32,
|
|
contributions: [Amounts.parseOrThrow("LOCAL:2.2")],
|
|
},
|
|
});
|
|
|
|
expect(t, tally).deep.equal({
|
|
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",
|
|
feeRefund: "LOCAL:0",
|
|
feeWithdraw: "LOCAL:0",
|
|
stampExpireDeposit: inTheDistantFuture,
|
|
stampExpireLegal: inTheDistantFuture,
|
|
stampExpireWithdraw: inTheDistantFuture,
|
|
stampStart: inThePast,
|
|
exchangeBaseUrl: r.fromExchange,
|
|
numAvailable: r.numAvailable,
|
|
maxAge: 32,
|
|
};
|
|
});
|
|
}
|
|
|
|
type Tester<T> = {
|
|
deep: {
|
|
equal(another: T): ReturnType<ExecutionContext["deepEqual"]>;
|
|
equals(another: T): ReturnType<ExecutionContext["deepEqual"]>;
|
|
};
|
|
};
|
|
|
|
function expect<T>(t: ExecutionContext, thing: T): Tester<T> {
|
|
return {
|
|
deep: {
|
|
equal: (another: T) => t.deepEqual(thing, another),
|
|
equals: (another: T) => t.deepEqual(thing, another),
|
|
},
|
|
};
|
|
}
|