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