This commit is contained in:
Sebastian 2022-09-12 10:57:13 -03:00
parent fc413bb5ec
commit 27201416c7
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
15 changed files with 1240 additions and 14632 deletions

View File

@ -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.

View File

@ -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>()

View File

@ -65,3 +65,4 @@ export {
} from "./crypto/cryptoImplementation.js";
export * from "./util/timer.js";
export * from "./util/denominations.js";

View 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)
// })
// })
// })

View 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[]);
}

View File

@ -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(

View File

@ -42,4 +42,5 @@ export const Ready = createExample(ReadyView, {
merchantBaseUrl: "http://merchant.url/",
exchangeBaseUrl: "http://exchange.url/",
accept: {},
cancel: {},
});

View File

@ -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>

View File

@ -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

View File

@ -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;
}

View File

@ -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[]);
}

View File

@ -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]),
});

View File

@ -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)
})
})
})

View File

@ -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>