diff options
Diffstat (limited to 'packages/taler-wallet-core/src/util/coinSelection.test.ts')
-rw-r--r-- | packages/taler-wallet-core/src/util/coinSelection.test.ts | 614 |
1 files changed, 471 insertions, 143 deletions
diff --git a/packages/taler-wallet-core/src/util/coinSelection.test.ts b/packages/taler-wallet-core/src/util/coinSelection.test.ts index ab3b2c4f8..3073b69c7 100644 --- a/packages/taler-wallet-core/src/util/coinSelection.test.ts +++ b/packages/taler-wallet-core/src/util/coinSelection.test.ts @@ -13,13 +13,6 @@ 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 test, { ExecutionContext } from "ava"; -import { - AmountMode, - OperationType, - calculatePlanFormAvailableCoins, - selectCoinForOperation, -} from "./coinSelection.js"; import { AbsoluteTime, AgeRestriction, @@ -27,28 +20,31 @@ import { Amounts, Duration, TransactionAmountMode, - TransactionType, } from "@gnu-taler/taler-util"; +import test, { ExecutionContext } from "ava"; +import { + CoinInfo, + convertDepositAmountForAvailableCoins, + convertWithdrawalAmountFromAvailableCoins, + getMaxDepositAmountForAvailableCoins, +} from "./coinSelection.js"; -function expect(t: ExecutionContext, thing: any): any { - return { - deep: { - equal: (another: any) => t.deepEqual(thing, another), - equals: (another: any) => t.deepEqual(thing, another), - }, +function makeCurrencyHelper(currency: string) { + return (sx: TemplateStringsArray, ...vx: any[]) => { + const s = String.raw({ raw: sx }, ...vx); + return Amounts.parseOrThrow(`${currency}:${s}`); }; } -function kudos(v: number): AmountJson { - return Amounts.fromFloat(v, "KUDOS"); -} +const kudos = makeCurrencyHelper("kudos"); -function defaultFeeConfig(value: AmountJson, totalAvailable: number) { +function defaultFeeConfig(value: AmountJson, totalAvailable: number): CoinInfo { return { id: Amounts.stringify(value), - denomDeposit: kudos(0.01), - denomRefresh: kudos(0.01), - denomWithdraw: kudos(0.01), + denomDeposit: kudos`0.01`, + denomRefresh: kudos`0.01`, + denomWithdraw: kudos`0.01`, + exchangeBaseUrl: "1", duration: Duration.getForever(), exchangePurse: undefined, exchangeWire: undefined, @@ -60,242 +56,574 @@ function defaultFeeConfig(value: AmountJson, totalAvailable: number) { type Coin = [AmountJson, number]; /** - * selectCoinForOperation test + * Making a deposit with effective amount * - * Test here should check that the correct coins are selected */ -test("get effective 2", (t) => { +test("deposit effective 2", (t) => { const coinList: Coin[] = [ - [kudos(2), 5], - [kudos(5), 5], + [kudos`2`, 5], + [kudos`5`, 5], ]; - const result = selectCoinForOperation( - OperationType.Credit, - kudos(2), - AmountMode.Net, + const result = convertDepositAmountForAvailableCoins( { list: coinList.map(([v, t]) => defaultFeeConfig(v, t)), exchanges: {}, }, + kudos`2`, + TransactionAmountMode.Effective, ); - expect(t, result.coins).deep.equal(["KUDOS:2"]); - t.assert(result.refresh === undefined); + t.is(Amounts.stringifyValue(result.effective), "2"); + t.is(Amounts.stringifyValue(result.raw), "1.99"); }); -test("get raw 4", (t) => { +test("deposit effective 10", (t) => { const coinList: Coin[] = [ - [kudos(2), 5], - [kudos(5), 5], + [kudos`2`, 5], + [kudos`5`, 5], ]; - const result = selectCoinForOperation( - OperationType.Credit, - kudos(4), - AmountMode.Gross, + 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"); +}); - expect(t, result.coins).deep.equal(["KUDOS:2", "KUDOS:2"]); - t.assert(result.refresh === undefined); +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("get raw 25, diff with demo ", (t) => { +test("deposit effective 40", (t) => { const coinList: Coin[] = [ - [kudos(0.1), 0], - [kudos(1), 0], - [kudos(2), 0], - [kudos(5), 0], - [kudos(10), 0], + [kudos`2`, 5], + [kudos`5`, 5], ]; - const result = selectCoinForOperation( - OperationType.Credit, - kudos(25), - AmountMode.Gross, + 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"); +}); - expect(t, result.coins).deep.equal(["KUDOS:10", "KUDOS:10", "KUDOS:5"]); - t.assert(result.refresh === undefined); +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"); }); -test("send effective 6", (t) => { +/** + * 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], + [kudos`2`, 5], + [kudos`5`, 5], ]; - const result = selectCoinForOperation( - OperationType.Debit, - kudos(6), - AmountMode.Gross, + const result = convertDepositAmountForAvailableCoins( { list: coinList.map(([v, t]) => defaultFeeConfig(v, t)), exchanges: {}, }, + kudos`1.99`, + TransactionAmountMode.Raw, ); - - expect(t, result.coins).deep.equal(["KUDOS:5"]); - t.assert(result.refresh?.selected === "KUDOS:2"); + t.is(Amounts.stringifyValue(result.effective), "2"); + t.is(Amounts.stringifyValue(result.raw), "1.99"); }); -test("send raw 6", (t) => { +test("deposit raw 9.98 (effective 10)", (t) => { const coinList: Coin[] = [ - [kudos(2), 5], - [kudos(5), 5], + [kudos`2`, 5], + [kudos`5`, 5], ]; - const result = selectCoinForOperation( - OperationType.Debit, - kudos(6), - AmountMode.Gross, + 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"); +}); - expect(t, result.coins).deep.equal(["KUDOS:5"]); - t.assert(result.refresh?.selected === "KUDOS:2"); +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("send raw 20 (not enough)", (t) => { +test("deposit raw 34.9 (effective 40)", (t) => { const coinList: Coin[] = [ - [kudos(2), 1], - [kudos(5), 2], + [kudos`2`, 5], + [kudos`5`, 5], ]; - const result = selectCoinForOperation( - OperationType.Debit, - kudos(20), - AmountMode.Gross, + 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"); +}); - expect(t, result.coins).deep.equal(["KUDOS:5", "KUDOS:5", "KUDOS:2"]); - t.assert(result.refresh === undefined); +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"); }); /** - * calculatePlanFormAvailableCoins test + * Calculating the max amount possible to deposit * - * Test here should check that the plan summary for a transaction is correct - * * effective amount - * * raw amount */ -test("deposit effective 2 ", (t) => { +test("deposit max 35", (t) => { const coinList: Coin[] = [ - [kudos(2), 1], - [kudos(5), 2], + [kudos`2`, 5], + [kudos`5`, 5], ]; - const result = calculatePlanFormAvailableCoins( - TransactionType.Deposit, - kudos(2), - TransactionAmountMode.Effective, + 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(), - wireFee: kudos(0.01), - purseFee: kudos(0.01), }, }, }, + "KUDOS", ); - - t.deepEqual(result.rawAmount, "KUDOS:1.98"); - t.deepEqual(result.effectiveAmount, "KUDOS:2"); + t.is(Amounts.stringifyValue(result.raw), "34.9"); + t.is(Amounts.stringifyValue(result.effective), "35"); }); -test("deposit raw 2 ", (t) => { +test("deposit max 35 with wirefee", (t) => { const coinList: Coin[] = [ - [kudos(2), 1], - [kudos(5), 2], + [kudos`2`, 5], + [kudos`5`, 5], ]; - const result = calculatePlanFormAvailableCoins( - TransactionType.Deposit, - kudos(2), - TransactionAmountMode.Raw, + 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(), - wireFee: kudos(0.01), - purseFee: kudos(0.01), }, }, }, + "KUDOS", ); - - t.deepEqual(result.rawAmount, "KUDOS:2"); - t.deepEqual(result.effectiveAmount, "KUDOS:2.04"); + t.is(Amounts.stringifyValue(result.raw), "33.9"); + t.is(Amounts.stringifyValue(result.effective), "35"); }); -test("withdraw raw 21 ", (t) => { +test("deposit max repeated denom", (t) => { const coinList: Coin[] = [ - [kudos(2), 1], - [kudos(5), 2], + [kudos`2`, 1], + [kudos`2`, 1], + [kudos`5`, 1], ]; - const result = calculatePlanFormAvailableCoins( - TransactionType.Withdrawal, - kudos(21), - TransactionAmountMode.Raw, + 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(), - wireFee: kudos(0.01), - purseFee: kudos(0.01), }, }, }, + "KUDOS", ); + t.is(Amounts.stringifyValue(result.raw), "8.97"); + t.is(Amounts.stringifyValue(result.effective), "9"); +}); - // denominations configuration is not suitable - // for greedy algorithm - t.deepEqual(result.rawAmount, "KUDOS:20"); - t.deepEqual(result.effectiveAmount, "KUDOS:19.96"); +/** + * 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 raw 25, diff with demo ", (t) => { +test("withdraw effective 40", (t) => { const coinList: Coin[] = [ - [kudos(0.1), 0], - [kudos(1), 0], - [kudos(2), 0], - [kudos(5), 0], - [kudos(10), 0], + [kudos`2`, 5], + [kudos`5`, 5], ]; - const result = calculatePlanFormAvailableCoins( - TransactionType.Withdrawal, - kudos(25), + 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: { - "2": { - creditDeadline: AbsoluteTime.never(), - debitDeadline: AbsoluteTime.never(), - wireFee: kudos(0.01), - purseFee: kudos(0.01), - }, - }, + 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 - t.deepEqual(result.rawAmount, "KUDOS:25"); - // here demo report KUDOS:0.2 fee - // t.deepEqual(result.effectiveAmount, "KUDOS:24.80"); - t.deepEqual(result.effectiveAmount, "KUDOS:24.97"); + //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>( + 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]; + }); +} |