From 610df1c9cf8ec91815130ac2a426f8f5b7d1ed0c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 12 Oct 2022 15:58:10 -0300 Subject: create a fee description timeline for global fee and wire fees --- .../src/util/denominations.test.ts | 197 ++++++++++++-------- .../taler-wallet-core/src/util/denominations.ts | 200 +++++++++++++-------- 2 files changed, 250 insertions(+), 147 deletions(-) (limited to 'packages/taler-wallet-core/src/util') diff --git a/packages/taler-wallet-core/src/util/denominations.test.ts b/packages/taler-wallet-core/src/util/denominations.test.ts index 31c561e88..9c93331a3 100644 --- a/packages/taler-wallet-core/src/util/denominations.test.ts +++ b/packages/taler-wallet-core/src/util/denominations.test.ts @@ -28,8 +28,9 @@ import { } from "@gnu-taler/taler-util"; // import { expect } from "chai"; import { - createDenominationPairTimeline, - createDenominationTimeline, + createPairTimeline, + createTimeline, + selectBestForOverlappingDenominations, } from "./denominations.js"; import test, { ExecutionContext } from "ava"; @@ -42,8 +43,14 @@ const VALUES = Array.from({ length: 10 }).map((undef, t) => const TIMESTAMPS = Array.from({ length: 20 }).map((undef, t_s) => ({ t_s })); const ABS_TIME = TIMESTAMPS.map((m) => AbsoluteTime.fromTimestamp(m)); -function normalize(list: DenominationInfo[]): DenominationInfo[] { - return list.map((e, idx) => ({ ...e, denomPubHash: `id${idx}` })); +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 @@ -61,7 +68,7 @@ function expect(t: ExecutionContext, thing: any): any { // describe("single value example", (t) => { test("should have one row with start and exp", (t) => { - const timeline = createDenominationTimeline( + const timeline = createTimeline( normalize([ { value: VALUES[1], @@ -70,13 +77,17 @@ test("should have one row with start and exp", (t) => { feeDeposit: VALUES[1], } as Partial as DenominationInfo, ]), + "denomPubHash", + "stampStart", "stampExpireDeposit", "feeDeposit", + "group", + selectBestForOverlappingDenominations, ); expect(t, timeline).deep.equal([ { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[1], until: ABS_TIME[2], fee: VALUES[1], @@ -85,7 +96,7 @@ test("should have one row with start and exp", (t) => { }); test("should have two rows with the second denom in the middle if second is better", (t) => { - const timeline = createDenominationTimeline( + const timeline = createTimeline( normalize([ { value: VALUES[1], @@ -100,19 +111,23 @@ test("should have two rows with the second denom in the middle if second is bett feeDeposit: VALUES[2], } as Partial as DenominationInfo, ]), + "denomPubHash", + "stampStart", "stampExpireDeposit", "feeDeposit", + "group", + selectBestForOverlappingDenominations, ); expect(t, timeline).deep.equal([ { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[1], until: ABS_TIME[3], fee: VALUES[1], }, { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[3], until: ABS_TIME[4], fee: VALUES[2], @@ -121,7 +136,7 @@ test("should have two rows with the second denom in the middle if second is bett }); test("should have two rows with the first denom in the middle if second is worse", (t) => { - const timeline = createDenominationTimeline( + const timeline = createTimeline( normalize([ { value: VALUES[1], @@ -136,19 +151,23 @@ test("should have two rows with the first denom in the middle if second is worse feeDeposit: VALUES[1], } as Partial as DenominationInfo, ]), + "denomPubHash", + "stampStart", "stampExpireDeposit", "feeDeposit", + "group", + selectBestForOverlappingDenominations, ); expect(t, timeline).deep.equal([ { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[1], until: ABS_TIME[2], fee: VALUES[2], }, { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[2], until: ABS_TIME[4], fee: VALUES[1], @@ -157,7 +176,7 @@ test("should have two rows with the first denom in the middle if second is worse }); test("should add a gap when there no fee", (t) => { - const timeline = createDenominationTimeline( + const timeline = createTimeline( normalize([ { value: VALUES[1], @@ -172,24 +191,28 @@ test("should add a gap when there no fee", (t) => { feeDeposit: VALUES[1], } as Partial as DenominationInfo, ]), + "denomPubHash", + "stampStart", "stampExpireDeposit", "feeDeposit", + "group", + selectBestForOverlappingDenominations, ); expect(t, timeline).deep.equal([ { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[1], until: ABS_TIME[2], fee: VALUES[2], }, { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[2], until: ABS_TIME[3], }, { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[3], until: ABS_TIME[4], fee: VALUES[1], @@ -198,7 +221,7 @@ test("should add a gap when there no fee", (t) => { }); test("should have three rows when first denom is between second and second is worse", (t) => { - const timeline = createDenominationTimeline( + const timeline = createTimeline( normalize([ { value: VALUES[1], @@ -213,24 +236,28 @@ test("should have three rows when first denom is between second and second is wo feeDeposit: VALUES[2], } as Partial as DenominationInfo, ]), + "denomPubHash", + "stampStart", "stampExpireDeposit", "feeDeposit", + "group", + selectBestForOverlappingDenominations, ); expect(t, timeline).deep.equal([ { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[1], until: ABS_TIME[2], fee: VALUES[2], }, { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[2], until: ABS_TIME[3], fee: VALUES[1], }, { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[3], until: ABS_TIME[4], fee: VALUES[2], @@ -239,7 +266,7 @@ test("should have three rows when first denom is between second and second is wo }); test("should have one row when first denom is between second and second is better", (t) => { - const timeline = createDenominationTimeline( + const timeline = createTimeline( normalize([ { value: VALUES[1], @@ -254,13 +281,17 @@ test("should have one row when first denom is between second and second is bette feeDeposit: VALUES[1], } as Partial as DenominationInfo, ]), + "denomPubHash", + "stampStart", "stampExpireDeposit", "feeDeposit", + "group", + selectBestForOverlappingDenominations, ); expect(t, timeline).deep.equal([ { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[1], until: ABS_TIME[4], fee: VALUES[1], @@ -269,7 +300,7 @@ test("should have one row when first denom is between second and second is bette }); test("should only add the best1", (t) => { - const timeline = createDenominationTimeline( + const timeline = createTimeline( normalize([ { value: VALUES[1], @@ -290,19 +321,23 @@ test("should only add the best1", (t) => { feeDeposit: VALUES[2], } as Partial as DenominationInfo, ]), + "denomPubHash", + "stampStart", "stampExpireDeposit", "feeDeposit", + "group", + selectBestForOverlappingDenominations, ); expect(t, timeline).deep.equal([ { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[1], until: ABS_TIME[2], fee: VALUES[2], }, { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[2], until: ABS_TIME[4], fee: VALUES[1], @@ -311,7 +346,7 @@ test("should only add the best1", (t) => { }); test("should only add the best2", (t) => { - const timeline = createDenominationTimeline( + const timeline = createTimeline( normalize([ { value: VALUES[1], @@ -338,25 +373,29 @@ test("should only add the best2", (t) => { feeDeposit: VALUES[3], } as Partial as DenominationInfo, ]), + "denomPubHash", + "stampStart", "stampExpireDeposit", "feeDeposit", + "group", + selectBestForOverlappingDenominations, ); expect(t, timeline).deep.equal([ { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[1], until: ABS_TIME[2], fee: VALUES[2], }, { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[2], until: ABS_TIME[5], fee: VALUES[1], }, { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[5], until: ABS_TIME[6], fee: VALUES[3], @@ -365,7 +404,7 @@ test("should only add the best2", (t) => { }); test("should only add the best3", (t) => { - const timeline = createDenominationTimeline( + const timeline = createTimeline( normalize([ { value: VALUES[1], @@ -386,13 +425,17 @@ test("should only add the best3", (t) => { feeDeposit: VALUES[2], } as Partial as DenominationInfo, ]), + "denomPubHash", + "stampStart", "stampExpireDeposit", "feeDeposit", + "group", + selectBestForOverlappingDenominations, ); expect(t, timeline).deep.equal([ { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[2], until: ABS_TIME[5], fee: VALUES[1], @@ -406,7 +449,7 @@ test("should only add the best3", (t) => { //TODO: test the same start but different value test("should not merge when there is different value", (t) => { - const timeline = createDenominationTimeline( + const timeline = createTimeline( normalize([ { value: VALUES[1], @@ -421,19 +464,23 @@ test("should not merge when there is different value", (t) => { feeDeposit: VALUES[2], } as Partial as DenominationInfo, ]), + "denomPubHash", + "stampStart", "stampExpireDeposit", "feeDeposit", + "group", + selectBestForOverlappingDenominations, ); expect(t, timeline).deep.equal([ { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[1], until: ABS_TIME[3], fee: VALUES[1], }, { - value: VALUES[2], + group: Amounts.stringifyValue(VALUES[2]), from: ABS_TIME[2], until: ABS_TIME[4], fee: VALUES[2], @@ -442,7 +489,7 @@ test("should not merge when there is different value", (t) => { }); test("should not merge when there is different value (with duplicates)", (t) => { - const timeline = createDenominationTimeline( + const timeline = createTimeline( normalize([ { value: VALUES[1], @@ -469,19 +516,23 @@ test("should not merge when there is different value (with duplicates)", (t) => feeDeposit: VALUES[2], } as Partial as DenominationInfo, ]), + "denomPubHash", + "stampStart", "stampExpireDeposit", "feeDeposit", + "group", + selectBestForOverlappingDenominations, ); expect(t, timeline).deep.equal([ { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[1], until: ABS_TIME[3], fee: VALUES[1], }, { - value: VALUES[2], + group: Amounts.stringifyValue(VALUES[2]), from: ABS_TIME[2], until: ABS_TIME[4], fee: VALUES[2], @@ -519,7 +570,7 @@ test("should return empty", (t) => { const left = [] as FeeDescription[]; const right = [] as FeeDescription[]; - const pairs = createDenominationPairTimeline(left, right); + const pairs = createPairTimeline(left, right); expect(t, pairs).deep.equals([]); }); @@ -527,7 +578,7 @@ test("should return empty", (t) => { test("should return first element", (t) => { const left = [ { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[1], until: ABS_TIME[3], fee: VALUES[1], @@ -537,24 +588,24 @@ test("should return first element", (t) => { const right = [] as FeeDescription[]; { - const pairs = createDenominationPairTimeline(left, right); + const pairs = createPairTimeline(left, right); expect(t, pairs).deep.equals([ { from: ABS_TIME[1], until: ABS_TIME[3], - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), left: VALUES[1], right: undefined, }, ] as FeeDescriptionPair[]); } { - const pairs = createDenominationPairTimeline(right, left); + const pairs = createPairTimeline(right, left); expect(t, pairs).deep.equals([ { from: ABS_TIME[1], until: ABS_TIME[3], - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), right: VALUES[1], left: undefined, }, @@ -565,7 +616,7 @@ test("should return first element", (t) => { test("should add both to the same row", (t) => { const left = [ { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[1], until: ABS_TIME[3], fee: VALUES[1], @@ -574,7 +625,7 @@ test("should add both to the same row", (t) => { const right = [ { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[1], until: ABS_TIME[3], fee: VALUES[2], @@ -582,24 +633,24 @@ test("should add both to the same row", (t) => { ] as FeeDescription[]; { - const pairs = createDenominationPairTimeline(left, right); + const pairs = createPairTimeline(left, right); expect(t, pairs).deep.equals([ { from: ABS_TIME[1], until: ABS_TIME[3], - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), left: VALUES[1], right: VALUES[2], }, ] as FeeDescriptionPair[]); } { - const pairs = createDenominationPairTimeline(right, left); + const pairs = createPairTimeline(right, left); expect(t, pairs).deep.equals([ { from: ABS_TIME[1], until: ABS_TIME[3], - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), left: VALUES[2], right: VALUES[1], }, @@ -610,7 +661,7 @@ test("should add both to the same row", (t) => { test("should repeat the first and change the second", (t) => { const left = [ { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[1], until: ABS_TIME[5], fee: VALUES[1], @@ -619,18 +670,18 @@ test("should repeat the first and change the second", (t) => { const right = [ { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[1], until: ABS_TIME[2], fee: VALUES[2], }, { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[2], until: ABS_TIME[3], }, { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[3], until: ABS_TIME[4], fee: VALUES[3], @@ -638,33 +689,33 @@ test("should repeat the first and change the second", (t) => { ] as FeeDescription[]; { - const pairs = createDenominationPairTimeline(left, right); + const pairs = createPairTimeline(left, right); expect(t, pairs).deep.equals([ { from: ABS_TIME[1], until: ABS_TIME[2], - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), left: VALUES[1], right: VALUES[2], }, { from: ABS_TIME[2], until: ABS_TIME[3], - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), left: VALUES[1], right: undefined, }, { from: ABS_TIME[3], until: ABS_TIME[4], - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), left: VALUES[1], right: VALUES[3], }, { from: ABS_TIME[4], until: ABS_TIME[5], - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), left: VALUES[1], right: undefined, }, @@ -679,7 +730,7 @@ test("should repeat the first and change the second", (t) => { test("should separate denominations of different value", (t) => { const left = [ { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[1], until: ABS_TIME[3], fee: VALUES[1], @@ -688,7 +739,7 @@ test("should separate denominations of different value", (t) => { const right = [ { - value: VALUES[2], + group: Amounts.stringifyValue(VALUES[2]), from: ABS_TIME[1], until: ABS_TIME[3], fee: VALUES[2], @@ -696,38 +747,38 @@ test("should separate denominations of different value", (t) => { ] as FeeDescription[]; { - const pairs = createDenominationPairTimeline(left, right); + const pairs = createPairTimeline(left, right); expect(t, pairs).deep.equals([ { from: ABS_TIME[1], until: ABS_TIME[3], - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), left: VALUES[1], right: undefined, }, { from: ABS_TIME[1], until: ABS_TIME[3], - value: VALUES[2], + group: Amounts.stringifyValue(VALUES[2]), left: undefined, right: VALUES[2], }, ] as FeeDescriptionPair[]); } { - const pairs = createDenominationPairTimeline(right, left); + const pairs = createPairTimeline(right, left); expect(t, pairs).deep.equals([ { from: ABS_TIME[1], until: ABS_TIME[3], - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), left: undefined, right: VALUES[1], }, { from: ABS_TIME[1], until: ABS_TIME[3], - value: VALUES[2], + group: Amounts.stringifyValue(VALUES[2]), left: VALUES[2], right: undefined, }, @@ -738,13 +789,13 @@ test("should separate denominations of different value", (t) => { test("should separate denominations of different value2", (t) => { const left = [ { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[1], until: ABS_TIME[2], fee: VALUES[1], }, { - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), from: ABS_TIME[2], until: ABS_TIME[4], fee: VALUES[2], @@ -753,7 +804,7 @@ test("should separate denominations of different value2", (t) => { const right = [ { - value: VALUES[2], + group: Amounts.stringifyValue(VALUES[2]), from: ABS_TIME[1], until: ABS_TIME[3], fee: VALUES[2], @@ -761,26 +812,26 @@ test("should separate denominations of different value2", (t) => { ] as FeeDescription[]; { - const pairs = createDenominationPairTimeline(left, right); + const pairs = createPairTimeline(left, right); expect(t, pairs).deep.equals([ { from: ABS_TIME[1], until: ABS_TIME[2], - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), left: VALUES[1], right: undefined, }, { from: ABS_TIME[2], until: ABS_TIME[4], - value: VALUES[1], + group: Amounts.stringifyValue(VALUES[1]), left: VALUES[2], right: undefined, }, { from: ABS_TIME[1], until: ABS_TIME[3], - value: VALUES[2], + group: Amounts.stringifyValue(VALUES[2]), left: undefined, right: VALUES[2], }, diff --git a/packages/taler-wallet-core/src/util/denominations.ts b/packages/taler-wallet-core/src/util/denominations.ts index 4efb902c8..9cd931acd 100644 --- a/packages/taler-wallet-core/src/util/denominations.ts +++ b/packages/taler-wallet-core/src/util/denominations.ts @@ -23,6 +23,7 @@ import { FeeDescriptionPair, TalerProtocolTimestamp, TimePoint, + WireFee, } from "@gnu-taler/taler-util"; /** @@ -33,9 +34,9 @@ import { * @param list denominations of same value * @returns */ -function selectBestForOverlappingDenominations( - list: DenominationInfo[], -): DenominationInfo | undefined { +export function selectBestForOverlappingDenominations< + T extends DenominationInfo, +>(list: T[]): T | undefined { let minDeposit: DenominationInfo | undefined = undefined; //TODO: improve denomination selection, this is a trivial implementation list.forEach((e) => { @@ -50,6 +51,23 @@ function selectBestForOverlappingDenominations( return minDeposit; } +export function selectMinimumFee( + list: T[], +): T | undefined { + let minFee: T | undefined = undefined; + //TODO: improve denomination selection, this is a trivial implementation + list.forEach((e) => { + if (minFee === undefined) { + minFee = e; + return; + } + if (Amounts.cmp(minFee.fee, e.fee) > -1) { + minFee = e; + } + }); + return minFee; +} + type PropsWithReturnType = Exclude< { [K in keyof T]: T[K] extends F ? K : never; @@ -58,17 +76,19 @@ type PropsWithReturnType = Exclude< >; /** - * Takes two list and create one with one timeline. - * For any element in the position "p" on the left or right "list", then - * list[p].until should be equal to list[p+1].from + * Takes two timelines and create one to compare them. + * + * For both lists the next condition should be true: + * for any element in the position "idx" then + * list[idx].until === list[idx+1].from * - * @see {createDenominationTimeline} + * @see {createTimeline} * * @param left list denominations @type {FeeDescription} * @param right list denominations @type {FeeDescription} * @returns list of pairs for the same time */ -export function createDenominationPairTimeline( +export function createPairTimeline( left: FeeDescription[], right: FeeDescription[], ): FeeDescriptionPair[] { @@ -81,23 +101,15 @@ export function createDenominationPairTimeline( let ri = 0; while (li < left.length && ri < right.length) { - const currentValue = - Amounts.cmp(left[li].value, right[ri].value) < 0 - ? left[li].value - : right[ri].value; + const currentGroup = + left[li].group < right[ri].group ? left[li].group : right[ri].group; let ll = 0; //left length (until next value) - while ( - li + ll < left.length && - Amounts.cmp(left[li + ll].value, currentValue) === 0 - ) { + while (li + ll < left.length && left[li + ll].group === currentGroup) { ll++; } let rl = 0; //right length (until next value) - while ( - ri + rl < right.length && - Amounts.cmp(right[ri + rl].value, currentValue) === 0 - ) { + while (ri + rl < right.length && right[ri + rl].group === currentGroup) { rl++; } const leftIsEmpty = ll === 0; @@ -120,7 +132,7 @@ export function createDenominationPairTimeline( right.splice(ri, 0, { from: leftStarts, until: ends, - value: left[li].value, + group: left[li].group, }); rl++; @@ -132,7 +144,7 @@ export function createDenominationPairTimeline( left.splice(li, 0, { from: rightStarts, until: ends, - value: right[ri].value, + group: right[ri].group, }); ll++; @@ -148,7 +160,7 @@ export function createDenominationPairTimeline( right.splice(ri + rl, 0, { from: rightEnds, until: leftEnds, - value: left[0].value, + group: left[0].group, }); rl++; } @@ -156,7 +168,7 @@ export function createDenominationPairTimeline( left.splice(li + ll, 0, { from: leftEnds, until: rightEnds, - value: right[0].value, + group: right[0].group, }); ll++; } @@ -165,7 +177,7 @@ export function createDenominationPairTimeline( while ( li < left.length && ri < right.length && - Amounts.cmp(left[li].value, right[ri].value) === 0 + left[li].group === right[ri].group ) { if ( AbsoluteTime.cmp(left[li].from, timeCut) !== 0 && @@ -186,7 +198,7 @@ export function createDenominationPairTimeline( right: right[ri].fee, from: timeCut, until: AbsoluteTime.never(), - value: currentValue, + group: currentGroup, }); if (left[li].until.t_ms === right[ri].until.t_ms) { @@ -204,7 +216,7 @@ export function createDenominationPairTimeline( if ( li < left.length && - Amounts.cmp(left[li].value, pairList[pairList.length - 1].value) !== 0 + left[li].group !== pairList[pairList.length - 1].group ) { //value changed, should break //this if will catch when both (left and right) change at the same time @@ -217,7 +229,7 @@ export function createDenominationPairTimeline( if (li < left.length) { let timeCut = pairList.length > 0 && - Amounts.cmp(pairList[pairList.length - 1].value, left[li].value) === 0 + pairList[pairList.length - 1].group === left[li].group ? pairList[pairList.length - 1].until : left[li].from; while (li < left.length) { @@ -226,7 +238,7 @@ export function createDenominationPairTimeline( right: undefined, from: timeCut, until: left[li].until, - value: left[li].value, + group: left[li].group, }); timeCut = left[li].until; li++; @@ -235,7 +247,7 @@ export function createDenominationPairTimeline( if (ri < right.length) { let timeCut = pairList.length > 0 && - Amounts.cmp(pairList[pairList.length - 1].value, right[ri].value) === 0 + pairList[pairList.length - 1].group === right[ri].group ? pairList[pairList.length - 1].until : right[ri].from; while (ri < right.length) { @@ -244,7 +256,7 @@ export function createDenominationPairTimeline( left: undefined, from: timeCut, until: right[ri].until, - value: right[ri].value, + group: right[ri].group, }); timeCut = right[ri].until; ri++; @@ -254,42 +266,70 @@ export function createDenominationPairTimeline( } /** - * Create a usage timeline with the denominations given. + * Create a usage timeline with the entity given. * - * If there are multiple denominations that can be used, the list will - * contain the one that minimize the fee cost. @see selectBestForOverlappingDenominations + * If there are multiple entities that can be used in the same period, + * the list will contain the one that minimize the fee cost. + * @see selectBestForOverlappingDenominations * - * @param list list of denominations - * @param periodProp property of element of the list that will be used as end of the usage period + * @param list list of entities + * @param idProp property used for identification + * @param periodStartProp property of element of the list that will be used as start of the usage period + * @param periodEndProp property of element of the list that will be used as end of the usage period * @param feeProp property of the element of the list that will be used as fee reference + * @param groupProp property of the element of the list that will be used for grouping * @returns list of @type {FeeDescription} sorted by usage period */ -export function createDenominationTimeline( - list: DenominationInfo[], - periodProp: PropsWithReturnType, - feeProp: PropsWithReturnType, +export function createTimeline( + list: Type[], + idProp: PropsWithReturnType, + periodStartProp: PropsWithReturnType, + periodEndProp: PropsWithReturnType, + feeProp: PropsWithReturnType, + groupProp: PropsWithReturnType | undefined, + selectBestForOverlapping: (l: Type[]) => Type | undefined, ): FeeDescription[] { - const points = list + /** + * First we create a list with with point in the timeline sorted + * by time and categorized by starting or ending. + */ + const sortedPointsInTime = list .reduce((ps, denom) => { //exclude denoms with bad configuration - if (denom.stampStart.t_s >= denom[periodProp].t_s) { - throw Error(`denom ${denom.denomPubHash} has start after the end`); - // return ps; + const id = denom[idProp] as string; + const stampStart = denom[periodStartProp] as TalerProtocolTimestamp; + const stampEnd = denom[periodEndProp] as TalerProtocolTimestamp; + const fee = denom[feeProp] as AmountJson; + const group = !groupProp ? "" : (denom[groupProp] as string); + + if (!id) { + throw Error( + `denomination without hash ${JSON.stringify(denom, undefined, 2)}`, + ); + } + if (stampStart.t_s >= stampEnd.t_s) { + throw Error(`denom ${id} has start after the end`); } ps.push({ type: "start", - moment: AbsoluteTime.fromTimestamp(denom.stampStart), + fee, + group, + id, + moment: AbsoluteTime.fromTimestamp(stampStart), denom, }); ps.push({ type: "end", - moment: AbsoluteTime.fromTimestamp(denom[periodProp]), + fee, + group, + id, + moment: AbsoluteTime.fromTimestamp(stampEnd), denom, }); return ps; - }, [] as TimePoint[]) + }, [] as TimePoint[]) .sort((a, b) => { - const v = Amounts.cmp(a.denom.value, b.denom.value); + const v = a.group == b.group ? 0 : a.group > b.group ? 1 : -1; if (v != 0) return v; const t = AbsoluteTime.cmp(a.moment, b.moment); if (t != 0) return t; @@ -297,21 +337,15 @@ export function createDenominationTimeline( return a.type === "start" ? 1 : -1; }); - const activeAtTheSameTime: DenominationInfo[] = []; - return points.reduce((result, cursor, idx) => { - const hash = cursor.denom.denomPubHash; - if (!hash) - throw Error( - `denomination without hash ${JSON.stringify( - cursor.denom, - undefined, - 2, - )}`, - ); - + const activeAtTheSameTime: Type[] = []; + return sortedPointsInTime.reduce((result, cursor, idx) => { + /** + * Now that we have move one step forward, we should + * update the previous element ending period with the + * current start time. + */ let prev = result.length > 0 ? result[result.length - 1] : undefined; - const prevHasSameValue = - prev && Amounts.cmp(prev.value, cursor.denom.value) === 0; + const prevHasSameValue = prev && prev.group == cursor.group; if (prev) { if (prevHasSameValue) { prev.until = cursor.moment; @@ -326,11 +360,15 @@ export function createDenominationTimeline( } } - //update the activeAtTheSameTime list + /** + * With the current moment in the iteration we + * should keep updated which entities are current + * active in this period of time. + */ if (cursor.type === "end") { - const loc = activeAtTheSameTime.findIndex((v) => v.denomPubHash === hash); + const loc = activeAtTheSameTime.findIndex((v) => v[idProp] === cursor.id); if (loc === -1) { - throw Error(`denomination ${hash} has an end but no start`); + throw Error(`denomination ${cursor.id} has an end but no start`); } activeAtTheSameTime.splice(loc, 1); } else if (cursor.type === "start") { @@ -340,12 +378,16 @@ export function createDenominationTimeline( throw new Error(`not TimePoint defined for type: ${exhaustiveCheck}`); } - if (idx == points.length - 1) { - //this is the last element in the list, prevent adding - //a gap in the end + if (idx == sortedPointsInTime.length - 1) { + /** + * This is the last element in the list, if we continue + * a gap will normally be added which is not necessary. + * Also, the last element should be ending and the list of active + * element should be empty + */ if (cursor.type !== "end") { throw Error( - `denomination ${hash} starts after ending or doesn't have an ending`, + `denomination ${cursor.id} starts after ending or doesn't have an ending`, ); } if (activeAtTheSameTime.length > 0) { @@ -356,26 +398,36 @@ export function createDenominationTimeline( return result; } - const current = selectBestForOverlappingDenominations(activeAtTheSameTime); + const current = selectBestForOverlapping(activeAtTheSameTime); if (current) { + /** + * We have a candidate to add in the list, check that we are + * not adding a duplicate. + * Next element in the list will defined the ending. + */ + const currentFee = current[feeProp] as AmountJson; if ( prev === undefined || //is the first !prev.fee || //is a gap - Amounts.cmp(prev.fee, current[feeProp]) !== 0 // prev has the same fee + Amounts.cmp(prev.fee, currentFee) !== 0 // prev has different fee ) { result.push({ - value: cursor.denom.value, + group: cursor.group, from: cursor.moment, until: AbsoluteTime.never(), //not yet known - fee: current[feeProp], + fee: currentFee, }); } else { prev.until = cursor.moment; } } else { + /** + * No active element in this period of time, so we add a gap (no fee) + * Next element in the list will defined the ending. + */ result.push({ - value: cursor.denom.value, + group: cursor.group, from: cursor.moment, until: AbsoluteTime.never(), //not yet known }); -- cgit v1.2.3