/*
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,
Amounts,
Duration,
TransactionAmountMode,
} from "@gnu-taler/taler-util";
import test, { ExecutionContext } from "ava";
import {
CoinInfo,
convertDepositAmountForAvailableCoins,
convertWithdrawalAmountFromAvailableCoins,
getMaxDepositAmountForAvailableCoins,
} from "./instructedAmountConversion.js";
function makeCurrencyHelper(currency: string) {
return (sx: TemplateStringsArray, ...vx: any[]) => {
const s = String.raw({ raw: sx }, ...vx);
return Amounts.parseOrThrow(`${currency}:${s}`);
};
}
const kudos = makeCurrencyHelper("kudos");
function defaultFeeConfig(value: AmountJson, totalAvailable: number): CoinInfo {
return {
id: Amounts.stringify(value),
denomDeposit: kudos`0.01`,
denomRefresh: kudos`0.01`,
denomWithdraw: kudos`0.01`,
exchangeBaseUrl: "1",
duration: Duration.getForever(),
exchangePurse: undefined,
exchangeWire: undefined,
maxAge: AgeRestriction.AGE_UNRESTRICTED,
totalAvailable,
value,
};
}
type Coin = [AmountJson, number];
/**
* Making a deposit with effective amount
*
*/
test("deposit effective 2", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`2`,
TransactionAmountMode.Effective,
);
t.is(Amounts.stringifyValue(result.effective), "2");
t.is(Amounts.stringifyValue(result.raw), "1.99");
});
test("deposit effective 10", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`10`,
TransactionAmountMode.Effective,
);
t.is(Amounts.stringifyValue(result.effective), "10");
t.is(Amounts.stringifyValue(result.raw), "9.98");
});
test("deposit effective 24", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`24`,
TransactionAmountMode.Effective,
);
t.is(Amounts.stringifyValue(result.effective), "24");
t.is(Amounts.stringifyValue(result.raw), "23.94");
});
test("deposit effective 40", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`40`,
TransactionAmountMode.Effective,
);
t.is(Amounts.stringifyValue(result.effective), "35");
t.is(Amounts.stringifyValue(result.raw), "34.9");
});
test("deposit with wire fee effective 2", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {
one: {
wireFee: kudos`0.1`,
purseFee: kudos`0.00`,
creditDeadline: AbsoluteTime.never(),
debitDeadline: AbsoluteTime.never(),
},
},
},
kudos`2`,
TransactionAmountMode.Effective,
);
t.is(Amounts.stringifyValue(result.effective), "2");
t.is(Amounts.stringifyValue(result.raw), "1.89");
});
/**
* Making a deposit with raw amount, using the result from effective
*
*/
test("deposit raw 1.99 (effective 2)", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`1.99`,
TransactionAmountMode.Raw,
);
t.is(Amounts.stringifyValue(result.effective), "2");
t.is(Amounts.stringifyValue(result.raw), "1.99");
});
test("deposit raw 9.98 (effective 10)", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`9.98`,
TransactionAmountMode.Raw,
);
t.is(Amounts.stringifyValue(result.effective), "10");
t.is(Amounts.stringifyValue(result.raw), "9.98");
});
test("deposit raw 23.94 (effective 24)", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`23.94`,
TransactionAmountMode.Raw,
);
t.is(Amounts.stringifyValue(result.effective), "24");
t.is(Amounts.stringifyValue(result.raw), "23.94");
});
test("deposit raw 34.9 (effective 40)", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`34.9`,
TransactionAmountMode.Raw,
);
t.is(Amounts.stringifyValue(result.effective), "35");
t.is(Amounts.stringifyValue(result.raw), "34.9");
});
test("deposit with wire fee raw 2", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {
one: {
wireFee: kudos`0.1`,
purseFee: kudos`0.00`,
creditDeadline: AbsoluteTime.never(),
debitDeadline: AbsoluteTime.never(),
},
},
},
kudos`2`,
TransactionAmountMode.Effective,
);
t.is(Amounts.stringifyValue(result.effective), "2");
t.is(Amounts.stringifyValue(result.raw), "1.89");
});
/**
* Calculating the max amount possible to deposit
*
*/
test("deposit max 35", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = getMaxDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {
"2": {
wireFee: kudos`0.00`,
purseFee: kudos`0.00`,
creditDeadline: AbsoluteTime.never(),
debitDeadline: AbsoluteTime.never(),
},
},
},
"KUDOS",
);
t.is(Amounts.stringifyValue(result.raw), "34.9");
t.is(Amounts.stringifyValue(result.effective), "35");
});
test("deposit max 35 with wirefee", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = getMaxDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {
"2": {
wireFee: kudos`1`,
purseFee: kudos`0.00`,
creditDeadline: AbsoluteTime.never(),
debitDeadline: AbsoluteTime.never(),
},
},
},
"KUDOS",
);
t.is(Amounts.stringifyValue(result.raw), "33.9");
t.is(Amounts.stringifyValue(result.effective), "35");
});
test("deposit max repeated denom", (t) => {
const coinList: Coin[] = [
[kudos`2`, 1],
[kudos`2`, 1],
[kudos`5`, 1],
];
const result = getMaxDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {
"2": {
wireFee: kudos`0.00`,
purseFee: kudos`0.00`,
creditDeadline: AbsoluteTime.never(),
debitDeadline: AbsoluteTime.never(),
},
},
},
"KUDOS",
);
t.is(Amounts.stringifyValue(result.raw), "8.97");
t.is(Amounts.stringifyValue(result.effective), "9");
});
/**
* Making a withdrawal with effective amount
*
*/
test("withdraw effective 2", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertWithdrawalAmountFromAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`2`,
TransactionAmountMode.Effective,
);
t.is(Amounts.stringifyValue(result.effective), "2");
t.is(Amounts.stringifyValue(result.raw), "2.01");
});
test("withdraw effective 10", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertWithdrawalAmountFromAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`10`,
TransactionAmountMode.Effective,
);
t.is(Amounts.stringifyValue(result.effective), "10");
t.is(Amounts.stringifyValue(result.raw), "10.02");
});
test("withdraw effective 24", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertWithdrawalAmountFromAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`24`,
TransactionAmountMode.Effective,
);
t.is(Amounts.stringifyValue(result.effective), "24");
t.is(Amounts.stringifyValue(result.raw), "24.06");
});
test("withdraw effective 40", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertWithdrawalAmountFromAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`40`,
TransactionAmountMode.Effective,
);
t.is(Amounts.stringifyValue(result.effective), "40");
t.is(Amounts.stringifyValue(result.raw), "40.08");
});
/**
* Making a deposit with raw amount, using the result from effective
*
*/
test("withdraw raw 2.01 (effective 2)", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertWithdrawalAmountFromAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`2.01`,
TransactionAmountMode.Raw,
);
t.is(Amounts.stringifyValue(result.effective), "2");
t.is(Amounts.stringifyValue(result.raw), "2.01");
});
test("withdraw raw 10.02 (effective 10)", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertWithdrawalAmountFromAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`10.02`,
TransactionAmountMode.Raw,
);
t.is(Amounts.stringifyValue(result.effective), "10");
t.is(Amounts.stringifyValue(result.raw), "10.02");
});
test("withdraw raw 24.06 (effective 24)", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertWithdrawalAmountFromAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`24.06`,
TransactionAmountMode.Raw,
);
t.is(Amounts.stringifyValue(result.effective), "24");
t.is(Amounts.stringifyValue(result.raw), "24.06");
});
test("withdraw raw 40.08 (effective 40)", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertWithdrawalAmountFromAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`40.08`,
TransactionAmountMode.Raw,
);
t.is(Amounts.stringifyValue(result.effective), "40");
t.is(Amounts.stringifyValue(result.raw), "40.08");
});
test("withdraw raw 25", (t) => {
const coinList: Coin[] = [
[kudos`0.1`, 0],
[kudos`1`, 0],
[kudos`2`, 0],
[kudos`5`, 0],
];
const result = convertWithdrawalAmountFromAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`25`,
TransactionAmountMode.Raw,
);
t.is(Amounts.stringifyValue(result.effective), "24.8");
t.is(Amounts.stringifyValue(result.raw), "24.94");
});
test("withdraw effective 24.8 (raw 25)", (t) => {
const coinList: Coin[] = [
[kudos`0.1`, 0],
[kudos`1`, 0],
[kudos`2`, 0],
[kudos`5`, 0],
];
const result = convertWithdrawalAmountFromAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`24.8`,
TransactionAmountMode.Effective,
);
t.is(Amounts.stringifyValue(result.effective), "24.8");
t.is(Amounts.stringifyValue(result.raw), "24.94");
});
/**
* Making a deposit with refresh
*
*/
test("deposit with refresh: effective 3", (t) => {
const coinList: Coin[] = [
[kudos`0.1`, 0],
[kudos`1`, 0],
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`3`,
TransactionAmountMode.Effective,
);
t.is(Amounts.stringifyValue(result.effective), "3.1");
t.is(Amounts.stringifyValue(result.raw), "2.98");
expectDefined(t, result.refresh);
//FEES
//deposit 2 x 0.01
//refresh 1 x 0.01
//withdraw 9 x 0.01
//-----------------
//op 0.12
//coins sent 2 x 2.0
//coins recv 9 x 0.1
//-------------------
//effective 3.10
//raw 2.98
t.is(Amounts.stringifyValue(result.refresh.selected.id), "2");
t.deepEqual(asCoinList(result.refresh.coins), [[kudos`0.1`, 9]]);
});
test("deposit with refresh: raw 2.98 (effective 3)", (t) => {
const coinList: Coin[] = [
[kudos`0.1`, 0],
[kudos`1`, 0],
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`2.98`,
TransactionAmountMode.Raw,
);
t.is(Amounts.stringifyValue(result.effective), "3.2");
t.is(Amounts.stringifyValue(result.raw), "3.09");
expectDefined(t, result.refresh);
//FEES
//deposit 1 x 0.01
//refresh 1 x 0.01
//withdraw 8 x 0.01
//-----------------
//op 0.10
//coins sent 1 x 2.0
//coins recv 8 x 0.1
//-------------------
//effective 3.20
//raw 3.09
t.is(Amounts.stringifyValue(result.refresh.selected.id), "2");
t.deepEqual(asCoinList(result.refresh.coins), [[kudos`0.1`, 8]]);
});
test("deposit with refresh: effective 3.2 (raw 2.98)", (t) => {
const coinList: Coin[] = [
[kudos`0.1`, 0],
[kudos`1`, 0],
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`3.2`,
TransactionAmountMode.Effective,
);
t.is(Amounts.stringifyValue(result.effective), "3.3");
t.is(Amounts.stringifyValue(result.raw), "3.2");
expectDefined(t, result.refresh);
//FEES
//deposit 2 x 0.01
//refresh 1 x 0.01
//withdraw 7 x 0.01
//-----------------
//op 0.10
//coins sent 2 x 2.0
//coins recv 7 x 0.1
//-------------------
//effective 3.30
//raw 3.20
t.is(Amounts.stringifyValue(result.refresh.selected.id), "2");
t.deepEqual(asCoinList(result.refresh.coins), [[kudos`0.1`, 7]]);
});
function expectDefined(
t: ExecutionContext,
v: T | undefined,
): asserts v is T {
t.assert(v !== undefined);
}
function asCoinList(v: { info: CoinInfo; size: number }[]): any {
return v.map((c) => {
return [c.info.value, c.size];
});
}
/**
* regression tests
*/
test("demo: withdraw raw 25", (t) => {
const coinList: Coin[] = [
[kudos`0.1`, 0],
[kudos`1`, 0],
[kudos`2`, 0],
[kudos`5`, 0],
[kudos`10`, 0],
];
const result = convertWithdrawalAmountFromAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`25`,
TransactionAmountMode.Raw,
);
t.is(Amounts.stringifyValue(result.effective), "24.8");
t.is(Amounts.stringifyValue(result.raw), "24.92");
// coins received
// 8 x 0.1
// 2 x 0.2
// 2 x 10.0
// total effective 24.8
// fee 12 x 0.01 = 0.12
// total raw 24.92
// left in reserve 25 - 24.92 == 0.08
//current wallet impl: hides the left in reserve fee
//shows fee = 0.2
});
test("demo: deposit max after withdraw raw 25", (t) => {
const coinList: Coin[] = [
[kudos`0.1`, 8],
[kudos`1`, 0],
[kudos`2`, 2],
[kudos`5`, 0],
[kudos`10`, 2],
];
const result = getMaxDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {
one: {
wireFee: kudos`0.01`,
purseFee: kudos`0.00`,
creditDeadline: AbsoluteTime.never(),
debitDeadline: AbsoluteTime.never(),
},
},
},
"KUDOS",
);
t.is(Amounts.stringifyValue(result.effective), "24.8");
t.is(Amounts.stringifyValue(result.raw), "24.67");
// 8 x 0.1
// 2 x 0.2
// 2 x 10.0
// total effective 24.8
// deposit fee 12 x 0.01 = 0.12
// wire fee 0.01
// total raw: 24.8 - 0.13 = 24.67
// current wallet impl fee 0.14
});
test("demo: withdraw raw 13", (t) => {
const coinList: Coin[] = [
[kudos`0.1`, 0],
[kudos`1`, 0],
[kudos`2`, 0],
[kudos`5`, 0],
[kudos`10`, 0],
];
const result = convertWithdrawalAmountFromAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`13`,
TransactionAmountMode.Raw,
);
t.is(Amounts.stringifyValue(result.effective), "12.8");
t.is(Amounts.stringifyValue(result.raw), "12.9");
// coins received
// 8 x 0.1
// 1 x 0.2
// 1 x 10.0
// total effective 12.8
// fee 10 x 0.01 = 0.10
// total raw 12.9
// left in reserve 13 - 12.9 == 0.1
//current wallet impl: hides the left in reserve fee
//shows fee = 0.2
});
test("demo: deposit max after withdraw raw 13", (t) => {
const coinList: Coin[] = [
[kudos`0.1`, 8],
[kudos`1`, 0],
[kudos`2`, 1],
[kudos`5`, 0],
[kudos`10`, 1],
];
const result = getMaxDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {
one: {
wireFee: kudos`0.01`,
purseFee: kudos`0.00`,
creditDeadline: AbsoluteTime.never(),
debitDeadline: AbsoluteTime.never(),
},
},
},
"KUDOS",
);
t.is(Amounts.stringifyValue(result.effective), "12.8");
t.is(Amounts.stringifyValue(result.raw), "12.69");
// 8 x 0.1
// 1 x 0.2
// 1 x 10.0
// total effective 12.8
// deposit fee 10 x 0.01 = 0.10
// wire fee 0.01
// total raw: 12.8 - 0.11 = 12.69
// current wallet impl fee 0.14
});