From 612b85c18fc17af412d08e075e1fddaa67aa7bf0 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 21 Feb 2024 13:01:23 +0100 Subject: move helpers to util --- .../taler-wallet-core/src/denominations.test.ts | 870 +++++++++++++++++++++ 1 file changed, 870 insertions(+) create mode 100644 packages/taler-wallet-core/src/denominations.test.ts (limited to 'packages/taler-wallet-core/src/denominations.test.ts') diff --git a/packages/taler-wallet-core/src/denominations.test.ts b/packages/taler-wallet-core/src/denominations.test.ts new file mode 100644 index 000000000..98af5d1a4 --- /dev/null +++ b/packages/taler-wallet-core/src/denominations.test.ts @@ -0,0 +1,870 @@ +/* + 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 + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { + AbsoluteTime, + FeeDescription, + FeeDescriptionPair, + Amounts, + DenominationInfo, + AmountString, +} from "@gnu-taler/taler-util"; +// import { expect } from "chai"; +import { + createPairTimeline, + createTimeline, + selectBestForOverlappingDenominations, +} from "./denominations.js"; +import test, { ExecutionContext } from "ava"; + +/** + * Create some constants to be used as reference in the tests + */ +const VALUES: AmountString[] = Array.from({ length: 10 }).map( + (undef, t) => `USD:${t}` as AmountString, +); +const TIMESTAMPS = Array.from({ length: 20 }).map((undef, t_s) => ({ t_s })); +const ABS_TIME = TIMESTAMPS.map((m) => AbsoluteTime.fromProtocolTimestamp(m)); + +function normalize( + list: DenominationInfo[], +): (DenominationInfo & { group: string })[] { + return list.map((e, idx) => ({ + ...e, + denomPubHash: `id${idx}`, + group: Amounts.stringifyValue(e.value), + })); +} + +//Avoiding to make an error-prone/time-consuming refactor +//this function calls AVA's deepEqual from a chai interface +function expect(t: ExecutionContext, thing: any): any { + return { + deep: { + equal: (another: any) => t.deepEqual(thing, another), + equals: (another: any) => t.deepEqual(thing, another), + }, + }; +} + +// describe("Denomination timeline creation", (t) => { +// describe("single value example", (t) => { + +test("should have one row with start and exp", (t) => { + const timeline = createTimeline( + normalize([ + { + value: VALUES[1], + stampStart: TIMESTAMPS[1], + stampExpireDeposit: TIMESTAMPS[2], + feeDeposit: VALUES[1], + } as Partial as DenominationInfo, + ]), + "denomPubHash", + "stampStart", + "stampExpireDeposit", + "feeDeposit", + "group", + selectBestForOverlappingDenominations, + ); + + expect(t, timeline).deep.equal([ + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[1], + until: ABS_TIME[2], + fee: VALUES[1], + } as FeeDescription, + ]); +}); + +test("should have two rows with the second denom in the middle if second is better", (t) => { + const timeline = createTimeline( + normalize([ + { + value: VALUES[1], + stampStart: TIMESTAMPS[1], + stampExpireDeposit: TIMESTAMPS[3], + feeDeposit: VALUES[1], + } as Partial as DenominationInfo, + { + value: VALUES[1], + stampStart: TIMESTAMPS[2], + stampExpireDeposit: TIMESTAMPS[4], + feeDeposit: VALUES[2], + } as Partial as DenominationInfo, + ]), + "denomPubHash", + "stampStart", + "stampExpireDeposit", + "feeDeposit", + "group", + selectBestForOverlappingDenominations, + ); + + expect(t, timeline).deep.equal([ + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[1], + until: ABS_TIME[3], + fee: VALUES[1], + }, + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[3], + until: ABS_TIME[4], + fee: VALUES[2], + }, + ] as FeeDescription[]); +}); + +test("should have two rows with the first denom in the middle if second is worse", (t) => { + const timeline = createTimeline( + normalize([ + { + value: VALUES[1], + stampStart: TIMESTAMPS[1], + stampExpireDeposit: TIMESTAMPS[3], + feeDeposit: VALUES[2], + } as Partial as DenominationInfo, + { + value: VALUES[1], + stampStart: TIMESTAMPS[2], + stampExpireDeposit: TIMESTAMPS[4], + feeDeposit: VALUES[1], + } as Partial as DenominationInfo, + ]), + "denomPubHash", + "stampStart", + "stampExpireDeposit", + "feeDeposit", + "group", + selectBestForOverlappingDenominations, + ); + + expect(t, timeline).deep.equal([ + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[1], + until: ABS_TIME[2], + fee: VALUES[2], + }, + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[2], + until: ABS_TIME[4], + fee: VALUES[1], + }, + ] as FeeDescription[]); +}); + +test("should add a gap when there no fee", (t) => { + const timeline = createTimeline( + normalize([ + { + value: VALUES[1], + stampStart: TIMESTAMPS[1], + stampExpireDeposit: TIMESTAMPS[2], + feeDeposit: VALUES[2], + } as Partial as DenominationInfo, + { + value: VALUES[1], + stampStart: TIMESTAMPS[3], + stampExpireDeposit: TIMESTAMPS[4], + feeDeposit: VALUES[1], + } as Partial as DenominationInfo, + ]), + "denomPubHash", + "stampStart", + "stampExpireDeposit", + "feeDeposit", + "group", + selectBestForOverlappingDenominations, + ); + + expect(t, timeline).deep.equal([ + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[1], + until: ABS_TIME[2], + fee: VALUES[2], + }, + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[2], + until: ABS_TIME[3], + }, + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[3], + until: ABS_TIME[4], + fee: VALUES[1], + }, + ] as FeeDescription[]); +}); + +test("should have three rows when first denom is between second and second is worse", (t) => { + const timeline = createTimeline( + normalize([ + { + value: VALUES[1], + stampStart: TIMESTAMPS[2], + stampExpireDeposit: TIMESTAMPS[3], + feeDeposit: VALUES[1], + } as Partial as DenominationInfo, + { + value: VALUES[1], + stampStart: TIMESTAMPS[1], + stampExpireDeposit: TIMESTAMPS[4], + feeDeposit: VALUES[2], + } as Partial as DenominationInfo, + ]), + "denomPubHash", + "stampStart", + "stampExpireDeposit", + "feeDeposit", + "group", + selectBestForOverlappingDenominations, + ); + expect(t, timeline).deep.equal([ + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[1], + until: ABS_TIME[2], + fee: VALUES[2], + }, + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[2], + until: ABS_TIME[3], + fee: VALUES[1], + }, + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[3], + until: ABS_TIME[4], + fee: VALUES[2], + }, + ] as FeeDescription[]); +}); + +test("should have one row when first denom is between second and second is better", (t) => { + const timeline = createTimeline( + normalize([ + { + value: VALUES[1], + stampStart: TIMESTAMPS[2], + stampExpireDeposit: TIMESTAMPS[3], + feeDeposit: VALUES[2], + } as Partial as DenominationInfo, + { + value: VALUES[1], + stampStart: TIMESTAMPS[1], + stampExpireDeposit: TIMESTAMPS[4], + feeDeposit: VALUES[1], + } as Partial as DenominationInfo, + ]), + "denomPubHash", + "stampStart", + "stampExpireDeposit", + "feeDeposit", + "group", + selectBestForOverlappingDenominations, + ); + + expect(t, timeline).deep.equal([ + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[1], + until: ABS_TIME[4], + fee: VALUES[1], + }, + ] as FeeDescription[]); +}); + +test("should only add the best1", (t) => { + const timeline = createTimeline( + normalize([ + { + value: VALUES[1], + stampStart: TIMESTAMPS[1], + stampExpireDeposit: TIMESTAMPS[3], + feeDeposit: VALUES[2], + } as Partial as DenominationInfo, + { + value: VALUES[1], + stampStart: TIMESTAMPS[2], + stampExpireDeposit: TIMESTAMPS[4], + feeDeposit: VALUES[1], + } as Partial as DenominationInfo, + { + value: VALUES[1], + stampStart: TIMESTAMPS[2], + stampExpireDeposit: TIMESTAMPS[4], + feeDeposit: VALUES[2], + } as Partial as DenominationInfo, + ]), + "denomPubHash", + "stampStart", + "stampExpireDeposit", + "feeDeposit", + "group", + selectBestForOverlappingDenominations, + ); + + expect(t, timeline).deep.equal([ + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[1], + until: ABS_TIME[2], + fee: VALUES[2], + }, + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[2], + until: ABS_TIME[4], + fee: VALUES[1], + }, + ] as FeeDescription[]); +}); + +test("should only add the best2", (t) => { + const timeline = createTimeline( + normalize([ + { + value: VALUES[1], + stampStart: TIMESTAMPS[1], + stampExpireDeposit: TIMESTAMPS[3], + feeDeposit: VALUES[2], + } as Partial as DenominationInfo, + { + value: VALUES[1], + stampStart: TIMESTAMPS[2], + stampExpireDeposit: TIMESTAMPS[5], + feeDeposit: VALUES[1], + } as Partial as DenominationInfo, + { + value: VALUES[1], + stampStart: TIMESTAMPS[2], + stampExpireDeposit: TIMESTAMPS[4], + feeDeposit: VALUES[2], + } as Partial as DenominationInfo, + { + value: VALUES[1], + stampStart: TIMESTAMPS[5], + stampExpireDeposit: TIMESTAMPS[6], + feeDeposit: VALUES[3], + } as Partial as DenominationInfo, + ]), + "denomPubHash", + "stampStart", + "stampExpireDeposit", + "feeDeposit", + "group", + selectBestForOverlappingDenominations, + ); + + expect(t, timeline).deep.equal([ + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[1], + until: ABS_TIME[2], + fee: VALUES[2], + }, + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[2], + until: ABS_TIME[5], + fee: VALUES[1], + }, + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[5], + until: ABS_TIME[6], + fee: VALUES[3], + }, + ] as FeeDescription[]); +}); + +test("should only add the best3", (t) => { + const timeline = createTimeline( + normalize([ + { + value: VALUES[1], + stampStart: TIMESTAMPS[2], + stampExpireDeposit: TIMESTAMPS[5], + feeDeposit: VALUES[3], + } as Partial as DenominationInfo, + { + value: VALUES[1], + stampStart: TIMESTAMPS[2], + stampExpireDeposit: TIMESTAMPS[5], + feeDeposit: VALUES[1], + } as Partial as DenominationInfo, + { + value: VALUES[1], + stampStart: TIMESTAMPS[2], + stampExpireDeposit: TIMESTAMPS[5], + feeDeposit: VALUES[2], + } as Partial as DenominationInfo, + ]), + "denomPubHash", + "stampStart", + "stampExpireDeposit", + "feeDeposit", + "group", + selectBestForOverlappingDenominations, + ); + + expect(t, timeline).deep.equal([ + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[2], + until: ABS_TIME[5], + fee: VALUES[1], + }, + ] as FeeDescription[]); +}); +// }) + +// describe("multiple value example", (t) => { + +//TODO: test the same start but different value + +test("should not merge when there is different value", (t) => { + const timeline = createTimeline( + normalize([ + { + value: VALUES[1], + stampStart: TIMESTAMPS[1], + stampExpireDeposit: TIMESTAMPS[3], + feeDeposit: VALUES[1], + } as Partial as DenominationInfo, + { + value: VALUES[2], + stampStart: TIMESTAMPS[2], + stampExpireDeposit: TIMESTAMPS[4], + feeDeposit: VALUES[2], + } as Partial as DenominationInfo, + ]), + "denomPubHash", + "stampStart", + "stampExpireDeposit", + "feeDeposit", + "group", + selectBestForOverlappingDenominations, + ); + + expect(t, timeline).deep.equal([ + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[1], + until: ABS_TIME[3], + fee: VALUES[1], + }, + { + group: Amounts.stringifyValue(VALUES[2]), + from: ABS_TIME[2], + until: ABS_TIME[4], + fee: VALUES[2], + }, + ] as FeeDescription[]); +}); + +test("should not merge when there is different value (with duplicates)", (t) => { + const timeline = createTimeline( + normalize([ + { + value: VALUES[1], + stampStart: TIMESTAMPS[1], + stampExpireDeposit: TIMESTAMPS[3], + feeDeposit: VALUES[1], + } as Partial as DenominationInfo, + { + value: VALUES[2], + stampStart: TIMESTAMPS[2], + stampExpireDeposit: TIMESTAMPS[4], + feeDeposit: VALUES[2], + } as Partial as DenominationInfo, + { + value: VALUES[1], + stampStart: TIMESTAMPS[1], + stampExpireDeposit: TIMESTAMPS[3], + feeDeposit: VALUES[1], + } as Partial as DenominationInfo, + { + value: VALUES[2], + stampStart: TIMESTAMPS[2], + stampExpireDeposit: TIMESTAMPS[4], + feeDeposit: VALUES[2], + } as Partial as DenominationInfo, + ]), + "denomPubHash", + "stampStart", + "stampExpireDeposit", + "feeDeposit", + "group", + selectBestForOverlappingDenominations, + ); + + expect(t, timeline).deep.equal([ + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[1], + until: ABS_TIME[3], + fee: VALUES[1], + }, + { + group: Amounts.stringifyValue(VALUES[2]), + from: ABS_TIME[2], + until: ABS_TIME[4], + fee: VALUES[2], + }, + ] as FeeDescription[]); +}); + +// it.skip("real world example: bitcoin exchange", (t) => { +// const timeline = createDenominationTimeline( +// bitcoinExchanges[0].denominations.filter(d => Amounts.cmp(d.value, Amounts.parseOrThrow('BITCOINBTC:0.01048576'))), +// "stampExpireDeposit", "feeDeposit"); + +// expect(t,timeline).deep.equal([{ +// fee: Amounts.parseOrThrow('BITCOINBTC:0.00000001'), +// from: { t_ms: 1652978648000 }, +// until: { t_ms: 1699633748000 }, +// value: Amounts.parseOrThrow('BITCOINBTC:0.01048576'), +// }, { +// fee: Amounts.parseOrThrow('BITCOINBTC:0.00000003'), +// from: { t_ms: 1699633748000 }, +// until: { t_ms: 1707409448000 }, +// value: Amounts.parseOrThrow('BITCOINBTC:0.01048576'), +// }] as FeeDescription[]) +// }) + +// }) + +// }) + +// describe("Denomination timeline pair creation", (t) => { + +// describe("single value example", (t) => { + +test("should return empty", (t) => { + const left = [] as FeeDescription[]; + const right = [] as FeeDescription[]; + + const pairs = createPairTimeline(left, right); + + expect(t, pairs).deep.equals([]); +}); + +test("should return first element", (t) => { + const left = [ + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[1], + until: ABS_TIME[3], + fee: VALUES[1], + }, + ] as FeeDescription[]; + + const right = [] as FeeDescription[]; + + { + const pairs = createPairTimeline(left, right); + expect(t, pairs).deep.equals([ + { + from: ABS_TIME[1], + until: ABS_TIME[3], + group: Amounts.stringifyValue(VALUES[1]), + left: VALUES[1], + right: undefined, + }, + ] as FeeDescriptionPair[]); + } + { + const pairs = createPairTimeline(right, left); + expect(t, pairs).deep.equals([ + { + from: ABS_TIME[1], + until: ABS_TIME[3], + group: Amounts.stringifyValue(VALUES[1]), + right: VALUES[1], + left: undefined, + }, + ] as FeeDescriptionPair[]); + } +}); + +test("should add both to the same row", (t) => { + const left = [ + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[1], + until: ABS_TIME[3], + fee: VALUES[1], + }, + ] as FeeDescription[]; + + const right = [ + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[1], + until: ABS_TIME[3], + fee: VALUES[2], + }, + ] as FeeDescription[]; + + { + const pairs = createPairTimeline(left, right); + expect(t, pairs).deep.equals([ + { + from: ABS_TIME[1], + until: ABS_TIME[3], + group: Amounts.stringifyValue(VALUES[1]), + left: VALUES[1], + right: VALUES[2], + }, + ] as FeeDescriptionPair[]); + } + { + const pairs = createPairTimeline(right, left); + expect(t, pairs).deep.equals([ + { + from: ABS_TIME[1], + until: ABS_TIME[3], + group: Amounts.stringifyValue(VALUES[1]), + left: VALUES[2], + right: VALUES[1], + }, + ] as FeeDescriptionPair[]); + } +}); + +test("should repeat the first and change the second", (t) => { + const left = [ + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[1], + until: ABS_TIME[5], + fee: VALUES[1], + }, + ] as FeeDescription[]; + + const right = [ + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[1], + until: ABS_TIME[2], + fee: VALUES[2], + }, + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[2], + until: ABS_TIME[3], + }, + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[3], + until: ABS_TIME[4], + fee: VALUES[3], + }, + ] as FeeDescription[]; + + { + const pairs = createPairTimeline(left, right); + expect(t, pairs).deep.equals([ + { + from: ABS_TIME[1], + until: ABS_TIME[2], + group: Amounts.stringifyValue(VALUES[1]), + left: VALUES[1], + right: VALUES[2], + }, + { + from: ABS_TIME[2], + until: ABS_TIME[3], + group: Amounts.stringifyValue(VALUES[1]), + left: VALUES[1], + right: undefined, + }, + { + from: ABS_TIME[3], + until: ABS_TIME[4], + group: Amounts.stringifyValue(VALUES[1]), + left: VALUES[1], + right: VALUES[3], + }, + { + from: ABS_TIME[4], + until: ABS_TIME[5], + group: Amounts.stringifyValue(VALUES[1]), + left: VALUES[1], + right: undefined, + }, + ] as FeeDescriptionPair[]); + } +}); + +// }) + +// describe("multiple value example", (t) => { + +test("should separate denominations of different value", (t) => { + const left = [ + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[1], + until: ABS_TIME[3], + fee: VALUES[1], + }, + ] as FeeDescription[]; + + const right = [ + { + group: Amounts.stringifyValue(VALUES[2]), + from: ABS_TIME[1], + until: ABS_TIME[3], + fee: VALUES[2], + }, + ] as FeeDescription[]; + + { + const pairs = createPairTimeline(left, right); + expect(t, pairs).deep.equals([ + { + from: ABS_TIME[1], + until: ABS_TIME[3], + group: Amounts.stringifyValue(VALUES[1]), + left: VALUES[1], + right: undefined, + }, + { + from: ABS_TIME[1], + until: ABS_TIME[3], + group: Amounts.stringifyValue(VALUES[2]), + left: undefined, + right: VALUES[2], + }, + ] as FeeDescriptionPair[]); + } + { + const pairs = createPairTimeline(right, left); + expect(t, pairs).deep.equals([ + { + from: ABS_TIME[1], + until: ABS_TIME[3], + group: Amounts.stringifyValue(VALUES[1]), + left: undefined, + right: VALUES[1], + }, + { + from: ABS_TIME[1], + until: ABS_TIME[3], + group: Amounts.stringifyValue(VALUES[2]), + left: VALUES[2], + right: undefined, + }, + ] as FeeDescriptionPair[]); + } +}); + +test("should separate denominations of different value2", (t) => { + const left = [ + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[1], + until: ABS_TIME[2], + fee: VALUES[1], + }, + { + group: Amounts.stringifyValue(VALUES[1]), + from: ABS_TIME[2], + until: ABS_TIME[4], + fee: VALUES[2], + }, + ] as FeeDescription[]; + + const right = [ + { + group: Amounts.stringifyValue(VALUES[2]), + from: ABS_TIME[1], + until: ABS_TIME[3], + fee: VALUES[2], + }, + ] as FeeDescription[]; + + { + const pairs = createPairTimeline(left, right); + expect(t, pairs).deep.equals([ + { + from: ABS_TIME[1], + until: ABS_TIME[2], + group: Amounts.stringifyValue(VALUES[1]), + left: VALUES[1], + right: undefined, + }, + { + from: ABS_TIME[2], + until: ABS_TIME[4], + group: Amounts.stringifyValue(VALUES[1]), + left: VALUES[2], + right: undefined, + }, + { + from: ABS_TIME[1], + until: ABS_TIME[3], + group: Amounts.stringifyValue(VALUES[2]), + left: undefined, + right: VALUES[2], + }, + ] as FeeDescriptionPair[]); + } + // { + // const pairs = createDenominationPairTimeline(right, left) + // expect(t,pairs).deep.equals([{ + // from: moments[1], + // until: moments[3], + // value: values[1], + // left: undefined, + // right: values[1], + // }, { + // from: moments[1], + // until: moments[3], + // value: values[2], + // left: values[2], + // right: undefined, + // }] as FeeDescriptionPair[]) + // } +}); +// it.skip("should render real world", (t) => { +// const left = createDenominationTimeline( +// bitcoinExchanges[0].denominations.filter(d => Amounts.cmp(d.value, Amounts.parseOrThrow('BITCOINBTC:0.01048576'))), +// "stampExpireDeposit", "feeDeposit"); +// const right = createDenominationTimeline( +// bitcoinExchanges[1].denominations.filter(d => Amounts.cmp(d.value, Amounts.parseOrThrow('BITCOINBTC:0.01048576'))), +// "stampExpireDeposit", "feeDeposit"); + +// const pairs = createDenominationPairTimeline(left, right) +// }) + +// }) +// }) -- cgit v1.2.3