ref #7323
This commit is contained in:
parent
fc413bb5ec
commit
27201416c7
@ -343,6 +343,21 @@ export function durationAdd(d1: Duration, d2: Duration): Duration {
|
||||
return { d_ms: d1.d_ms + d2.d_ms };
|
||||
}
|
||||
|
||||
export const codecForAbsoluteTime: Codec<AbsoluteTime> = {
|
||||
decode(x: any, c?: Context): AbsoluteTime {
|
||||
const t_ms = x.t_ms;
|
||||
if (typeof t_ms === "string") {
|
||||
if (t_ms === "never") {
|
||||
return { t_ms: "never" };
|
||||
}
|
||||
} else if (typeof t_ms === "number") {
|
||||
return { t_ms };
|
||||
}
|
||||
throw Error(`expected timestamp at ${renderContext(c)}`);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export const codecForTimestamp: Codec<TalerProtocolTimestamp> = {
|
||||
decode(x: any, c?: Context): TalerProtocolTimestamp {
|
||||
// Compatibility, should be removed soon.
|
||||
|
@ -32,7 +32,7 @@ import {
|
||||
codecForAmountJson,
|
||||
codecForAmountString,
|
||||
} from "./amounts.js";
|
||||
import { codecForTimestamp, TalerProtocolTimestamp } from "./time.js";
|
||||
import { AbsoluteTime, codecForAbsoluteTime, codecForTimestamp, TalerProtocolTimestamp } from "./time.js";
|
||||
import {
|
||||
buildCodecForObject,
|
||||
codecForString,
|
||||
@ -733,6 +733,30 @@ export interface DenominationInfo {
|
||||
stampExpireDeposit: TalerProtocolTimestamp;
|
||||
}
|
||||
|
||||
export type Operation = "deposit" | "withdraw" | "refresh" | "refund";
|
||||
export type OperationMap<T> = { [op in Operation]: T };
|
||||
|
||||
export interface FeeDescription {
|
||||
value: AmountJson;
|
||||
from: AbsoluteTime;
|
||||
until: AbsoluteTime;
|
||||
fee?: AmountJson;
|
||||
}
|
||||
|
||||
export interface FeeDescriptionPair {
|
||||
value: AmountJson;
|
||||
from: AbsoluteTime;
|
||||
until: AbsoluteTime;
|
||||
left?: AmountJson;
|
||||
right?: AmountJson;
|
||||
}
|
||||
|
||||
export interface TimePoint {
|
||||
type: "start" | "end";
|
||||
moment: AbsoluteTime;
|
||||
denom: DenominationInfo;
|
||||
}
|
||||
|
||||
export interface ExchangeFullDetails {
|
||||
exchangeBaseUrl: string;
|
||||
currency: string;
|
||||
@ -740,7 +764,7 @@ export interface ExchangeFullDetails {
|
||||
tos: ExchangeTos;
|
||||
auditors: ExchangeAuditor[];
|
||||
wireInfo: WireInfo;
|
||||
denominations: DenominationInfo[];
|
||||
feesDescription: OperationMap<FeeDescription[]>;
|
||||
}
|
||||
|
||||
export interface ExchangeListItem {
|
||||
@ -771,6 +795,35 @@ const codecForExchangeTos = (): Codec<ExchangeTos> =>
|
||||
.property("content", codecOptional(codecForString()))
|
||||
.build("ExchangeTos");
|
||||
|
||||
export const codecForFeeDescriptionPair =
|
||||
(): Codec<FeeDescriptionPair> =>
|
||||
buildCodecForObject<FeeDescriptionPair>()
|
||||
.property("value", codecForAmountJson())
|
||||
.property("from", codecForAbsoluteTime)
|
||||
.property("until", codecForAbsoluteTime)
|
||||
.property("left", codecOptional(codecForAmountJson()))
|
||||
.property("right", codecOptional(codecForAmountJson()))
|
||||
.build("FeeDescriptionPair");
|
||||
|
||||
export const codecForFeeDescription =
|
||||
(): Codec<FeeDescription> =>
|
||||
buildCodecForObject<FeeDescription>()
|
||||
.property("value", codecForAmountJson())
|
||||
.property("from", codecForAbsoluteTime)
|
||||
.property("until", codecForAbsoluteTime)
|
||||
.property("fee", codecOptional(codecForAmountJson()))
|
||||
.build("FeeDescription");
|
||||
|
||||
|
||||
export const codecForFeesByOperations =
|
||||
(): Codec<OperationMap<FeeDescription[]>> =>
|
||||
buildCodecForObject<OperationMap<FeeDescription[]>>()
|
||||
.property("deposit", codecForList(codecForFeeDescription()))
|
||||
.property("withdraw", codecForList(codecForFeeDescription()))
|
||||
.property("refresh", codecForList(codecForFeeDescription()))
|
||||
.property("refund", codecForList(codecForFeeDescription()))
|
||||
.build("FeesByOperations");
|
||||
|
||||
export const codecForExchangeFullDetails =
|
||||
(): Codec<ExchangeFullDetails> =>
|
||||
buildCodecForObject<ExchangeFullDetails>()
|
||||
@ -780,8 +833,8 @@ export const codecForExchangeFullDetails =
|
||||
.property("tos", codecForExchangeTos())
|
||||
.property("auditors", codecForList(codecForExchangeAuditor()))
|
||||
.property("wireInfo", codecForWireInfo())
|
||||
.property("denominations", codecForList(codecForDenominationInfo()))
|
||||
.build("ExchangeListItem");
|
||||
.property("feesDescription", codecForFeesByOperations())
|
||||
.build("ExchangeFullDetails");
|
||||
|
||||
export const codecForExchangeListItem = (): Codec<ExchangeListItem> =>
|
||||
buildCodecForObject<ExchangeListItem>()
|
||||
|
@ -65,3 +65,4 @@ export {
|
||||
} from "./crypto/cryptoImplementation.js";
|
||||
|
||||
export * from "./util/timer.js";
|
||||
export * from "./util/denominations.js";
|
||||
|
712
packages/taler-wallet-core/src/util/denominations.test.ts
Normal file
712
packages/taler-wallet-core/src/util/denominations.test.ts
Normal file
@ -0,0 +1,712 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import {
|
||||
AbsoluteTime,FeeDescription, FeeDescriptionPair,
|
||||
Amounts, DenominationInfo
|
||||
} from "@gnu-taler/taler-util";
|
||||
// import { expect } from "chai";
|
||||
import { createDenominationPairTimeline, createDenominationTimeline } from "./denominations.js";
|
||||
import test, { ExecutionContext } from "ava";
|
||||
|
||||
/**
|
||||
* Create some constants to be used as reference in the tests
|
||||
*/
|
||||
const VALUES = Array.from({ length: 10 }).map((undef, t) => Amounts.parseOrThrow(`USD:${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}` }))
|
||||
}
|
||||
|
||||
//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 = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[1],
|
||||
stampExpireDeposit: TIMESTAMPS[2],
|
||||
feeDeposit: VALUES[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
|
||||
expect(t,timeline).deep.equal([{
|
||||
value: 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 = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[1],
|
||||
stampExpireDeposit: TIMESTAMPS[3],
|
||||
feeDeposit: VALUES[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[2],
|
||||
stampExpireDeposit: TIMESTAMPS[4],
|
||||
feeDeposit: VALUES[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
|
||||
expect(t,timeline).deep.equal([{
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[3],
|
||||
fee: VALUES[1],
|
||||
}, {
|
||||
value: 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 = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[1],
|
||||
stampExpireDeposit: TIMESTAMPS[3],
|
||||
feeDeposit: VALUES[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[2],
|
||||
stampExpireDeposit: TIMESTAMPS[4],
|
||||
feeDeposit: VALUES[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
|
||||
expect(t,timeline).deep.equal([{
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[2],
|
||||
fee: VALUES[2],
|
||||
}, {
|
||||
value: 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 = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[1],
|
||||
stampExpireDeposit: TIMESTAMPS[2],
|
||||
feeDeposit: VALUES[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[3],
|
||||
stampExpireDeposit: TIMESTAMPS[4],
|
||||
feeDeposit: VALUES[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
|
||||
expect(t,timeline).deep.equal([{
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[2],
|
||||
fee: VALUES[2],
|
||||
}, {
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[2],
|
||||
until: ABS_TIME[3],
|
||||
|
||||
}, {
|
||||
value: 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 = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[2],
|
||||
stampExpireDeposit: TIMESTAMPS[3],
|
||||
feeDeposit: VALUES[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[1],
|
||||
stampExpireDeposit: TIMESTAMPS[4],
|
||||
feeDeposit: VALUES[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
expect(t,timeline).deep.equal([{
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[2],
|
||||
fee: VALUES[2],
|
||||
}, {
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[2],
|
||||
until: ABS_TIME[3],
|
||||
fee: VALUES[1],
|
||||
}, {
|
||||
value: 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 = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[2],
|
||||
stampExpireDeposit: TIMESTAMPS[3],
|
||||
feeDeposit: VALUES[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[1],
|
||||
stampExpireDeposit: TIMESTAMPS[4],
|
||||
feeDeposit: VALUES[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
|
||||
expect(t,timeline).deep.equal([{
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[4],
|
||||
fee: VALUES[1],
|
||||
}] as FeeDescription[])
|
||||
|
||||
});
|
||||
|
||||
test("should only add the best1", (t) => {
|
||||
const timeline = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[1],
|
||||
stampExpireDeposit: TIMESTAMPS[3],
|
||||
feeDeposit: VALUES[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[2],
|
||||
stampExpireDeposit: TIMESTAMPS[4],
|
||||
feeDeposit: VALUES[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[2],
|
||||
stampExpireDeposit: TIMESTAMPS[4],
|
||||
feeDeposit: VALUES[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
|
||||
expect(t,timeline).deep.equal([{
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[2],
|
||||
fee: VALUES[2],
|
||||
}, {
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[2],
|
||||
until: ABS_TIME[4],
|
||||
fee: VALUES[1],
|
||||
}] as FeeDescription[])
|
||||
|
||||
});
|
||||
|
||||
test("should only add the best2", (t) => {
|
||||
const timeline = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[1],
|
||||
stampExpireDeposit: TIMESTAMPS[3],
|
||||
feeDeposit: VALUES[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[2],
|
||||
stampExpireDeposit: TIMESTAMPS[5],
|
||||
feeDeposit: VALUES[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[2],
|
||||
stampExpireDeposit: TIMESTAMPS[4],
|
||||
feeDeposit: VALUES[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[5],
|
||||
stampExpireDeposit: TIMESTAMPS[6],
|
||||
feeDeposit: VALUES[3]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
|
||||
expect(t,timeline).deep.equal([{
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[2],
|
||||
fee: VALUES[2],
|
||||
}, {
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[2],
|
||||
until: ABS_TIME[5],
|
||||
fee: VALUES[1],
|
||||
}, {
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[5],
|
||||
until: ABS_TIME[6],
|
||||
fee: VALUES[3],
|
||||
}] as FeeDescription[])
|
||||
|
||||
});
|
||||
|
||||
test("should only add the best3", (t) => {
|
||||
const timeline = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[2],
|
||||
stampExpireDeposit: TIMESTAMPS[5],
|
||||
feeDeposit: VALUES[3]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[2],
|
||||
stampExpireDeposit: TIMESTAMPS[5],
|
||||
feeDeposit: VALUES[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[2],
|
||||
stampExpireDeposit: TIMESTAMPS[5],
|
||||
feeDeposit: VALUES[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
|
||||
expect(t,timeline).deep.equal([{
|
||||
value: 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 = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[1],
|
||||
stampExpireDeposit: TIMESTAMPS[3],
|
||||
feeDeposit: VALUES[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: VALUES[2],
|
||||
stampStart: TIMESTAMPS[2],
|
||||
stampExpireDeposit: TIMESTAMPS[4],
|
||||
feeDeposit: VALUES[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
|
||||
expect(t,timeline).deep.equal([{
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[3],
|
||||
fee: VALUES[1],
|
||||
}, {
|
||||
value: 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 = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[1],
|
||||
stampExpireDeposit: TIMESTAMPS[3],
|
||||
feeDeposit: VALUES[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: VALUES[2],
|
||||
stampStart: TIMESTAMPS[2],
|
||||
stampExpireDeposit: TIMESTAMPS[4],
|
||||
feeDeposit: VALUES[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: VALUES[1],
|
||||
stampStart: TIMESTAMPS[1],
|
||||
stampExpireDeposit: TIMESTAMPS[3],
|
||||
feeDeposit: VALUES[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: VALUES[2],
|
||||
stampStart: TIMESTAMPS[2],
|
||||
stampExpireDeposit: TIMESTAMPS[4],
|
||||
feeDeposit: VALUES[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
|
||||
expect(t,timeline).deep.equal([{
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[3],
|
||||
fee: VALUES[1],
|
||||
}, {
|
||||
value: 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 = createDenominationPairTimeline(left, right)
|
||||
|
||||
expect(t,pairs).deep.equals([])
|
||||
});
|
||||
|
||||
test("should return first element", (t) => {
|
||||
|
||||
const left = [{
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[3],
|
||||
fee: VALUES[1],
|
||||
}] as FeeDescription[];
|
||||
|
||||
const right = [] as FeeDescription[];
|
||||
|
||||
{
|
||||
const pairs = createDenominationPairTimeline(left, right)
|
||||
expect(t,pairs).deep.equals([{
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[3],
|
||||
value: VALUES[1],
|
||||
left: VALUES[1],
|
||||
right: undefined,
|
||||
}] as FeeDescriptionPair[])
|
||||
}
|
||||
{
|
||||
const pairs = createDenominationPairTimeline(right, left)
|
||||
expect(t,pairs).deep.equals([{
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[3],
|
||||
value: VALUES[1],
|
||||
right: VALUES[1],
|
||||
left: undefined,
|
||||
}] as FeeDescriptionPair[])
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
test("should add both to the same row", (t) => {
|
||||
|
||||
const left = [{
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[3],
|
||||
fee: VALUES[1],
|
||||
}] as FeeDescription[];
|
||||
|
||||
const right = [{
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[3],
|
||||
fee: VALUES[2],
|
||||
}] as FeeDescription[];
|
||||
|
||||
{
|
||||
const pairs = createDenominationPairTimeline(left, right)
|
||||
expect(t,pairs).deep.equals([{
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[3],
|
||||
value: VALUES[1],
|
||||
left: VALUES[1],
|
||||
right: VALUES[2],
|
||||
}] as FeeDescriptionPair[])
|
||||
}
|
||||
{
|
||||
const pairs = createDenominationPairTimeline(right, left)
|
||||
expect(t,pairs).deep.equals([{
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[3],
|
||||
value: VALUES[1],
|
||||
left: VALUES[2],
|
||||
right: VALUES[1],
|
||||
}] as FeeDescriptionPair[])
|
||||
}
|
||||
});
|
||||
|
||||
test("should repeat the first and change the second", (t) => {
|
||||
|
||||
const left = [{
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[5],
|
||||
fee: VALUES[1],
|
||||
}] as FeeDescription[];
|
||||
|
||||
const right = [{
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[2],
|
||||
fee: VALUES[2],
|
||||
}, {
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[2],
|
||||
until: ABS_TIME[3],
|
||||
}, {
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[3],
|
||||
until: ABS_TIME[4],
|
||||
fee: VALUES[3],
|
||||
}] as FeeDescription[];
|
||||
|
||||
{
|
||||
const pairs = createDenominationPairTimeline(left, right)
|
||||
expect(t,pairs).deep.equals([{
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[2],
|
||||
value: VALUES[1],
|
||||
left: VALUES[1],
|
||||
right: VALUES[2],
|
||||
}, {
|
||||
from: ABS_TIME[2],
|
||||
until: ABS_TIME[3],
|
||||
value: VALUES[1],
|
||||
left: VALUES[1],
|
||||
right: undefined,
|
||||
}, {
|
||||
from: ABS_TIME[3],
|
||||
until: ABS_TIME[4],
|
||||
value: VALUES[1],
|
||||
left: VALUES[1],
|
||||
right: VALUES[3],
|
||||
}, {
|
||||
from: ABS_TIME[4],
|
||||
until: ABS_TIME[5],
|
||||
value: VALUES[1],
|
||||
left: VALUES[1],
|
||||
right: undefined,
|
||||
}] as FeeDescriptionPair[])
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
// })
|
||||
|
||||
// describe("multiple value example", (t) => {
|
||||
|
||||
test("should separate denominations of different value", (t) => {
|
||||
|
||||
const left = [{
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[3],
|
||||
fee: VALUES[1],
|
||||
}] as FeeDescription[];
|
||||
|
||||
const right = [{
|
||||
value: VALUES[2],
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[3],
|
||||
fee: VALUES[2],
|
||||
}] as FeeDescription[];
|
||||
|
||||
{
|
||||
const pairs = createDenominationPairTimeline(left, right)
|
||||
expect(t,pairs).deep.equals([{
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[3],
|
||||
value: VALUES[1],
|
||||
left: VALUES[1],
|
||||
right: undefined,
|
||||
}, {
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[3],
|
||||
value: VALUES[2],
|
||||
left: undefined,
|
||||
right: VALUES[2],
|
||||
}] as FeeDescriptionPair[])
|
||||
}
|
||||
{
|
||||
const pairs = createDenominationPairTimeline(right, left)
|
||||
expect(t,pairs).deep.equals([{
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[3],
|
||||
value: VALUES[1],
|
||||
left: undefined,
|
||||
right: VALUES[1],
|
||||
}, {
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[3],
|
||||
value: VALUES[2],
|
||||
left: VALUES[2],
|
||||
right: undefined,
|
||||
}] as FeeDescriptionPair[])
|
||||
}
|
||||
});
|
||||
|
||||
test("should separate denominations of different value2", (t) => {
|
||||
|
||||
const left = [{
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[2],
|
||||
fee: VALUES[1],
|
||||
}, {
|
||||
value: VALUES[1],
|
||||
from: ABS_TIME[2],
|
||||
until: ABS_TIME[4],
|
||||
fee: VALUES[2],
|
||||
}] as FeeDescription[];
|
||||
|
||||
const right = [{
|
||||
value: VALUES[2],
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[3],
|
||||
fee: VALUES[2],
|
||||
}] as FeeDescription[];
|
||||
|
||||
{
|
||||
const pairs = createDenominationPairTimeline(left, right)
|
||||
expect(t,pairs).deep.equals([{
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[2],
|
||||
value: VALUES[1],
|
||||
left: VALUES[1],
|
||||
right: undefined,
|
||||
}, {
|
||||
from: ABS_TIME[2],
|
||||
until: ABS_TIME[4],
|
||||
value: VALUES[1],
|
||||
left: VALUES[2],
|
||||
right: undefined,
|
||||
}, {
|
||||
from: ABS_TIME[1],
|
||||
until: ABS_TIME[3],
|
||||
value: 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)
|
||||
// })
|
||||
|
||||
// })
|
||||
// })
|
||||
|
349
packages/taler-wallet-core/src/util/denominations.ts
Normal file
349
packages/taler-wallet-core/src/util/denominations.ts
Normal file
@ -0,0 +1,349 @@
|
||||
/*
|
||||
This file is part of GNU Taler
|
||||
(C) 2021 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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
import { AbsoluteTime, AmountJson, Amounts, DenominationInfo, FeeDescription, FeeDescriptionPair, TalerProtocolTimestamp, TimePoint } from "@gnu-taler/taler-util";
|
||||
|
||||
/**
|
||||
* Given a list of denominations with the same value and same period of time:
|
||||
* return the one that will be used.
|
||||
* The best denomination is the one that will minimize the fee cost.
|
||||
*
|
||||
* @param list denominations of same value
|
||||
* @returns
|
||||
*/
|
||||
function selectBestForOverlappingDenominations(
|
||||
list: DenominationInfo[],
|
||||
): DenominationInfo | undefined {
|
||||
let minDeposit: DenominationInfo | undefined = undefined;
|
||||
//TODO: improve denomination selection, this is a trivial implementation
|
||||
list.forEach((e) => {
|
||||
if (minDeposit === undefined) {
|
||||
minDeposit = e;
|
||||
return;
|
||||
}
|
||||
if (Amounts.cmp(minDeposit.feeDeposit, e.feeDeposit) > -1) {
|
||||
minDeposit = e;
|
||||
}
|
||||
});
|
||||
return minDeposit;
|
||||
}
|
||||
|
||||
type PropsWithReturnType<T extends object, F> = Exclude<
|
||||
{
|
||||
[K in keyof T]: T[K] extends F ? K : never;
|
||||
}[keyof T],
|
||||
undefined
|
||||
>;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @see {createDenominationTimeline}
|
||||
*
|
||||
* @param left list denominations @type {FeeDescription}
|
||||
* @param right list denominations @type {FeeDescription}
|
||||
* @returns list of pairs for the same time
|
||||
*/
|
||||
export function createDenominationPairTimeline(left: FeeDescription[], right: FeeDescription[]): FeeDescriptionPair[] {
|
||||
//both list empty, discarded
|
||||
if (left.length === 0 && right.length === 0) return [];
|
||||
|
||||
const pairList: FeeDescriptionPair[] = [];
|
||||
|
||||
let li = 0;
|
||||
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;
|
||||
|
||||
let ll = 0 //left length (until next value)
|
||||
while (li + ll < left.length && Amounts.cmp(left[li + ll].value, currentValue) === 0) {
|
||||
ll++
|
||||
}
|
||||
let rl = 0 //right length (until next value)
|
||||
while (ri + rl < right.length && Amounts.cmp(right[ri + rl].value, currentValue) === 0) {
|
||||
rl++
|
||||
}
|
||||
const leftIsEmpty = ll === 0
|
||||
const rightIsEmpty = rl === 0
|
||||
//check which start after, add gap so both list starts at the same time
|
||||
// one list may be empty
|
||||
const leftStarts: AbsoluteTime =
|
||||
leftIsEmpty ? { t_ms: "never" } : left[li].from;
|
||||
const rightStarts: AbsoluteTime =
|
||||
rightIsEmpty ? { t_ms: "never" } : right[ri].from;
|
||||
|
||||
//first time cut is the smallest time
|
||||
let timeCut: AbsoluteTime = leftStarts;
|
||||
|
||||
if (AbsoluteTime.cmp(leftStarts, rightStarts) < 0) {
|
||||
const ends =
|
||||
rightIsEmpty ? left[li + ll - 1].until : right[0].from;
|
||||
|
||||
right.splice(ri, 0, {
|
||||
from: leftStarts,
|
||||
until: ends,
|
||||
value: left[li].value,
|
||||
});
|
||||
rl++;
|
||||
|
||||
timeCut = leftStarts
|
||||
}
|
||||
if (AbsoluteTime.cmp(leftStarts, rightStarts) > 0) {
|
||||
const ends =
|
||||
leftIsEmpty ? right[ri + rl - 1].until : left[0].from;
|
||||
|
||||
left.splice(li, 0, {
|
||||
from: rightStarts,
|
||||
until: ends,
|
||||
value: right[ri].value,
|
||||
});
|
||||
ll++;
|
||||
|
||||
timeCut = rightStarts
|
||||
}
|
||||
|
||||
//check which ends sooner, add gap so both list ends at the same time
|
||||
// here both list are non empty
|
||||
const leftEnds: AbsoluteTime = left[li + ll - 1].until;
|
||||
const rightEnds: AbsoluteTime = right[ri + rl - 1].until;
|
||||
|
||||
if (AbsoluteTime.cmp(leftEnds, rightEnds) > 0) {
|
||||
right.splice(ri + rl, 0, {
|
||||
from: rightEnds,
|
||||
until: leftEnds,
|
||||
value: left[0].value,
|
||||
});
|
||||
rl++;
|
||||
|
||||
}
|
||||
if (AbsoluteTime.cmp(leftEnds, rightEnds) < 0) {
|
||||
left.splice(li + ll, 0, {
|
||||
from: leftEnds,
|
||||
until: rightEnds,
|
||||
value: right[0].value,
|
||||
});
|
||||
ll++;
|
||||
}
|
||||
|
||||
//now both lists are non empty and (starts,ends) at the same time
|
||||
while (li < left.length && ri < right.length && Amounts.cmp(left[li].value, right[ri].value) === 0) {
|
||||
|
||||
if (AbsoluteTime.cmp(left[li].from, timeCut) !== 0 && AbsoluteTime.cmp(right[ri].from, timeCut) !== 0) {
|
||||
// timeCut comes from the latest "until" (expiration from the previous)
|
||||
// and this value comes from the latest left or right
|
||||
// it should be the same as the "from" from one of the latest left or right
|
||||
// otherwise it means that there is missing a gap object in the middle
|
||||
// the list is not complete and the behavior is undefined
|
||||
throw Error('one of the list is not completed: list[i].until !== list[i+1].from')
|
||||
}
|
||||
|
||||
pairList.push({
|
||||
left: left[li].fee,
|
||||
right: right[ri].fee,
|
||||
from: timeCut,
|
||||
until: AbsoluteTime.never(),
|
||||
value: currentValue,
|
||||
});
|
||||
|
||||
if (left[li].until.t_ms === right[ri].until.t_ms) {
|
||||
timeCut = left[li].until;
|
||||
ri++;
|
||||
li++;
|
||||
} else if (left[li].until.t_ms < right[ri].until.t_ms) {
|
||||
timeCut = left[li].until;
|
||||
li++;
|
||||
} else if (left[li].until.t_ms > right[ri].until.t_ms) {
|
||||
timeCut = right[ri].until;
|
||||
ri++;
|
||||
}
|
||||
pairList[pairList.length - 1].until = timeCut
|
||||
|
||||
if (li < left.length && Amounts.cmp(left[li].value, pairList[pairList.length - 1].value) !== 0) {
|
||||
//value changed, should break
|
||||
//this if will catch when both (left and right) change at the same time
|
||||
//if just one side changed it will catch in the while condition
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
//one of the list left or right can still have elements
|
||||
if (li < left.length) {
|
||||
let timeCut = pairList.length > 0 && Amounts.cmp(pairList[pairList.length - 1].value, left[li].value) === 0 ? pairList[pairList.length - 1].until : left[li].from;
|
||||
while (li < left.length) {
|
||||
pairList.push({
|
||||
left: left[li].fee,
|
||||
right: undefined,
|
||||
from: timeCut,
|
||||
until: left[li].until,
|
||||
value: left[li].value,
|
||||
})
|
||||
timeCut = left[li].until
|
||||
li++;
|
||||
}
|
||||
}
|
||||
if (ri < right.length) {
|
||||
let timeCut = pairList.length > 0 && Amounts.cmp(pairList[pairList.length - 1].value, right[ri].value) === 0 ? pairList[pairList.length - 1].until : right[ri].from;
|
||||
while (ri < right.length) {
|
||||
pairList.push({
|
||||
right: right[ri].fee,
|
||||
left: undefined,
|
||||
from: timeCut,
|
||||
until: right[ri].until,
|
||||
value: right[ri].value,
|
||||
})
|
||||
timeCut = right[ri].until
|
||||
ri++;
|
||||
}
|
||||
}
|
||||
return pairList
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a usage timeline with the denominations given.
|
||||
*
|
||||
* If there are multiple denominations that can be used, 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 feeProp property of the element of the list that will be used as fee reference
|
||||
* @returns list of @type {FeeDescription} sorted by usage period
|
||||
*/
|
||||
export function createDenominationTimeline(
|
||||
list: DenominationInfo[],
|
||||
periodProp: PropsWithReturnType<DenominationInfo, TalerProtocolTimestamp>,
|
||||
feeProp: PropsWithReturnType<DenominationInfo, AmountJson>,
|
||||
): FeeDescription[] {
|
||||
const points = 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;
|
||||
}
|
||||
ps.push({
|
||||
type: "start",
|
||||
moment: AbsoluteTime.fromTimestamp(denom.stampStart),
|
||||
denom,
|
||||
});
|
||||
ps.push({
|
||||
type: "end",
|
||||
moment: AbsoluteTime.fromTimestamp(denom[periodProp]),
|
||||
denom,
|
||||
});
|
||||
return ps;
|
||||
}, [] as TimePoint[])
|
||||
.sort((a, b) => {
|
||||
const v = Amounts.cmp(a.denom.value, b.denom.value);
|
||||
if (v != 0) return v;
|
||||
const t = AbsoluteTime.cmp(a.moment, b.moment);
|
||||
if (t != 0) return t;
|
||||
if (a.type === b.type) return 0;
|
||||
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,
|
||||
)}`,
|
||||
);
|
||||
|
||||
let prev = result.length > 0 ? result[result.length - 1] : undefined;
|
||||
const prevHasSameValue =
|
||||
prev && Amounts.cmp(prev.value, cursor.denom.value) === 0;
|
||||
if (prev) {
|
||||
if (prevHasSameValue) {
|
||||
prev.until = cursor.moment;
|
||||
|
||||
if (prev.from.t_ms === prev.until.t_ms) {
|
||||
result.pop();
|
||||
prev = result[result.length - 1];
|
||||
}
|
||||
} else {
|
||||
// the last end adds a gap that we have to remove
|
||||
result.pop();
|
||||
}
|
||||
}
|
||||
|
||||
//update the activeAtTheSameTime list
|
||||
if (cursor.type === "end") {
|
||||
const loc = activeAtTheSameTime.findIndex((v) => v.denomPubHash === hash);
|
||||
if (loc === -1) {
|
||||
throw Error(`denomination ${hash} has an end but no start`);
|
||||
}
|
||||
activeAtTheSameTime.splice(loc, 1);
|
||||
} else if (cursor.type === "start") {
|
||||
activeAtTheSameTime.push(cursor.denom);
|
||||
} else {
|
||||
const exhaustiveCheck: never = cursor.type;
|
||||
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 (cursor.type !== "end") {
|
||||
throw Error(
|
||||
`denomination ${hash} starts after ending or doesn't have an ending`,
|
||||
);
|
||||
}
|
||||
if (activeAtTheSameTime.length > 0) {
|
||||
throw Error(
|
||||
`there are ${activeAtTheSameTime.length} denominations without ending`,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const current = selectBestForOverlappingDenominations(activeAtTheSameTime);
|
||||
|
||||
if (current) {
|
||||
if (
|
||||
prev === undefined || //is the first
|
||||
!prev.fee || //is a gap
|
||||
Amounts.cmp(prev.fee, current[feeProp]) !== 0 // prev has the same fee
|
||||
) {
|
||||
result.push({
|
||||
value: cursor.denom.value,
|
||||
from: cursor.moment,
|
||||
until: AbsoluteTime.never(), //not yet known
|
||||
fee: current[feeProp],
|
||||
});
|
||||
} else {
|
||||
prev.until = cursor.moment;
|
||||
}
|
||||
} else {
|
||||
result.push({
|
||||
value: cursor.denom.value,
|
||||
from: cursor.moment,
|
||||
until: AbsoluteTime.never(), //not yet known
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [] as FeeDescription[]);
|
||||
}
|
@ -88,6 +88,8 @@ import {
|
||||
WalletNotification,
|
||||
WalletCoreVersion,
|
||||
ExchangeListItem,
|
||||
OperationMap,
|
||||
FeeDescription,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
|
||||
import {
|
||||
@ -102,6 +104,7 @@ import {
|
||||
WalletStoresV1,
|
||||
} from "./db.js";
|
||||
import { getErrorDetailFromException, TalerError } from "./errors.js";
|
||||
import { createDenominationTimeline } from "./index.browser.js";
|
||||
import {
|
||||
DenomInfo,
|
||||
ExchangeOperations,
|
||||
@ -646,24 +649,54 @@ async function getExchangeDetailedInfo(
|
||||
}
|
||||
|
||||
return {
|
||||
exchangeBaseUrl: ex.baseUrl,
|
||||
currency,
|
||||
tos: {
|
||||
acceptedVersion: exchangeDetails.termsOfServiceAcceptedEtag,
|
||||
currentVersion: exchangeDetails.termsOfServiceLastEtag,
|
||||
contentType: exchangeDetails.termsOfServiceContentType,
|
||||
content: exchangeDetails.termsOfServiceText,
|
||||
info: {
|
||||
exchangeBaseUrl: ex.baseUrl,
|
||||
currency,
|
||||
tos: {
|
||||
acceptedVersion: exchangeDetails.termsOfServiceAcceptedEtag,
|
||||
currentVersion: exchangeDetails.termsOfServiceLastEtag,
|
||||
contentType: exchangeDetails.termsOfServiceContentType,
|
||||
content: exchangeDetails.termsOfServiceText,
|
||||
},
|
||||
paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
|
||||
auditors: exchangeDetails.auditors,
|
||||
wireInfo: exchangeDetails.wireInfo,
|
||||
},
|
||||
paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
|
||||
auditors: exchangeDetails.auditors,
|
||||
wireInfo: exchangeDetails.wireInfo,
|
||||
denominations: denominations,
|
||||
}
|
||||
});
|
||||
|
||||
if (!exchange) {
|
||||
throw Error(`exchange with base url "${exchangeBaseurl}" not found`)
|
||||
}
|
||||
return exchange;
|
||||
|
||||
const feesDescription: OperationMap<FeeDescription[]> = {
|
||||
deposit: createDenominationTimeline(
|
||||
exchange.denominations,
|
||||
"stampExpireDeposit",
|
||||
"feeDeposit",
|
||||
),
|
||||
refresh: createDenominationTimeline(
|
||||
exchange.denominations,
|
||||
"stampExpireWithdraw",
|
||||
"feeRefresh",
|
||||
),
|
||||
refund: createDenominationTimeline(
|
||||
exchange.denominations,
|
||||
"stampExpireWithdraw",
|
||||
"feeRefund",
|
||||
),
|
||||
withdraw: createDenominationTimeline(
|
||||
exchange.denominations,
|
||||
"stampExpireWithdraw",
|
||||
"feeWithdraw",
|
||||
),
|
||||
};
|
||||
|
||||
return {
|
||||
...exchange.info,
|
||||
feesDescription,
|
||||
};
|
||||
}
|
||||
|
||||
async function setCoinSuspended(
|
||||
|
@ -42,4 +42,5 @@ export const Ready = createExample(ReadyView, {
|
||||
merchantBaseUrl: "http://merchant.url/",
|
||||
exchangeBaseUrl: "http://exchange.url/",
|
||||
accept: {},
|
||||
cancel: {},
|
||||
});
|
||||
|
@ -93,7 +93,7 @@ export function ReadyView(state: State.Ready): VNode {
|
||||
</Button>
|
||||
</section>
|
||||
<section>
|
||||
<Link upperCased onClick={state.cancel}>
|
||||
<Link upperCased onClick={state.cancel.onClick}>
|
||||
<i18n.Translate>Cancel</i18n.Translate>
|
||||
</Link>
|
||||
</section>
|
||||
|
@ -45,7 +45,12 @@ const exchanges: ExchangeFullDetails[] = [
|
||||
denomination_keys: [],
|
||||
},
|
||||
],
|
||||
denominations: [{} as any],
|
||||
feesDescription: {
|
||||
deposit: [],
|
||||
refresh: [],
|
||||
refund: [],
|
||||
withdraw: [],
|
||||
},
|
||||
wireInfo: {
|
||||
accounts: [],
|
||||
feesForType: {},
|
||||
|
File diff suppressed because one or more lines are too long
@ -14,7 +14,7 @@
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
import { AbsoluteTime, AmountJson, ExchangeFullDetails } from "@gnu-taler/taler-util";
|
||||
import { FeeDescription, FeeDescriptionPair, AbsoluteTime, ExchangeFullDetails, OperationMap } from "@gnu-taler/taler-util";
|
||||
import { Loading } from "../../components/Loading.js";
|
||||
import { HookError } from "../../hooks/useAsyncAsHook.js";
|
||||
import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js";
|
||||
@ -53,7 +53,6 @@ export namespace State {
|
||||
export interface BaseInfo {
|
||||
exchanges: SelectFieldHandler;
|
||||
selected: ExchangeFullDetails;
|
||||
nextFeeUpdate: AbsoluteTime;
|
||||
error: undefined;
|
||||
}
|
||||
|
||||
@ -76,9 +75,6 @@ export namespace State {
|
||||
}
|
||||
}
|
||||
|
||||
export type Operation = "deposit" | "withdraw" | "refresh" | "refund";
|
||||
export type OperationMap<T> = { [op in Operation]: T };
|
||||
|
||||
|
||||
const viewMapping: StateViewMap<State> = {
|
||||
loading: Loading,
|
||||
@ -89,17 +85,3 @@ const viewMapping: StateViewMap<State> = {
|
||||
};
|
||||
|
||||
export const ExchangeSelectionPage = compose("ExchangeSelectionPage", (p: Props) => useComponentState(p, wxApi), viewMapping)
|
||||
|
||||
export interface FeeDescription {
|
||||
value: AmountJson;
|
||||
from: AbsoluteTime;
|
||||
until: AbsoluteTime;
|
||||
fee?: AmountJson;
|
||||
}
|
||||
export interface FeeDescriptionPair {
|
||||
value: AmountJson;
|
||||
from: AbsoluteTime;
|
||||
until: AbsoluteTime;
|
||||
left?: AmountJson;
|
||||
right?: AmountJson;
|
||||
}
|
||||
|
@ -15,11 +15,12 @@
|
||||
*/
|
||||
|
||||
|
||||
import { AbsoluteTime, AmountJson, Amounts, DenominationInfo, TalerProtocolTimestamp } from "@gnu-taler/taler-util";
|
||||
import { FeeDescription, OperationMap } from "@gnu-taler/taler-util";
|
||||
import { createDenominationPairTimeline } from "@gnu-taler/taler-wallet-core";
|
||||
import { useState } from "preact/hooks";
|
||||
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
|
||||
import * as wxApi from "../../wxApi.js";
|
||||
import { FeeDescription, FeeDescriptionPair, OperationMap, Props, State } from "./index.js";
|
||||
import { Props, State } from "./index.js";
|
||||
|
||||
export function useComponentState(
|
||||
{ onCancel, onSelection, currency }: Props,
|
||||
@ -63,46 +64,6 @@ export function useComponentState(
|
||||
}
|
||||
}
|
||||
|
||||
let nextFeeUpdate = TalerProtocolTimestamp.never();
|
||||
|
||||
nextFeeUpdate = Object.values(selected.wireInfo.feesForType).reduce(
|
||||
(prev, cur) => {
|
||||
return cur.reduce((p, c) => nearestTimestamp(p, c.endStamp), prev);
|
||||
},
|
||||
nextFeeUpdate,
|
||||
);
|
||||
|
||||
nextFeeUpdate = selected.denominations.reduce((prev, cur) => {
|
||||
return [
|
||||
cur.stampExpireWithdraw,
|
||||
cur.stampExpireLegal,
|
||||
cur.stampExpireDeposit,
|
||||
].reduce(nearestTimestamp, prev);
|
||||
}, nextFeeUpdate);
|
||||
|
||||
const timeline: OperationMap<FeeDescription[]> = {
|
||||
deposit: createDenominationTimeline(
|
||||
selected.denominations,
|
||||
"stampExpireDeposit",
|
||||
"feeDeposit",
|
||||
),
|
||||
refresh: createDenominationTimeline(
|
||||
selected.denominations,
|
||||
"stampExpireWithdraw",
|
||||
"feeRefresh",
|
||||
),
|
||||
refund: createDenominationTimeline(
|
||||
selected.denominations,
|
||||
"stampExpireWithdraw",
|
||||
"feeRefund",
|
||||
),
|
||||
withdraw: createDenominationTimeline(
|
||||
selected.denominations,
|
||||
"stampExpireWithdraw",
|
||||
"feeWithdraw",
|
||||
),
|
||||
};
|
||||
|
||||
const exchangeMap = exchanges.reduce((prev, cur, idx) => ({ ...prev, [cur.exchangeBaseUrl]: String(idx) }), {} as Record<string, string>)
|
||||
|
||||
if (!original) {
|
||||
@ -117,42 +78,19 @@ export function useComponentState(
|
||||
}
|
||||
},
|
||||
error: undefined,
|
||||
nextFeeUpdate: AbsoluteTime.fromTimestamp(nextFeeUpdate),
|
||||
onClose: {
|
||||
onClick: onCancel
|
||||
},
|
||||
selected,
|
||||
timeline
|
||||
timeline: selected.feesDescription
|
||||
}
|
||||
}
|
||||
|
||||
const originalTimeline: OperationMap<FeeDescription[]> = {
|
||||
deposit: createDenominationTimeline(
|
||||
original.denominations,
|
||||
"stampExpireDeposit",
|
||||
"feeDeposit",
|
||||
),
|
||||
refresh: createDenominationTimeline(
|
||||
original.denominations,
|
||||
"stampExpireWithdraw",
|
||||
"feeRefresh",
|
||||
),
|
||||
refund: createDenominationTimeline(
|
||||
original.denominations,
|
||||
"stampExpireWithdraw",
|
||||
"feeRefund",
|
||||
),
|
||||
withdraw: createDenominationTimeline(
|
||||
original.denominations,
|
||||
"stampExpireWithdraw",
|
||||
"feeWithdraw",
|
||||
),
|
||||
};
|
||||
const pairTimeline: OperationMap<FeeDescription[]> = {
|
||||
deposit: createDenominationPairTimeline(timeline.deposit, originalTimeline.deposit),
|
||||
refresh: createDenominationPairTimeline(timeline.refresh, originalTimeline.refresh),
|
||||
refund: createDenominationPairTimeline(timeline.refund, originalTimeline.refund),
|
||||
withdraw: createDenominationPairTimeline(timeline.withdraw, originalTimeline.withdraw),
|
||||
deposit: createDenominationPairTimeline(selected.feesDescription.deposit, original.feesDescription.deposit),
|
||||
refresh: createDenominationPairTimeline(selected.feesDescription.refresh, original.feesDescription.refresh),
|
||||
refund: createDenominationPairTimeline(selected.feesDescription.refund, original.feesDescription.refund),
|
||||
withdraw: createDenominationPairTimeline(selected.feesDescription.withdraw, original.feesDescription.withdraw),
|
||||
}
|
||||
|
||||
return {
|
||||
@ -165,7 +103,6 @@ export function useComponentState(
|
||||
}
|
||||
},
|
||||
error: undefined,
|
||||
nextFeeUpdate: AbsoluteTime.fromTimestamp(nextFeeUpdate),
|
||||
onReset: {
|
||||
onClick: async () => {
|
||||
setValue(String(initialValue))
|
||||
@ -182,351 +119,3 @@ export function useComponentState(
|
||||
|
||||
}
|
||||
|
||||
function nearestTimestamp(
|
||||
first: TalerProtocolTimestamp,
|
||||
second: TalerProtocolTimestamp,
|
||||
): TalerProtocolTimestamp {
|
||||
const f = AbsoluteTime.fromTimestamp(first);
|
||||
const s = AbsoluteTime.fromTimestamp(second);
|
||||
const a = AbsoluteTime.min(f, s);
|
||||
return AbsoluteTime.toTimestamp(a);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface TimePoint {
|
||||
type: "start" | "end";
|
||||
moment: AbsoluteTime;
|
||||
denom: DenominationInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of denominations with the same value and same period of time:
|
||||
* return the one that will be used.
|
||||
* The best denomination is the one that will minimize the fee cost.
|
||||
*
|
||||
* @param list denominations of same value
|
||||
* @returns
|
||||
*/
|
||||
function selectBestForOverlappingDenominations(
|
||||
list: DenominationInfo[],
|
||||
): DenominationInfo | undefined {
|
||||
let minDeposit: DenominationInfo | undefined = undefined;
|
||||
list.forEach((e) => {
|
||||
if (minDeposit === undefined) {
|
||||
minDeposit = e;
|
||||
return;
|
||||
}
|
||||
if (Amounts.cmp(minDeposit.feeDeposit, e.feeDeposit) > -1) {
|
||||
minDeposit = e;
|
||||
}
|
||||
});
|
||||
return minDeposit;
|
||||
}
|
||||
|
||||
type PropsWithReturnType<T extends object, F> = Exclude<
|
||||
{
|
||||
[K in keyof T]: T[K] extends F ? K : never;
|
||||
}[keyof T],
|
||||
undefined
|
||||
>;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @see {createDenominationTimeline}
|
||||
*
|
||||
* @param left list denominations @type {FeeDescription}
|
||||
* @param right list denominations @type {FeeDescription}
|
||||
* @returns list of pairs for the same time
|
||||
*/
|
||||
export function createDenominationPairTimeline(left: FeeDescription[], right: FeeDescription[]): FeeDescriptionPair[] {
|
||||
//both list empty, discarded
|
||||
if (left.length === 0 && right.length === 0) return [];
|
||||
|
||||
const pairList: FeeDescriptionPair[] = [];
|
||||
|
||||
let li = 0;
|
||||
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;
|
||||
|
||||
let ll = 0 //left length (until next value)
|
||||
while (li + ll < left.length && Amounts.cmp(left[li + ll].value, currentValue) === 0) {
|
||||
ll++
|
||||
}
|
||||
let rl = 0 //right length (until next value)
|
||||
while (ri + rl < right.length && Amounts.cmp(right[ri + rl].value, currentValue) === 0) {
|
||||
rl++
|
||||
}
|
||||
const leftIsEmpty = ll === 0
|
||||
const rightIsEmpty = rl === 0
|
||||
//check which start after, add gap so both list starts at the same time
|
||||
// one list may be empty
|
||||
const leftStarts: AbsoluteTime =
|
||||
leftIsEmpty ? { t_ms: "never" } : left[li].from;
|
||||
const rightStarts: AbsoluteTime =
|
||||
rightIsEmpty ? { t_ms: "never" } : right[ri].from;
|
||||
|
||||
//first time cut is the smallest time
|
||||
let timeCut: AbsoluteTime = leftStarts;
|
||||
|
||||
if (AbsoluteTime.cmp(leftStarts, rightStarts) < 0) {
|
||||
const ends =
|
||||
rightIsEmpty ? left[li + ll - 1].until : right[0].from;
|
||||
|
||||
right.splice(ri, 0, {
|
||||
from: leftStarts,
|
||||
until: ends,
|
||||
value: left[li].value,
|
||||
});
|
||||
rl++;
|
||||
|
||||
timeCut = leftStarts
|
||||
}
|
||||
if (AbsoluteTime.cmp(leftStarts, rightStarts) > 0) {
|
||||
const ends =
|
||||
leftIsEmpty ? right[ri + rl - 1].until : left[0].from;
|
||||
|
||||
left.splice(li, 0, {
|
||||
from: rightStarts,
|
||||
until: ends,
|
||||
value: right[ri].value,
|
||||
});
|
||||
ll++;
|
||||
|
||||
timeCut = rightStarts
|
||||
}
|
||||
|
||||
//check which ends sooner, add gap so both list ends at the same time
|
||||
// here both list are non empty
|
||||
const leftEnds: AbsoluteTime = left[li + ll - 1].until;
|
||||
const rightEnds: AbsoluteTime = right[ri + rl - 1].until;
|
||||
|
||||
if (AbsoluteTime.cmp(leftEnds, rightEnds) > 0) {
|
||||
right.splice(ri + rl, 0, {
|
||||
from: rightEnds,
|
||||
until: leftEnds,
|
||||
value: left[0].value,
|
||||
});
|
||||
rl++;
|
||||
|
||||
}
|
||||
if (AbsoluteTime.cmp(leftEnds, rightEnds) < 0) {
|
||||
left.splice(li + ll, 0, {
|
||||
from: leftEnds,
|
||||
until: rightEnds,
|
||||
value: right[0].value,
|
||||
});
|
||||
ll++;
|
||||
}
|
||||
|
||||
//now both lists are non empty and (starts,ends) at the same time
|
||||
while (li < left.length && ri < right.length && Amounts.cmp(left[li].value, right[ri].value) === 0) {
|
||||
|
||||
if (AbsoluteTime.cmp(left[li].from, timeCut) !== 0 && AbsoluteTime.cmp(right[ri].from, timeCut) !== 0) {
|
||||
// timeCut comes from the latest "until" (expiration from the previous)
|
||||
// and this value comes from the latest left or right
|
||||
// it should be the same as the "from" from one of the latest left or right
|
||||
// otherwise it means that there is missing a gap object in the middle
|
||||
// the list is not complete and the behavior is undefined
|
||||
throw Error('one of the list is not completed: list[i].until !== list[i+1].from')
|
||||
}
|
||||
|
||||
pairList.push({
|
||||
left: left[li].fee,
|
||||
right: right[ri].fee,
|
||||
from: timeCut,
|
||||
until: AbsoluteTime.never(),
|
||||
value: currentValue,
|
||||
});
|
||||
|
||||
if (left[li].until.t_ms === right[ri].until.t_ms) {
|
||||
timeCut = left[li].until;
|
||||
ri++;
|
||||
li++;
|
||||
} else if (left[li].until.t_ms < right[ri].until.t_ms) {
|
||||
timeCut = left[li].until;
|
||||
li++;
|
||||
} else if (left[li].until.t_ms > right[ri].until.t_ms) {
|
||||
timeCut = right[ri].until;
|
||||
ri++;
|
||||
}
|
||||
pairList[pairList.length - 1].until = timeCut
|
||||
|
||||
if (li < left.length && Amounts.cmp(left[li].value, pairList[pairList.length - 1].value) !== 0) {
|
||||
//value changed, should break
|
||||
//this if will catch when both (left and right) change at the same time
|
||||
//if just one side changed it will catch in the while condition
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
//one of the list left or right can still have elements
|
||||
if (li < left.length) {
|
||||
let timeCut = pairList.length > 0 && Amounts.cmp(pairList[pairList.length - 1].value, left[li].value) === 0 ? pairList[pairList.length - 1].until : left[li].from;
|
||||
while (li < left.length) {
|
||||
pairList.push({
|
||||
left: left[li].fee,
|
||||
right: undefined,
|
||||
from: timeCut,
|
||||
until: left[li].until,
|
||||
value: left[li].value,
|
||||
})
|
||||
timeCut = left[li].until
|
||||
li++;
|
||||
}
|
||||
}
|
||||
if (ri < right.length) {
|
||||
let timeCut = pairList.length > 0 && Amounts.cmp(pairList[pairList.length - 1].value, right[ri].value) === 0 ? pairList[pairList.length - 1].until : right[ri].from;
|
||||
while (ri < right.length) {
|
||||
pairList.push({
|
||||
right: right[ri].fee,
|
||||
left: undefined,
|
||||
from: timeCut,
|
||||
until: right[ri].until,
|
||||
value: right[ri].value,
|
||||
})
|
||||
timeCut = right[ri].until
|
||||
ri++;
|
||||
}
|
||||
}
|
||||
return pairList
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a usage timeline with the denominations given.
|
||||
*
|
||||
* If there are multiple denominations that can be used, 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 feeProp property of the element of the list that will be used as fee reference
|
||||
* @returns list of @type {FeeDescription} sorted by usage period
|
||||
*/
|
||||
export function createDenominationTimeline(
|
||||
list: DenominationInfo[],
|
||||
periodProp: PropsWithReturnType<DenominationInfo, TalerProtocolTimestamp>,
|
||||
feeProp: PropsWithReturnType<DenominationInfo, AmountJson>,
|
||||
): FeeDescription[] {
|
||||
const points = 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;
|
||||
}
|
||||
ps.push({
|
||||
type: "start",
|
||||
moment: AbsoluteTime.fromTimestamp(denom.stampStart),
|
||||
denom,
|
||||
});
|
||||
ps.push({
|
||||
type: "end",
|
||||
moment: AbsoluteTime.fromTimestamp(denom[periodProp]),
|
||||
denom,
|
||||
});
|
||||
return ps;
|
||||
}, [] as TimePoint[])
|
||||
.sort((a, b) => {
|
||||
const v = Amounts.cmp(a.denom.value, b.denom.value);
|
||||
if (v != 0) return v;
|
||||
const t = AbsoluteTime.cmp(a.moment, b.moment);
|
||||
if (t != 0) return t;
|
||||
if (a.type === b.type) return 0;
|
||||
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,
|
||||
)}`,
|
||||
);
|
||||
|
||||
let prev = result.length > 0 ? result[result.length - 1] : undefined;
|
||||
const prevHasSameValue =
|
||||
prev && Amounts.cmp(prev.value, cursor.denom.value) === 0;
|
||||
if (prev) {
|
||||
if (prevHasSameValue) {
|
||||
prev.until = cursor.moment;
|
||||
|
||||
if (prev.from.t_ms === prev.until.t_ms) {
|
||||
result.pop();
|
||||
prev = result[result.length - 1];
|
||||
}
|
||||
} else {
|
||||
// the last end adds a gap that we have to remove
|
||||
result.pop();
|
||||
}
|
||||
}
|
||||
|
||||
//update the activeAtTheSameTime list
|
||||
if (cursor.type === "end") {
|
||||
const loc = activeAtTheSameTime.findIndex((v) => v.denomPubHash === hash);
|
||||
if (loc === -1) {
|
||||
throw Error(`denomination ${hash} has an end but no start`);
|
||||
}
|
||||
activeAtTheSameTime.splice(loc, 1);
|
||||
} else if (cursor.type === "start") {
|
||||
activeAtTheSameTime.push(cursor.denom);
|
||||
} else {
|
||||
const exhaustiveCheck: never = cursor.type;
|
||||
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 (cursor.type !== "end") {
|
||||
throw Error(
|
||||
`denomination ${hash} starts after ending or doesn't have an ending`,
|
||||
);
|
||||
}
|
||||
if (activeAtTheSameTime.length > 0) {
|
||||
throw Error(
|
||||
`there are ${activeAtTheSameTime.length} denominations without ending`,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const current = selectBestForOverlappingDenominations(activeAtTheSameTime);
|
||||
|
||||
if (current) {
|
||||
if (
|
||||
prev === undefined || //is the first
|
||||
!prev.fee || //is a gap
|
||||
Amounts.cmp(prev.fee, current[feeProp]) !== 0 // prev has the same fee
|
||||
) {
|
||||
result.push({
|
||||
value: cursor.denom.value,
|
||||
from: cursor.moment,
|
||||
until: AbsoluteTime.never(), //not yet known
|
||||
fee: current[feeProp],
|
||||
});
|
||||
} else {
|
||||
prev.until = cursor.moment;
|
||||
}
|
||||
} else {
|
||||
result.push({
|
||||
value: cursor.denom.value,
|
||||
from: cursor.moment,
|
||||
until: AbsoluteTime.never(), //not yet known
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [] as FeeDescription[]);
|
||||
}
|
||||
|
@ -19,61 +19,13 @@
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ExchangeFullDetails, ExchangeListItem } from "@gnu-taler/taler-util";
|
||||
import { createExample } from "../../test-utils.js";
|
||||
import { bitcoinExchanges, kudosExchanges } from "./example.js";
|
||||
import { FeeDescription, FeeDescriptionPair, OperationMap } from "./index.js";
|
||||
import {
|
||||
createDenominationPairTimeline,
|
||||
createDenominationTimeline,
|
||||
} from "./state.js";
|
||||
import { ReadyView, ComparingView } from "./views.js";
|
||||
import { ComparingView, ReadyView } from "./views.js";
|
||||
|
||||
export default {
|
||||
title: "wallet/select exchange",
|
||||
};
|
||||
|
||||
function timelineForExchange(
|
||||
ex: ExchangeFullDetails,
|
||||
): OperationMap<FeeDescription[]> {
|
||||
return {
|
||||
deposit: createDenominationTimeline(
|
||||
ex.denominations,
|
||||
"stampExpireDeposit",
|
||||
"feeDeposit",
|
||||
),
|
||||
refresh: createDenominationTimeline(
|
||||
ex.denominations,
|
||||
"stampExpireWithdraw",
|
||||
"feeRefresh",
|
||||
),
|
||||
refund: createDenominationTimeline(
|
||||
ex.denominations,
|
||||
"stampExpireWithdraw",
|
||||
"feeRefund",
|
||||
),
|
||||
withdraw: createDenominationTimeline(
|
||||
ex.denominations,
|
||||
"stampExpireWithdraw",
|
||||
"feeWithdraw",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function timelinePairForExchange(
|
||||
ex1: ExchangeFullDetails,
|
||||
ex2: ExchangeFullDetails,
|
||||
): OperationMap<FeeDescriptionPair[]> {
|
||||
const om1 = timelineForExchange(ex1);
|
||||
const om2 = timelineForExchange(ex2);
|
||||
return {
|
||||
deposit: createDenominationPairTimeline(om1.deposit, om2.deposit),
|
||||
refresh: createDenominationPairTimeline(om1.refresh, om2.refresh),
|
||||
refund: createDenominationPairTimeline(om1.refund, om2.refund),
|
||||
withdraw: createDenominationPairTimeline(om1.withdraw, om2.withdraw),
|
||||
};
|
||||
}
|
||||
|
||||
export const Bitcoin1 = createExample(ReadyView, {
|
||||
exchanges: {
|
||||
list: { "http://exchange": "http://exchange" },
|
||||
@ -84,10 +36,12 @@ export const Bitcoin1 = createExample(ReadyView, {
|
||||
auditors: [],
|
||||
} as any,
|
||||
onClose: {},
|
||||
nextFeeUpdate: {
|
||||
t_ms: 1,
|
||||
timeline: {
|
||||
deposit: [],
|
||||
refresh: [],
|
||||
refund: [],
|
||||
withdraw: [],
|
||||
},
|
||||
timeline: timelineForExchange(bitcoinExchanges[0]),
|
||||
});
|
||||
export const Bitcoin2 = createExample(ReadyView, {
|
||||
exchanges: {
|
||||
@ -99,10 +53,12 @@ export const Bitcoin2 = createExample(ReadyView, {
|
||||
auditors: [],
|
||||
} as any,
|
||||
onClose: {},
|
||||
nextFeeUpdate: {
|
||||
t_ms: 1,
|
||||
timeline: {
|
||||
deposit: [],
|
||||
refresh: [],
|
||||
refund: [],
|
||||
withdraw: [],
|
||||
},
|
||||
timeline: timelineForExchange(bitcoinExchanges[1]),
|
||||
});
|
||||
export const Kudos1 = createExample(ReadyView, {
|
||||
exchanges: {
|
||||
@ -114,10 +70,12 @@ export const Kudos1 = createExample(ReadyView, {
|
||||
auditors: [],
|
||||
} as any,
|
||||
onClose: {},
|
||||
nextFeeUpdate: {
|
||||
t_ms: 1,
|
||||
timeline: {
|
||||
deposit: [],
|
||||
refresh: [],
|
||||
refund: [],
|
||||
withdraw: [],
|
||||
},
|
||||
timeline: timelineForExchange(kudosExchanges[0]),
|
||||
});
|
||||
export const Kudos2 = createExample(ReadyView, {
|
||||
exchanges: {
|
||||
@ -129,10 +87,12 @@ export const Kudos2 = createExample(ReadyView, {
|
||||
auditors: [],
|
||||
} as any,
|
||||
onClose: {},
|
||||
nextFeeUpdate: {
|
||||
t_ms: 1,
|
||||
timeline: {
|
||||
deposit: [],
|
||||
refresh: [],
|
||||
refund: [],
|
||||
withdraw: [],
|
||||
},
|
||||
timeline: timelineForExchange(kudosExchanges[1]),
|
||||
});
|
||||
export const ComparingBitcoin = createExample(ComparingView, {
|
||||
exchanges: {
|
||||
@ -146,13 +106,12 @@ export const ComparingBitcoin = createExample(ComparingView, {
|
||||
onReset: {},
|
||||
onSelect: {},
|
||||
error: undefined,
|
||||
nextFeeUpdate: {
|
||||
t_ms: 1,
|
||||
pairTimeline: {
|
||||
deposit: [],
|
||||
refresh: [],
|
||||
refund: [],
|
||||
withdraw: [],
|
||||
},
|
||||
pairTimeline: timelinePairForExchange(
|
||||
bitcoinExchanges[0],
|
||||
bitcoinExchanges[1],
|
||||
),
|
||||
});
|
||||
export const ComparingKudos = createExample(ComparingView, {
|
||||
exchanges: {
|
||||
@ -166,8 +125,10 @@ export const ComparingKudos = createExample(ComparingView, {
|
||||
onReset: {},
|
||||
onSelect: {},
|
||||
error: undefined,
|
||||
nextFeeUpdate: {
|
||||
t_ms: 1,
|
||||
pairTimeline: {
|
||||
deposit: [],
|
||||
refresh: [],
|
||||
refund: [],
|
||||
withdraw: [],
|
||||
},
|
||||
pairTimeline: timelinePairForExchange(kudosExchanges[0], kudosExchanges[1]),
|
||||
});
|
||||
|
@ -24,675 +24,3 @@ import {
|
||||
Amounts, DenominationInfo
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { expect } from "chai";
|
||||
import { bitcoinExchanges } from "./example.js";
|
||||
import { FeeDescription, FeeDescriptionPair } from "./index.js";
|
||||
import { createDenominationPairTimeline, createDenominationTimeline } from "./state.js";
|
||||
|
||||
const values = Array.from({ length: 10 }).map((undef, t) => Amounts.parseOrThrow(`USD:${t}`))
|
||||
const timestamps = Array.from({ length: 20 }).map((undef, t_s) => ({ t_s }))
|
||||
const moments = timestamps.map(m => AbsoluteTime.fromTimestamp(m))
|
||||
|
||||
function normalize(list: DenominationInfo[]): DenominationInfo[] {
|
||||
return list.map((e, idx) => ({ ...e, denomPubHash: `id${idx}` }))
|
||||
}
|
||||
|
||||
describe("Denomination timeline creation", () => {
|
||||
describe("single value example", () => {
|
||||
|
||||
it("should have one row with start and exp", () => {
|
||||
|
||||
const timeline = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[1],
|
||||
stampExpireDeposit: timestamps[2],
|
||||
feeDeposit: values[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
|
||||
expect(timeline).deep.equal([{
|
||||
value: values[1],
|
||||
from: moments[1],
|
||||
until: moments[2],
|
||||
fee: values[1],
|
||||
} as FeeDescription])
|
||||
});
|
||||
|
||||
it("should have two rows with the second denom in the middle if second is better", () => {
|
||||
const timeline = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[1],
|
||||
stampExpireDeposit: timestamps[3],
|
||||
feeDeposit: values[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[2],
|
||||
stampExpireDeposit: timestamps[4],
|
||||
feeDeposit: values[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
|
||||
expect(timeline).deep.equal([{
|
||||
value: values[1],
|
||||
from: moments[1],
|
||||
until: moments[3],
|
||||
fee: values[1],
|
||||
}, {
|
||||
value: values[1],
|
||||
from: moments[3],
|
||||
until: moments[4],
|
||||
fee: values[2],
|
||||
}] as FeeDescription[])
|
||||
|
||||
});
|
||||
|
||||
it("should have two rows with the first denom in the middle if second is worse", () => {
|
||||
const timeline = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[1],
|
||||
stampExpireDeposit: timestamps[3],
|
||||
feeDeposit: values[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[2],
|
||||
stampExpireDeposit: timestamps[4],
|
||||
feeDeposit: values[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
|
||||
expect(timeline).deep.equal([{
|
||||
value: values[1],
|
||||
from: moments[1],
|
||||
until: moments[2],
|
||||
fee: values[2],
|
||||
}, {
|
||||
value: values[1],
|
||||
from: moments[2],
|
||||
until: moments[4],
|
||||
fee: values[1],
|
||||
}] as FeeDescription[])
|
||||
|
||||
});
|
||||
|
||||
it("should add a gap when there no fee", () => {
|
||||
const timeline = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[1],
|
||||
stampExpireDeposit: timestamps[2],
|
||||
feeDeposit: values[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[3],
|
||||
stampExpireDeposit: timestamps[4],
|
||||
feeDeposit: values[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
|
||||
expect(timeline).deep.equal([{
|
||||
value: values[1],
|
||||
from: moments[1],
|
||||
until: moments[2],
|
||||
fee: values[2],
|
||||
}, {
|
||||
value: values[1],
|
||||
from: moments[2],
|
||||
until: moments[3],
|
||||
|
||||
}, {
|
||||
value: values[1],
|
||||
from: moments[3],
|
||||
until: moments[4],
|
||||
fee: values[1],
|
||||
}] as FeeDescription[])
|
||||
|
||||
});
|
||||
|
||||
it("should have three rows when first denom is between second and second is worse", () => {
|
||||
const timeline = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[2],
|
||||
stampExpireDeposit: timestamps[3],
|
||||
feeDeposit: values[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[1],
|
||||
stampExpireDeposit: timestamps[4],
|
||||
feeDeposit: values[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
expect(timeline).deep.equal([{
|
||||
value: values[1],
|
||||
from: moments[1],
|
||||
until: moments[2],
|
||||
fee: values[2],
|
||||
}, {
|
||||
value: values[1],
|
||||
from: moments[2],
|
||||
until: moments[3],
|
||||
fee: values[1],
|
||||
}, {
|
||||
value: values[1],
|
||||
from: moments[3],
|
||||
until: moments[4],
|
||||
fee: values[2],
|
||||
}] as FeeDescription[])
|
||||
|
||||
});
|
||||
|
||||
it("should have one row when first denom is between second and second is better", () => {
|
||||
const timeline = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[2],
|
||||
stampExpireDeposit: timestamps[3],
|
||||
feeDeposit: values[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[1],
|
||||
stampExpireDeposit: timestamps[4],
|
||||
feeDeposit: values[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
|
||||
expect(timeline).deep.equal([{
|
||||
value: values[1],
|
||||
from: moments[1],
|
||||
until: moments[4],
|
||||
fee: values[1],
|
||||
}] as FeeDescription[])
|
||||
|
||||
});
|
||||
|
||||
it("should only add the best1", () => {
|
||||
const timeline = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[1],
|
||||
stampExpireDeposit: timestamps[3],
|
||||
feeDeposit: values[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[2],
|
||||
stampExpireDeposit: timestamps[4],
|
||||
feeDeposit: values[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[2],
|
||||
stampExpireDeposit: timestamps[4],
|
||||
feeDeposit: values[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
|
||||
expect(timeline).deep.equal([{
|
||||
value: values[1],
|
||||
from: moments[1],
|
||||
until: moments[2],
|
||||
fee: values[2],
|
||||
}, {
|
||||
value: values[1],
|
||||
from: moments[2],
|
||||
until: moments[4],
|
||||
fee: values[1],
|
||||
}] as FeeDescription[])
|
||||
|
||||
});
|
||||
|
||||
it("should only add the best2", () => {
|
||||
const timeline = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[1],
|
||||
stampExpireDeposit: timestamps[3],
|
||||
feeDeposit: values[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[2],
|
||||
stampExpireDeposit: timestamps[5],
|
||||
feeDeposit: values[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[2],
|
||||
stampExpireDeposit: timestamps[4],
|
||||
feeDeposit: values[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[5],
|
||||
stampExpireDeposit: timestamps[6],
|
||||
feeDeposit: values[3]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
|
||||
expect(timeline).deep.equal([{
|
||||
value: values[1],
|
||||
from: moments[1],
|
||||
until: moments[2],
|
||||
fee: values[2],
|
||||
}, {
|
||||
value: values[1],
|
||||
from: moments[2],
|
||||
until: moments[5],
|
||||
fee: values[1],
|
||||
}, {
|
||||
value: values[1],
|
||||
from: moments[5],
|
||||
until: moments[6],
|
||||
fee: values[3],
|
||||
}] as FeeDescription[])
|
||||
|
||||
});
|
||||
|
||||
it("should only add the best3", () => {
|
||||
const timeline = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[2],
|
||||
stampExpireDeposit: timestamps[5],
|
||||
feeDeposit: values[3]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[2],
|
||||
stampExpireDeposit: timestamps[5],
|
||||
feeDeposit: values[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[2],
|
||||
stampExpireDeposit: timestamps[5],
|
||||
feeDeposit: values[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
|
||||
expect(timeline).deep.equal([{
|
||||
value: values[1],
|
||||
from: moments[2],
|
||||
until: moments[5],
|
||||
fee: values[1],
|
||||
}] as FeeDescription[])
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
describe("multiple value example", () => {
|
||||
|
||||
//TODO: test the same start but different value
|
||||
|
||||
it("should not merge when there is different value", () => {
|
||||
const timeline = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[1],
|
||||
stampExpireDeposit: timestamps[3],
|
||||
feeDeposit: values[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: values[2],
|
||||
stampStart: timestamps[2],
|
||||
stampExpireDeposit: timestamps[4],
|
||||
feeDeposit: values[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
|
||||
expect(timeline).deep.equal([{
|
||||
value: values[1],
|
||||
from: moments[1],
|
||||
until: moments[3],
|
||||
fee: values[1],
|
||||
}, {
|
||||
value: values[2],
|
||||
from: moments[2],
|
||||
until: moments[4],
|
||||
fee: values[2],
|
||||
}] as FeeDescription[])
|
||||
|
||||
});
|
||||
|
||||
it("should not merge when there is different value (with duplicates)", () => {
|
||||
const timeline = createDenominationTimeline(normalize([
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[1],
|
||||
stampExpireDeposit: timestamps[3],
|
||||
feeDeposit: values[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: values[2],
|
||||
stampStart: timestamps[2],
|
||||
stampExpireDeposit: timestamps[4],
|
||||
feeDeposit: values[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: values[1],
|
||||
stampStart: timestamps[1],
|
||||
stampExpireDeposit: timestamps[3],
|
||||
feeDeposit: values[1]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
{
|
||||
value: values[2],
|
||||
stampStart: timestamps[2],
|
||||
stampExpireDeposit: timestamps[4],
|
||||
feeDeposit: values[2]
|
||||
} as Partial<DenominationInfo> as DenominationInfo,
|
||||
]), "stampExpireDeposit", "feeDeposit");
|
||||
|
||||
expect(timeline).deep.equal([{
|
||||
value: values[1],
|
||||
from: moments[1],
|
||||
until: moments[3],
|
||||
fee: values[1],
|
||||
}, {
|
||||
value: values[2],
|
||||
from: moments[2],
|
||||
until: moments[4],
|
||||
fee: values[2],
|
||||
}] as FeeDescription[])
|
||||
|
||||
});
|
||||
|
||||
it.skip("real world example: bitcoin exchange", () => {
|
||||
const timeline = createDenominationTimeline(
|
||||
bitcoinExchanges[0].denominations.filter(d => Amounts.cmp(d.value, Amounts.parseOrThrow('BITCOINBTC:0.01048576'))),
|
||||
"stampExpireDeposit", "feeDeposit");
|
||||
|
||||
expect(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", () => {
|
||||
|
||||
describe("single value example", () => {
|
||||
|
||||
it("should return empty", () => {
|
||||
|
||||
const left = [] as FeeDescription[];
|
||||
const right = [] as FeeDescription[];
|
||||
|
||||
const pairs = createDenominationPairTimeline(left, right)
|
||||
|
||||
expect(pairs).deep.equals([])
|
||||
});
|
||||
|
||||
it("should return first element", () => {
|
||||
|
||||
const left = [{
|
||||
value: values[1],
|
||||
from: moments[1],
|
||||
until: moments[3],
|
||||
fee: values[1],
|
||||
}] as FeeDescription[];
|
||||
|
||||
const right = [] as FeeDescription[];
|
||||
|
||||
{
|
||||
const pairs = createDenominationPairTimeline(left, right)
|
||||
expect(pairs).deep.equals([{
|
||||
from: moments[1],
|
||||
until: moments[3],
|
||||
value: values[1],
|
||||
left: values[1],
|
||||
right: undefined,
|
||||
}] as FeeDescriptionPair[])
|
||||
}
|
||||
{
|
||||
const pairs = createDenominationPairTimeline(right, left)
|
||||
expect(pairs).deep.equals([{
|
||||
from: moments[1],
|
||||
until: moments[3],
|
||||
value: values[1],
|
||||
right: values[1],
|
||||
left: undefined,
|
||||
}] as FeeDescriptionPair[])
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
it("should add both to the same row", () => {
|
||||
|
||||
const left = [{
|
||||
value: values[1],
|
||||
from: moments[1],
|
||||
until: moments[3],
|
||||
fee: values[1],
|
||||
}] as FeeDescription[];
|
||||
|
||||
const right = [{
|
||||
value: values[1],
|
||||
from: moments[1],
|
||||
until: moments[3],
|
||||
fee: values[2],
|
||||
}] as FeeDescription[];
|
||||
|
||||
{
|
||||
const pairs = createDenominationPairTimeline(left, right)
|
||||
expect(pairs).deep.equals([{
|
||||
from: moments[1],
|
||||
until: moments[3],
|
||||
value: values[1],
|
||||
left: values[1],
|
||||
right: values[2],
|
||||
}] as FeeDescriptionPair[])
|
||||
}
|
||||
{
|
||||
const pairs = createDenominationPairTimeline(right, left)
|
||||
expect(pairs).deep.equals([{
|
||||
from: moments[1],
|
||||
until: moments[3],
|
||||
value: values[1],
|
||||
left: values[2],
|
||||
right: values[1],
|
||||
}] as FeeDescriptionPair[])
|
||||
}
|
||||
});
|
||||
|
||||
it("should repeat the first and change the second", () => {
|
||||
|
||||
const left = [{
|
||||
value: values[1],
|
||||
from: moments[1],
|
||||
until: moments[5],
|
||||
fee: values[1],
|
||||
}] as FeeDescription[];
|
||||
|
||||
const right = [{
|
||||
value: values[1],
|
||||
from: moments[1],
|
||||
until: moments[2],
|
||||
fee: values[2],
|
||||
}, {
|
||||
value: values[1],
|
||||
from: moments[2],
|
||||
until: moments[3],
|
||||
}, {
|
||||
value: values[1],
|
||||
from: moments[3],
|
||||
until: moments[4],
|
||||
fee: values[3],
|
||||
}] as FeeDescription[];
|
||||
|
||||
{
|
||||
const pairs = createDenominationPairTimeline(left, right)
|
||||
expect(pairs).deep.equals([{
|
||||
from: moments[1],
|
||||
until: moments[2],
|
||||
value: values[1],
|
||||
left: values[1],
|
||||
right: values[2],
|
||||
}, {
|
||||
from: moments[2],
|
||||
until: moments[3],
|
||||
value: values[1],
|
||||
left: values[1],
|
||||
right: undefined,
|
||||
}, {
|
||||
from: moments[3],
|
||||
until: moments[4],
|
||||
value: values[1],
|
||||
left: values[1],
|
||||
right: values[3],
|
||||
}, {
|
||||
from: moments[4],
|
||||
until: moments[5],
|
||||
value: values[1],
|
||||
left: values[1],
|
||||
right: undefined,
|
||||
}] as FeeDescriptionPair[])
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
describe("multiple value example", () => {
|
||||
|
||||
it("should separate denominations of different value", () => {
|
||||
|
||||
const left = [{
|
||||
value: values[1],
|
||||
from: moments[1],
|
||||
until: moments[3],
|
||||
fee: values[1],
|
||||
}] as FeeDescription[];
|
||||
|
||||
const right = [{
|
||||
value: values[2],
|
||||
from: moments[1],
|
||||
until: moments[3],
|
||||
fee: values[2],
|
||||
}] as FeeDescription[];
|
||||
|
||||
{
|
||||
const pairs = createDenominationPairTimeline(left, right)
|
||||
expect(pairs).deep.equals([{
|
||||
from: moments[1],
|
||||
until: moments[3],
|
||||
value: values[1],
|
||||
left: values[1],
|
||||
right: undefined,
|
||||
}, {
|
||||
from: moments[1],
|
||||
until: moments[3],
|
||||
value: values[2],
|
||||
left: undefined,
|
||||
right: values[2],
|
||||
}] as FeeDescriptionPair[])
|
||||
}
|
||||
{
|
||||
const pairs = createDenominationPairTimeline(right, left)
|
||||
expect(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("should separate denominations of different value2", () => {
|
||||
|
||||
const left = [{
|
||||
value: values[1],
|
||||
from: moments[1],
|
||||
until: moments[2],
|
||||
fee: values[1],
|
||||
}, {
|
||||
value: values[1],
|
||||
from: moments[2],
|
||||
until: moments[4],
|
||||
fee: values[2],
|
||||
}] as FeeDescription[];
|
||||
|
||||
const right = [{
|
||||
value: values[2],
|
||||
from: moments[1],
|
||||
until: moments[3],
|
||||
fee: values[2],
|
||||
}] as FeeDescription[];
|
||||
|
||||
{
|
||||
const pairs = createDenominationPairTimeline(left, right)
|
||||
expect(pairs).deep.equals([{
|
||||
from: moments[1],
|
||||
until: moments[2],
|
||||
value: values[1],
|
||||
left: values[1],
|
||||
right: undefined,
|
||||
}, {
|
||||
from: moments[2],
|
||||
until: moments[4],
|
||||
value: values[1],
|
||||
left: values[2],
|
||||
right: undefined,
|
||||
}, {
|
||||
from: moments[1],
|
||||
until: moments[3],
|
||||
value: values[2],
|
||||
left: undefined,
|
||||
right: values[2],
|
||||
}] as FeeDescriptionPair[])
|
||||
}
|
||||
// {
|
||||
// const pairs = createDenominationPairTimeline(right, left)
|
||||
// expect(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", () => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -14,26 +14,23 @@
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { LoadingError } from "../../components/LoadingError.js";
|
||||
import { LogoHeader } from "../../components/LogoHeader.js";
|
||||
import {
|
||||
Input,
|
||||
LinkPrimary,
|
||||
SubTitle,
|
||||
SvgIcon,
|
||||
WalletAction,
|
||||
} from "../../components/styled/index.js";
|
||||
import { useTranslationContext } from "../../context/translation.js";
|
||||
import { FeeDescription, FeeDescriptionPair, State } from "./index.js";
|
||||
Amounts,
|
||||
FeeDescription,
|
||||
FeeDescriptionPair,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { styled } from "@linaria/react";
|
||||
import arrowDown from "../../svg/chevron-down.svg";
|
||||
import { Amount } from "../../components/Amount.js";
|
||||
import { Time } from "../../components/Time.js";
|
||||
import { AbsoluteTime, AmountJson, Amounts } from "@gnu-taler/taler-util";
|
||||
import { Button } from "../../mui/Button.js";
|
||||
import { SelectList } from "../../components/SelectList.js";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import { Amount } from "../../components/Amount.js";
|
||||
import { LoadingError } from "../../components/LoadingError.js";
|
||||
import { SelectList } from "../../components/SelectList.js";
|
||||
import { Input, LinkPrimary, SvgIcon } from "../../components/styled/index.js";
|
||||
import { Time } from "../../components/Time.js";
|
||||
import { useTranslationContext } from "../../context/translation.js";
|
||||
import { Button } from "../../mui/Button.js";
|
||||
import arrowDown from "../../svg/chevron-down.svg";
|
||||
import { State } from "./index.js";
|
||||
|
||||
const ButtonGroup = styled.div`
|
||||
& > button {
|
||||
@ -123,7 +120,6 @@ export function NoExchangesView(state: State.NoExchanges): VNode {
|
||||
export function ComparingView({
|
||||
exchanges,
|
||||
selected,
|
||||
nextFeeUpdate,
|
||||
onReset,
|
||||
onSelect,
|
||||
pairTimeline,
|
||||
@ -185,12 +181,6 @@ export function ComparingView({
|
||||
<td>currency</td>
|
||||
<td>{selected.currency}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>next fee update</td>
|
||||
<td>
|
||||
{<Time timestamp={nextFeeUpdate} format="dd MMMM yyyy, HH:mm" />}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</section>
|
||||
<section>
|
||||
@ -305,7 +295,6 @@ export function ComparingView({
|
||||
export function ReadyView({
|
||||
exchanges,
|
||||
selected,
|
||||
nextFeeUpdate,
|
||||
onClose,
|
||||
timeline,
|
||||
}: State.Ready): VNode {
|
||||
@ -362,12 +351,6 @@ export function ReadyView({
|
||||
<td>currency</td>
|
||||
<td>{selected.currency}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>next fee update</td>
|
||||
<td>
|
||||
{<Time timestamp={nextFeeUpdate} format="dd MMMM yyyy, HH:mm" />}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</section>
|
||||
<section>
|
||||
|
Loading…
Reference in New Issue
Block a user