create a fee description timeline for global fee and wire fees

This commit is contained in:
Sebastian 2022-10-12 15:58:10 -03:00
parent cb44202440
commit 610df1c9cf
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
15 changed files with 1059 additions and 378 deletions

View File

@ -1091,17 +1091,21 @@ export interface BackupExchangeWireFee {
* *
*/ */
export interface BackupExchangeGlobalFees { export interface BackupExchangeGlobalFees {
start_date: TalerProtocolTimestamp; startDate: TalerProtocolTimestamp;
end_date: TalerProtocolTimestamp; endDate: TalerProtocolTimestamp;
kyc_fee: BackupAmountString;
history_fee: BackupAmountString; kycFee: BackupAmountString;
account_fee: BackupAmountString; historyFee: BackupAmountString;
purse_fee: BackupAmountString; accountFee: BackupAmountString;
history_expiration: TalerProtocolDuration; purseFee: BackupAmountString;
account_kyc_timeout: TalerProtocolDuration;
purse_account_limit: number; historyTimeout: TalerProtocolDuration;
purse_timeout: TalerProtocolDuration; kycTimeout: TalerProtocolDuration;
master_sig: string; purseTimeout: TalerProtocolDuration;
purseLimit: number;
signature: string;
} }
/** /**
* Structure of one exchange signing key in the /keys response. * Structure of one exchange signing key in the /keys response.

View File

@ -36,6 +36,7 @@ import {
AbsoluteTime, AbsoluteTime,
codecForAbsoluteTime, codecForAbsoluteTime,
codecForTimestamp, codecForTimestamp,
TalerProtocolDuration,
TalerProtocolTimestamp, TalerProtocolTimestamp,
} from "./time.js"; } from "./time.js";
import { import {
@ -673,6 +674,23 @@ export interface WireInfo {
accounts: ExchangeAccount[]; accounts: ExchangeAccount[];
} }
export interface ExchangeGlobalFees {
startDate: TalerProtocolTimestamp;
endDate: TalerProtocolTimestamp;
kycFee: AmountJson;
historyFee: AmountJson;
accountFee: AmountJson;
purseFee: AmountJson;
historyTimeout: TalerProtocolDuration;
kycTimeout: TalerProtocolDuration;
purseTimeout: TalerProtocolDuration;
purseLimit: number;
signature: string;
}
const codecForExchangeAccount = (): Codec<ExchangeAccount> => const codecForExchangeAccount = (): Codec<ExchangeAccount> =>
buildCodecForObject<ExchangeAccount>() buildCodecForObject<ExchangeAccount>()
.property("payto_uri", codecForString()) .property("payto_uri", codecForString())
@ -752,28 +770,31 @@ export interface DenominationInfo {
exchangeBaseUrl: string; exchangeBaseUrl: string;
} }
export type Operation = "deposit" | "withdraw" | "refresh" | "refund"; export type DenomOperation = "deposit" | "withdraw" | "refresh" | "refund";
export type OperationMap<T> = { [op in Operation]: T }; export type DenomOperationMap<T> = { [op in DenomOperation]: T };
export interface FeeDescription { export interface FeeDescription {
value: AmountJson; group: string;
from: AbsoluteTime; from: AbsoluteTime;
until: AbsoluteTime; until: AbsoluteTime;
fee?: AmountJson; fee?: AmountJson;
} }
export interface FeeDescriptionPair { export interface FeeDescriptionPair {
value: AmountJson; group: string;
from: AbsoluteTime; from: AbsoluteTime;
until: AbsoluteTime; until: AbsoluteTime;
left?: AmountJson; left?: AmountJson;
right?: AmountJson; right?: AmountJson;
} }
export interface TimePoint { export interface TimePoint<T> {
id: string;
group: string;
fee: AmountJson;
type: "start" | "end"; type: "start" | "end";
moment: AbsoluteTime; moment: AbsoluteTime;
denom: DenominationInfo; denom: T;
} }
export interface ExchangeFullDetails { export interface ExchangeFullDetails {
@ -783,7 +804,9 @@ export interface ExchangeFullDetails {
tos: ExchangeTos; tos: ExchangeTos;
auditors: ExchangeAuditor[]; auditors: ExchangeAuditor[];
wireInfo: WireInfo; wireInfo: WireInfo;
feesDescription: OperationMap<FeeDescription[]>; denomFees: DenomOperationMap<FeeDescription[]>;
transferFees: Record<string, FeeDescription[]>;
globalFees: FeeDescription[];
} }
export interface ExchangeListItem { export interface ExchangeListItem {
@ -816,7 +839,7 @@ const codecForExchangeTos = (): Codec<ExchangeTos> =>
export const codecForFeeDescriptionPair = (): Codec<FeeDescriptionPair> => export const codecForFeeDescriptionPair = (): Codec<FeeDescriptionPair> =>
buildCodecForObject<FeeDescriptionPair>() buildCodecForObject<FeeDescriptionPair>()
.property("value", codecForAmountJson()) .property("group", codecForString())
.property("from", codecForAbsoluteTime) .property("from", codecForAbsoluteTime)
.property("until", codecForAbsoluteTime) .property("until", codecForAbsoluteTime)
.property("left", codecOptional(codecForAmountJson())) .property("left", codecOptional(codecForAmountJson()))
@ -825,21 +848,21 @@ export const codecForFeeDescriptionPair = (): Codec<FeeDescriptionPair> =>
export const codecForFeeDescription = (): Codec<FeeDescription> => export const codecForFeeDescription = (): Codec<FeeDescription> =>
buildCodecForObject<FeeDescription>() buildCodecForObject<FeeDescription>()
.property("value", codecForAmountJson()) .property("group", codecForString())
.property("from", codecForAbsoluteTime) .property("from", codecForAbsoluteTime)
.property("until", codecForAbsoluteTime) .property("until", codecForAbsoluteTime)
.property("fee", codecOptional(codecForAmountJson())) .property("fee", codecOptional(codecForAmountJson()))
.build("FeeDescription"); .build("FeeDescription");
export const codecForFeesByOperations = (): Codec< export const codecForFeesByOperations = (): Codec<
OperationMap<FeeDescription[]> DenomOperationMap<FeeDescription[]>
> => > =>
buildCodecForObject<OperationMap<FeeDescription[]>>() buildCodecForObject<DenomOperationMap<FeeDescription[]>>()
.property("deposit", codecForList(codecForFeeDescription())) .property("deposit", codecForList(codecForFeeDescription()))
.property("withdraw", codecForList(codecForFeeDescription())) .property("withdraw", codecForList(codecForFeeDescription()))
.property("refresh", codecForList(codecForFeeDescription())) .property("refresh", codecForList(codecForFeeDescription()))
.property("refund", codecForList(codecForFeeDescription())) .property("refund", codecForList(codecForFeeDescription()))
.build("FeesByOperations"); .build("DenomOperationMap");
export const codecForExchangeFullDetails = (): Codec<ExchangeFullDetails> => export const codecForExchangeFullDetails = (): Codec<ExchangeFullDetails> =>
buildCodecForObject<ExchangeFullDetails>() buildCodecForObject<ExchangeFullDetails>()
@ -849,7 +872,12 @@ export const codecForExchangeFullDetails = (): Codec<ExchangeFullDetails> =>
.property("tos", codecForExchangeTos()) .property("tos", codecForExchangeTos())
.property("auditors", codecForList(codecForExchangeAuditor())) .property("auditors", codecForList(codecForExchangeAuditor()))
.property("wireInfo", codecForWireInfo()) .property("wireInfo", codecForWireInfo())
.property("feesDescription", codecForFeesByOperations()) .property("denomFees", codecForFeesByOperations())
.property(
"transferFees",
codecForMap(codecForList(codecForFeeDescription())),
)
.property("globalFees", codecForList(codecForFeeDescription()))
.build("ExchangeFullDetails"); .build("ExchangeFullDetails");
export const codecForExchangeListItem = (): Codec<ExchangeListItem> => export const codecForExchangeListItem = (): Codec<ExchangeListItem> =>

View File

@ -53,7 +53,6 @@ export class CryptoRpcClient {
this.proc.unref(); this.proc.unref();
this.proc.stdout.on("data", (x) => { this.proc.stdout.on("data", (x) => {
// console.log("got chunk", x.toString("utf-8"));
if (x instanceof Buffer) { if (x instanceof Buffer) {
const nlIndex = x.indexOf("\n"); const nlIndex = x.indexOf("\n");
if (nlIndex >= 0) { if (nlIndex >= 0) {

View File

@ -46,6 +46,7 @@ import {
WireInfo, WireInfo,
DenominationInfo, DenominationInfo,
GlobalFees, GlobalFees,
ExchangeGlobalFees,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { RetryInfo, RetryTags } from "./util/retries.js"; import { RetryInfo, RetryTags } from "./util/retries.js";
import { Event, IDBDatabase } from "@gnu-taler/idb-bridge"; import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
@ -428,7 +429,7 @@ export interface ExchangeDetailsRecord {
/** /**
* Fees for exchange services * Fees for exchange services
*/ */
globalFees: GlobalFees[]; globalFees: ExchangeGlobalFees[];
/** /**
* Signing keys we got from the exchange, can also contain * Signing keys we got from the exchange, can also contain
* older signing keys that are not returned by /keys anymore. * older signing keys that are not returned by /keys anymore.

View File

@ -345,7 +345,19 @@ export async function exportBackup(
stamp_expire: x.stamp_expire, stamp_expire: x.stamp_expire,
stamp_start: x.stamp_start, stamp_start: x.stamp_start,
})), })),
global_fees: ex.globalFees, global_fees: ex.globalFees.map((x) => ({
accountFee: Amounts.stringify(x.accountFee),
historyFee: Amounts.stringify(x.historyFee),
kycFee: Amounts.stringify(x.kycFee),
purseFee: Amounts.stringify(x.purseFee),
kycTimeout: x.kycTimeout,
endDate: x.endDate,
historyTimeout: x.historyTimeout,
signature: x.signature,
purseLimit: x.purseLimit,
purseTimeout: x.purseTimeout,
startDate: x.startDate,
})),
tos_accepted_etag: ex.termsOfServiceAcceptedEtag, tos_accepted_etag: ex.termsOfServiceAcceptedEtag,
tos_accepted_timestamp: ex.termsOfServiceAcceptedTimestamp, tos_accepted_timestamp: ex.termsOfServiceAcceptedTimestamp,
denominations: denominations:

View File

@ -405,7 +405,20 @@ export async function importBackup(
masterPublicKey: backupExchangeDetails.master_public_key, masterPublicKey: backupExchangeDetails.master_public_key,
protocolVersion: backupExchangeDetails.protocol_version, protocolVersion: backupExchangeDetails.protocol_version,
reserveClosingDelay: backupExchangeDetails.reserve_closing_delay, reserveClosingDelay: backupExchangeDetails.reserve_closing_delay,
globalFees: backupExchangeDetails.global_fees, globalFees: backupExchangeDetails.global_fees.map((x) => ({
accountFee: Amounts.parseOrThrow(x.accountFee),
historyFee: Amounts.parseOrThrow(x.historyFee),
kycFee: Amounts.parseOrThrow(x.kycFee),
purseFee: Amounts.parseOrThrow(x.purseFee),
kycTimeout: x.kycTimeout,
endDate: x.endDate,
historyTimeout: x.historyTimeout,
signature: x.signature,
purseLimit: x.purseLimit,
purseTimeout: x.purseTimeout,
startDate: x.startDate,
})),
signingKeys: backupExchangeDetails.signing_keys.map((x) => ({ signingKeys: backupExchangeDetails.signing_keys.map((x) => ({
key: x.key, key: x.key,
master_sig: x.master_sig, master_sig: x.master_sig,

View File

@ -30,6 +30,7 @@ import {
encodeCrock, encodeCrock,
ExchangeAuditor, ExchangeAuditor,
ExchangeDenomination, ExchangeDenomination,
ExchangeGlobalFees,
ExchangeSignKeyJson, ExchangeSignKeyJson,
ExchangeWireJson, ExchangeWireJson,
GlobalFees, GlobalFees,
@ -274,7 +275,8 @@ async function validateGlobalFees(
ws: InternalWalletState, ws: InternalWalletState,
fees: GlobalFees[], fees: GlobalFees[],
masterPub: string, masterPub: string,
): Promise<GlobalFees[]> { ): Promise<ExchangeGlobalFees[]> {
const egf: ExchangeGlobalFees[] = [];
for (const gf of fees) { for (const gf of fees) {
logger.trace("validating exchange global fees"); logger.trace("validating exchange global fees");
let isValid = false; let isValid = false;
@ -291,9 +293,22 @@ async function validateGlobalFees(
if (!isValid) { if (!isValid) {
throw Error("exchange global fees signature invalid: " + gf.master_sig); throw Error("exchange global fees signature invalid: " + gf.master_sig);
} }
egf.push({
accountFee: Amounts.parseOrThrow(gf.account_fee),
historyFee: Amounts.parseOrThrow(gf.history_fee),
purseFee: Amounts.parseOrThrow(gf.purse_fee),
kycFee: Amounts.parseOrThrow(gf.kyc_fee),
startDate: gf.start_date,
endDate: gf.end_date,
signature: gf.master_sig,
historyTimeout: gf.history_expiration,
kycTimeout: gf.account_kyc_timeout,
purseLimit: gf.purse_account_limit,
purseTimeout: gf.purse_timeout,
});
} }
return fees; return egf;
} }
export interface ExchangeInfo { export interface ExchangeInfo {

View File

@ -28,8 +28,9 @@ import {
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
// import { expect } from "chai"; // import { expect } from "chai";
import { import {
createDenominationPairTimeline, createPairTimeline,
createDenominationTimeline, createTimeline,
selectBestForOverlappingDenominations,
} from "./denominations.js"; } from "./denominations.js";
import test, { ExecutionContext } from "ava"; import test, { ExecutionContext } from "ava";
@ -42,8 +43,14 @@ const VALUES = Array.from({ length: 10 }).map((undef, t) =>
const TIMESTAMPS = Array.from({ length: 20 }).map((undef, t_s) => ({ t_s })); const TIMESTAMPS = Array.from({ length: 20 }).map((undef, t_s) => ({ t_s }));
const ABS_TIME = TIMESTAMPS.map((m) => AbsoluteTime.fromTimestamp(m)); const ABS_TIME = TIMESTAMPS.map((m) => AbsoluteTime.fromTimestamp(m));
function normalize(list: DenominationInfo[]): DenominationInfo[] { function normalize(
return list.map((e, idx) => ({ ...e, denomPubHash: `id${idx}` })); list: DenominationInfo[],
): (DenominationInfo & { group: string })[] {
return list.map((e, idx) => ({
...e,
denomPubHash: `id${idx}`,
group: Amounts.stringifyValue(e.value),
}));
} }
//Avoiding to make an error-prone/time-consuming refactor //Avoiding to make an error-prone/time-consuming refactor
@ -61,7 +68,7 @@ function expect(t: ExecutionContext, thing: any): any {
// describe("single value example", (t) => { // describe("single value example", (t) => {
test("should have one row with start and exp", (t) => { test("should have one row with start and exp", (t) => {
const timeline = createDenominationTimeline( const timeline = createTimeline(
normalize([ normalize([
{ {
value: VALUES[1], value: VALUES[1],
@ -70,13 +77,17 @@ test("should have one row with start and exp", (t) => {
feeDeposit: VALUES[1], feeDeposit: VALUES[1],
} as Partial<DenominationInfo> as DenominationInfo, } as Partial<DenominationInfo> as DenominationInfo,
]), ]),
"denomPubHash",
"stampStart",
"stampExpireDeposit", "stampExpireDeposit",
"feeDeposit", "feeDeposit",
"group",
selectBestForOverlappingDenominations,
); );
expect(t, timeline).deep.equal([ expect(t, timeline).deep.equal([
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[2], until: ABS_TIME[2],
fee: VALUES[1], fee: VALUES[1],
@ -85,7 +96,7 @@ test("should have one row with start and exp", (t) => {
}); });
test("should have two rows with the second denom in the middle if second is better", (t) => { test("should have two rows with the second denom in the middle if second is better", (t) => {
const timeline = createDenominationTimeline( const timeline = createTimeline(
normalize([ normalize([
{ {
value: VALUES[1], value: VALUES[1],
@ -100,19 +111,23 @@ test("should have two rows with the second denom in the middle if second is bett
feeDeposit: VALUES[2], feeDeposit: VALUES[2],
} as Partial<DenominationInfo> as DenominationInfo, } as Partial<DenominationInfo> as DenominationInfo,
]), ]),
"denomPubHash",
"stampStart",
"stampExpireDeposit", "stampExpireDeposit",
"feeDeposit", "feeDeposit",
"group",
selectBestForOverlappingDenominations,
); );
expect(t, timeline).deep.equal([ expect(t, timeline).deep.equal([
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[3], until: ABS_TIME[3],
fee: VALUES[1], fee: VALUES[1],
}, },
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[3], from: ABS_TIME[3],
until: ABS_TIME[4], until: ABS_TIME[4],
fee: VALUES[2], fee: VALUES[2],
@ -121,7 +136,7 @@ test("should have two rows with the second denom in the middle if second is bett
}); });
test("should have two rows with the first denom in the middle if second is worse", (t) => { test("should have two rows with the first denom in the middle if second is worse", (t) => {
const timeline = createDenominationTimeline( const timeline = createTimeline(
normalize([ normalize([
{ {
value: VALUES[1], value: VALUES[1],
@ -136,19 +151,23 @@ test("should have two rows with the first denom in the middle if second is worse
feeDeposit: VALUES[1], feeDeposit: VALUES[1],
} as Partial<DenominationInfo> as DenominationInfo, } as Partial<DenominationInfo> as DenominationInfo,
]), ]),
"denomPubHash",
"stampStart",
"stampExpireDeposit", "stampExpireDeposit",
"feeDeposit", "feeDeposit",
"group",
selectBestForOverlappingDenominations,
); );
expect(t, timeline).deep.equal([ expect(t, timeline).deep.equal([
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[2], until: ABS_TIME[2],
fee: VALUES[2], fee: VALUES[2],
}, },
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[2], from: ABS_TIME[2],
until: ABS_TIME[4], until: ABS_TIME[4],
fee: VALUES[1], fee: VALUES[1],
@ -157,7 +176,7 @@ test("should have two rows with the first denom in the middle if second is worse
}); });
test("should add a gap when there no fee", (t) => { test("should add a gap when there no fee", (t) => {
const timeline = createDenominationTimeline( const timeline = createTimeline(
normalize([ normalize([
{ {
value: VALUES[1], value: VALUES[1],
@ -172,24 +191,28 @@ test("should add a gap when there no fee", (t) => {
feeDeposit: VALUES[1], feeDeposit: VALUES[1],
} as Partial<DenominationInfo> as DenominationInfo, } as Partial<DenominationInfo> as DenominationInfo,
]), ]),
"denomPubHash",
"stampStart",
"stampExpireDeposit", "stampExpireDeposit",
"feeDeposit", "feeDeposit",
"group",
selectBestForOverlappingDenominations,
); );
expect(t, timeline).deep.equal([ expect(t, timeline).deep.equal([
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[2], until: ABS_TIME[2],
fee: VALUES[2], fee: VALUES[2],
}, },
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[2], from: ABS_TIME[2],
until: ABS_TIME[3], until: ABS_TIME[3],
}, },
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[3], from: ABS_TIME[3],
until: ABS_TIME[4], until: ABS_TIME[4],
fee: VALUES[1], fee: VALUES[1],
@ -198,7 +221,7 @@ test("should add a gap when there no fee", (t) => {
}); });
test("should have three rows when first denom is between second and second is worse", (t) => { test("should have three rows when first denom is between second and second is worse", (t) => {
const timeline = createDenominationTimeline( const timeline = createTimeline(
normalize([ normalize([
{ {
value: VALUES[1], value: VALUES[1],
@ -213,24 +236,28 @@ test("should have three rows when first denom is between second and second is wo
feeDeposit: VALUES[2], feeDeposit: VALUES[2],
} as Partial<DenominationInfo> as DenominationInfo, } as Partial<DenominationInfo> as DenominationInfo,
]), ]),
"denomPubHash",
"stampStart",
"stampExpireDeposit", "stampExpireDeposit",
"feeDeposit", "feeDeposit",
"group",
selectBestForOverlappingDenominations,
); );
expect(t, timeline).deep.equal([ expect(t, timeline).deep.equal([
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[2], until: ABS_TIME[2],
fee: VALUES[2], fee: VALUES[2],
}, },
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[2], from: ABS_TIME[2],
until: ABS_TIME[3], until: ABS_TIME[3],
fee: VALUES[1], fee: VALUES[1],
}, },
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[3], from: ABS_TIME[3],
until: ABS_TIME[4], until: ABS_TIME[4],
fee: VALUES[2], fee: VALUES[2],
@ -239,7 +266,7 @@ test("should have three rows when first denom is between second and second is wo
}); });
test("should have one row when first denom is between second and second is better", (t) => { test("should have one row when first denom is between second and second is better", (t) => {
const timeline = createDenominationTimeline( const timeline = createTimeline(
normalize([ normalize([
{ {
value: VALUES[1], value: VALUES[1],
@ -254,13 +281,17 @@ test("should have one row when first denom is between second and second is bette
feeDeposit: VALUES[1], feeDeposit: VALUES[1],
} as Partial<DenominationInfo> as DenominationInfo, } as Partial<DenominationInfo> as DenominationInfo,
]), ]),
"denomPubHash",
"stampStart",
"stampExpireDeposit", "stampExpireDeposit",
"feeDeposit", "feeDeposit",
"group",
selectBestForOverlappingDenominations,
); );
expect(t, timeline).deep.equal([ expect(t, timeline).deep.equal([
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[4], until: ABS_TIME[4],
fee: VALUES[1], fee: VALUES[1],
@ -269,7 +300,7 @@ test("should have one row when first denom is between second and second is bette
}); });
test("should only add the best1", (t) => { test("should only add the best1", (t) => {
const timeline = createDenominationTimeline( const timeline = createTimeline(
normalize([ normalize([
{ {
value: VALUES[1], value: VALUES[1],
@ -290,19 +321,23 @@ test("should only add the best1", (t) => {
feeDeposit: VALUES[2], feeDeposit: VALUES[2],
} as Partial<DenominationInfo> as DenominationInfo, } as Partial<DenominationInfo> as DenominationInfo,
]), ]),
"denomPubHash",
"stampStart",
"stampExpireDeposit", "stampExpireDeposit",
"feeDeposit", "feeDeposit",
"group",
selectBestForOverlappingDenominations,
); );
expect(t, timeline).deep.equal([ expect(t, timeline).deep.equal([
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[2], until: ABS_TIME[2],
fee: VALUES[2], fee: VALUES[2],
}, },
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[2], from: ABS_TIME[2],
until: ABS_TIME[4], until: ABS_TIME[4],
fee: VALUES[1], fee: VALUES[1],
@ -311,7 +346,7 @@ test("should only add the best1", (t) => {
}); });
test("should only add the best2", (t) => { test("should only add the best2", (t) => {
const timeline = createDenominationTimeline( const timeline = createTimeline(
normalize([ normalize([
{ {
value: VALUES[1], value: VALUES[1],
@ -338,25 +373,29 @@ test("should only add the best2", (t) => {
feeDeposit: VALUES[3], feeDeposit: VALUES[3],
} as Partial<DenominationInfo> as DenominationInfo, } as Partial<DenominationInfo> as DenominationInfo,
]), ]),
"denomPubHash",
"stampStart",
"stampExpireDeposit", "stampExpireDeposit",
"feeDeposit", "feeDeposit",
"group",
selectBestForOverlappingDenominations,
); );
expect(t, timeline).deep.equal([ expect(t, timeline).deep.equal([
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[2], until: ABS_TIME[2],
fee: VALUES[2], fee: VALUES[2],
}, },
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[2], from: ABS_TIME[2],
until: ABS_TIME[5], until: ABS_TIME[5],
fee: VALUES[1], fee: VALUES[1],
}, },
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[5], from: ABS_TIME[5],
until: ABS_TIME[6], until: ABS_TIME[6],
fee: VALUES[3], fee: VALUES[3],
@ -365,7 +404,7 @@ test("should only add the best2", (t) => {
}); });
test("should only add the best3", (t) => { test("should only add the best3", (t) => {
const timeline = createDenominationTimeline( const timeline = createTimeline(
normalize([ normalize([
{ {
value: VALUES[1], value: VALUES[1],
@ -386,13 +425,17 @@ test("should only add the best3", (t) => {
feeDeposit: VALUES[2], feeDeposit: VALUES[2],
} as Partial<DenominationInfo> as DenominationInfo, } as Partial<DenominationInfo> as DenominationInfo,
]), ]),
"denomPubHash",
"stampStart",
"stampExpireDeposit", "stampExpireDeposit",
"feeDeposit", "feeDeposit",
"group",
selectBestForOverlappingDenominations,
); );
expect(t, timeline).deep.equal([ expect(t, timeline).deep.equal([
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[2], from: ABS_TIME[2],
until: ABS_TIME[5], until: ABS_TIME[5],
fee: VALUES[1], fee: VALUES[1],
@ -406,7 +449,7 @@ test("should only add the best3", (t) => {
//TODO: test the same start but different value //TODO: test the same start but different value
test("should not merge when there is different value", (t) => { test("should not merge when there is different value", (t) => {
const timeline = createDenominationTimeline( const timeline = createTimeline(
normalize([ normalize([
{ {
value: VALUES[1], value: VALUES[1],
@ -421,19 +464,23 @@ test("should not merge when there is different value", (t) => {
feeDeposit: VALUES[2], feeDeposit: VALUES[2],
} as Partial<DenominationInfo> as DenominationInfo, } as Partial<DenominationInfo> as DenominationInfo,
]), ]),
"denomPubHash",
"stampStart",
"stampExpireDeposit", "stampExpireDeposit",
"feeDeposit", "feeDeposit",
"group",
selectBestForOverlappingDenominations,
); );
expect(t, timeline).deep.equal([ expect(t, timeline).deep.equal([
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[3], until: ABS_TIME[3],
fee: VALUES[1], fee: VALUES[1],
}, },
{ {
value: VALUES[2], group: Amounts.stringifyValue(VALUES[2]),
from: ABS_TIME[2], from: ABS_TIME[2],
until: ABS_TIME[4], until: ABS_TIME[4],
fee: VALUES[2], fee: VALUES[2],
@ -442,7 +489,7 @@ test("should not merge when there is different value", (t) => {
}); });
test("should not merge when there is different value (with duplicates)", (t) => { test("should not merge when there is different value (with duplicates)", (t) => {
const timeline = createDenominationTimeline( const timeline = createTimeline(
normalize([ normalize([
{ {
value: VALUES[1], value: VALUES[1],
@ -469,19 +516,23 @@ test("should not merge when there is different value (with duplicates)", (t) =>
feeDeposit: VALUES[2], feeDeposit: VALUES[2],
} as Partial<DenominationInfo> as DenominationInfo, } as Partial<DenominationInfo> as DenominationInfo,
]), ]),
"denomPubHash",
"stampStart",
"stampExpireDeposit", "stampExpireDeposit",
"feeDeposit", "feeDeposit",
"group",
selectBestForOverlappingDenominations,
); );
expect(t, timeline).deep.equal([ expect(t, timeline).deep.equal([
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[3], until: ABS_TIME[3],
fee: VALUES[1], fee: VALUES[1],
}, },
{ {
value: VALUES[2], group: Amounts.stringifyValue(VALUES[2]),
from: ABS_TIME[2], from: ABS_TIME[2],
until: ABS_TIME[4], until: ABS_TIME[4],
fee: VALUES[2], fee: VALUES[2],
@ -519,7 +570,7 @@ test("should return empty", (t) => {
const left = [] as FeeDescription[]; const left = [] as FeeDescription[];
const right = [] as FeeDescription[]; const right = [] as FeeDescription[];
const pairs = createDenominationPairTimeline(left, right); const pairs = createPairTimeline(left, right);
expect(t, pairs).deep.equals([]); expect(t, pairs).deep.equals([]);
}); });
@ -527,7 +578,7 @@ test("should return empty", (t) => {
test("should return first element", (t) => { test("should return first element", (t) => {
const left = [ const left = [
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[3], until: ABS_TIME[3],
fee: VALUES[1], fee: VALUES[1],
@ -537,24 +588,24 @@ test("should return first element", (t) => {
const right = [] as FeeDescription[]; const right = [] as FeeDescription[];
{ {
const pairs = createDenominationPairTimeline(left, right); const pairs = createPairTimeline(left, right);
expect(t, pairs).deep.equals([ expect(t, pairs).deep.equals([
{ {
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[3], until: ABS_TIME[3],
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
left: VALUES[1], left: VALUES[1],
right: undefined, right: undefined,
}, },
] as FeeDescriptionPair[]); ] as FeeDescriptionPair[]);
} }
{ {
const pairs = createDenominationPairTimeline(right, left); const pairs = createPairTimeline(right, left);
expect(t, pairs).deep.equals([ expect(t, pairs).deep.equals([
{ {
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[3], until: ABS_TIME[3],
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
right: VALUES[1], right: VALUES[1],
left: undefined, left: undefined,
}, },
@ -565,7 +616,7 @@ test("should return first element", (t) => {
test("should add both to the same row", (t) => { test("should add both to the same row", (t) => {
const left = [ const left = [
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[3], until: ABS_TIME[3],
fee: VALUES[1], fee: VALUES[1],
@ -574,7 +625,7 @@ test("should add both to the same row", (t) => {
const right = [ const right = [
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[3], until: ABS_TIME[3],
fee: VALUES[2], fee: VALUES[2],
@ -582,24 +633,24 @@ test("should add both to the same row", (t) => {
] as FeeDescription[]; ] as FeeDescription[];
{ {
const pairs = createDenominationPairTimeline(left, right); const pairs = createPairTimeline(left, right);
expect(t, pairs).deep.equals([ expect(t, pairs).deep.equals([
{ {
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[3], until: ABS_TIME[3],
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
left: VALUES[1], left: VALUES[1],
right: VALUES[2], right: VALUES[2],
}, },
] as FeeDescriptionPair[]); ] as FeeDescriptionPair[]);
} }
{ {
const pairs = createDenominationPairTimeline(right, left); const pairs = createPairTimeline(right, left);
expect(t, pairs).deep.equals([ expect(t, pairs).deep.equals([
{ {
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[3], until: ABS_TIME[3],
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
left: VALUES[2], left: VALUES[2],
right: VALUES[1], right: VALUES[1],
}, },
@ -610,7 +661,7 @@ test("should add both to the same row", (t) => {
test("should repeat the first and change the second", (t) => { test("should repeat the first and change the second", (t) => {
const left = [ const left = [
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[5], until: ABS_TIME[5],
fee: VALUES[1], fee: VALUES[1],
@ -619,18 +670,18 @@ test("should repeat the first and change the second", (t) => {
const right = [ const right = [
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[2], until: ABS_TIME[2],
fee: VALUES[2], fee: VALUES[2],
}, },
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[2], from: ABS_TIME[2],
until: ABS_TIME[3], until: ABS_TIME[3],
}, },
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[3], from: ABS_TIME[3],
until: ABS_TIME[4], until: ABS_TIME[4],
fee: VALUES[3], fee: VALUES[3],
@ -638,33 +689,33 @@ test("should repeat the first and change the second", (t) => {
] as FeeDescription[]; ] as FeeDescription[];
{ {
const pairs = createDenominationPairTimeline(left, right); const pairs = createPairTimeline(left, right);
expect(t, pairs).deep.equals([ expect(t, pairs).deep.equals([
{ {
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[2], until: ABS_TIME[2],
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
left: VALUES[1], left: VALUES[1],
right: VALUES[2], right: VALUES[2],
}, },
{ {
from: ABS_TIME[2], from: ABS_TIME[2],
until: ABS_TIME[3], until: ABS_TIME[3],
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
left: VALUES[1], left: VALUES[1],
right: undefined, right: undefined,
}, },
{ {
from: ABS_TIME[3], from: ABS_TIME[3],
until: ABS_TIME[4], until: ABS_TIME[4],
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
left: VALUES[1], left: VALUES[1],
right: VALUES[3], right: VALUES[3],
}, },
{ {
from: ABS_TIME[4], from: ABS_TIME[4],
until: ABS_TIME[5], until: ABS_TIME[5],
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
left: VALUES[1], left: VALUES[1],
right: undefined, right: undefined,
}, },
@ -679,7 +730,7 @@ test("should repeat the first and change the second", (t) => {
test("should separate denominations of different value", (t) => { test("should separate denominations of different value", (t) => {
const left = [ const left = [
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[3], until: ABS_TIME[3],
fee: VALUES[1], fee: VALUES[1],
@ -688,7 +739,7 @@ test("should separate denominations of different value", (t) => {
const right = [ const right = [
{ {
value: VALUES[2], group: Amounts.stringifyValue(VALUES[2]),
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[3], until: ABS_TIME[3],
fee: VALUES[2], fee: VALUES[2],
@ -696,38 +747,38 @@ test("should separate denominations of different value", (t) => {
] as FeeDescription[]; ] as FeeDescription[];
{ {
const pairs = createDenominationPairTimeline(left, right); const pairs = createPairTimeline(left, right);
expect(t, pairs).deep.equals([ expect(t, pairs).deep.equals([
{ {
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[3], until: ABS_TIME[3],
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
left: VALUES[1], left: VALUES[1],
right: undefined, right: undefined,
}, },
{ {
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[3], until: ABS_TIME[3],
value: VALUES[2], group: Amounts.stringifyValue(VALUES[2]),
left: undefined, left: undefined,
right: VALUES[2], right: VALUES[2],
}, },
] as FeeDescriptionPair[]); ] as FeeDescriptionPair[]);
} }
{ {
const pairs = createDenominationPairTimeline(right, left); const pairs = createPairTimeline(right, left);
expect(t, pairs).deep.equals([ expect(t, pairs).deep.equals([
{ {
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[3], until: ABS_TIME[3],
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
left: undefined, left: undefined,
right: VALUES[1], right: VALUES[1],
}, },
{ {
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[3], until: ABS_TIME[3],
value: VALUES[2], group: Amounts.stringifyValue(VALUES[2]),
left: VALUES[2], left: VALUES[2],
right: undefined, right: undefined,
}, },
@ -738,13 +789,13 @@ test("should separate denominations of different value", (t) => {
test("should separate denominations of different value2", (t) => { test("should separate denominations of different value2", (t) => {
const left = [ const left = [
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[2], until: ABS_TIME[2],
fee: VALUES[1], fee: VALUES[1],
}, },
{ {
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[2], from: ABS_TIME[2],
until: ABS_TIME[4], until: ABS_TIME[4],
fee: VALUES[2], fee: VALUES[2],
@ -753,7 +804,7 @@ test("should separate denominations of different value2", (t) => {
const right = [ const right = [
{ {
value: VALUES[2], group: Amounts.stringifyValue(VALUES[2]),
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[3], until: ABS_TIME[3],
fee: VALUES[2], fee: VALUES[2],
@ -761,26 +812,26 @@ test("should separate denominations of different value2", (t) => {
] as FeeDescription[]; ] as FeeDescription[];
{ {
const pairs = createDenominationPairTimeline(left, right); const pairs = createPairTimeline(left, right);
expect(t, pairs).deep.equals([ expect(t, pairs).deep.equals([
{ {
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[2], until: ABS_TIME[2],
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
left: VALUES[1], left: VALUES[1],
right: undefined, right: undefined,
}, },
{ {
from: ABS_TIME[2], from: ABS_TIME[2],
until: ABS_TIME[4], until: ABS_TIME[4],
value: VALUES[1], group: Amounts.stringifyValue(VALUES[1]),
left: VALUES[2], left: VALUES[2],
right: undefined, right: undefined,
}, },
{ {
from: ABS_TIME[1], from: ABS_TIME[1],
until: ABS_TIME[3], until: ABS_TIME[3],
value: VALUES[2], group: Amounts.stringifyValue(VALUES[2]),
left: undefined, left: undefined,
right: VALUES[2], right: VALUES[2],
}, },

View File

@ -23,6 +23,7 @@ import {
FeeDescriptionPair, FeeDescriptionPair,
TalerProtocolTimestamp, TalerProtocolTimestamp,
TimePoint, TimePoint,
WireFee,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
/** /**
@ -33,9 +34,9 @@ import {
* @param list denominations of same value * @param list denominations of same value
* @returns * @returns
*/ */
function selectBestForOverlappingDenominations( export function selectBestForOverlappingDenominations<
list: DenominationInfo[], T extends DenominationInfo,
): DenominationInfo | undefined { >(list: T[]): T | undefined {
let minDeposit: DenominationInfo | undefined = undefined; let minDeposit: DenominationInfo | undefined = undefined;
//TODO: improve denomination selection, this is a trivial implementation //TODO: improve denomination selection, this is a trivial implementation
list.forEach((e) => { list.forEach((e) => {
@ -50,6 +51,23 @@ function selectBestForOverlappingDenominations(
return minDeposit; return minDeposit;
} }
export function selectMinimumFee<T extends { fee: AmountJson }>(
list: T[],
): T | undefined {
let minFee: T | undefined = undefined;
//TODO: improve denomination selection, this is a trivial implementation
list.forEach((e) => {
if (minFee === undefined) {
minFee = e;
return;
}
if (Amounts.cmp(minFee.fee, e.fee) > -1) {
minFee = e;
}
});
return minFee;
}
type PropsWithReturnType<T extends object, F> = Exclude< type PropsWithReturnType<T extends object, F> = Exclude<
{ {
[K in keyof T]: T[K] extends F ? K : never; [K in keyof T]: T[K] extends F ? K : never;
@ -58,17 +76,19 @@ type PropsWithReturnType<T extends object, F> = Exclude<
>; >;
/** /**
* Takes two list and create one with one timeline. * Takes two timelines and create one to compare them.
* 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} * For both lists the next condition should be true:
* for any element in the position "idx" then
* list[idx].until === list[idx+1].from
*
* @see {createTimeline}
* *
* @param left list denominations @type {FeeDescription} * @param left list denominations @type {FeeDescription}
* @param right list denominations @type {FeeDescription} * @param right list denominations @type {FeeDescription}
* @returns list of pairs for the same time * @returns list of pairs for the same time
*/ */
export function createDenominationPairTimeline( export function createPairTimeline(
left: FeeDescription[], left: FeeDescription[],
right: FeeDescription[], right: FeeDescription[],
): FeeDescriptionPair[] { ): FeeDescriptionPair[] {
@ -81,23 +101,15 @@ export function createDenominationPairTimeline(
let ri = 0; let ri = 0;
while (li < left.length && ri < right.length) { while (li < left.length && ri < right.length) {
const currentValue = const currentGroup =
Amounts.cmp(left[li].value, right[ri].value) < 0 left[li].group < right[ri].group ? left[li].group : right[ri].group;
? left[li].value
: right[ri].value;
let ll = 0; //left length (until next value) let ll = 0; //left length (until next value)
while ( while (li + ll < left.length && left[li + ll].group === currentGroup) {
li + ll < left.length &&
Amounts.cmp(left[li + ll].value, currentValue) === 0
) {
ll++; ll++;
} }
let rl = 0; //right length (until next value) let rl = 0; //right length (until next value)
while ( while (ri + rl < right.length && right[ri + rl].group === currentGroup) {
ri + rl < right.length &&
Amounts.cmp(right[ri + rl].value, currentValue) === 0
) {
rl++; rl++;
} }
const leftIsEmpty = ll === 0; const leftIsEmpty = ll === 0;
@ -120,7 +132,7 @@ export function createDenominationPairTimeline(
right.splice(ri, 0, { right.splice(ri, 0, {
from: leftStarts, from: leftStarts,
until: ends, until: ends,
value: left[li].value, group: left[li].group,
}); });
rl++; rl++;
@ -132,7 +144,7 @@ export function createDenominationPairTimeline(
left.splice(li, 0, { left.splice(li, 0, {
from: rightStarts, from: rightStarts,
until: ends, until: ends,
value: right[ri].value, group: right[ri].group,
}); });
ll++; ll++;
@ -148,7 +160,7 @@ export function createDenominationPairTimeline(
right.splice(ri + rl, 0, { right.splice(ri + rl, 0, {
from: rightEnds, from: rightEnds,
until: leftEnds, until: leftEnds,
value: left[0].value, group: left[0].group,
}); });
rl++; rl++;
} }
@ -156,7 +168,7 @@ export function createDenominationPairTimeline(
left.splice(li + ll, 0, { left.splice(li + ll, 0, {
from: leftEnds, from: leftEnds,
until: rightEnds, until: rightEnds,
value: right[0].value, group: right[0].group,
}); });
ll++; ll++;
} }
@ -165,7 +177,7 @@ export function createDenominationPairTimeline(
while ( while (
li < left.length && li < left.length &&
ri < right.length && ri < right.length &&
Amounts.cmp(left[li].value, right[ri].value) === 0 left[li].group === right[ri].group
) { ) {
if ( if (
AbsoluteTime.cmp(left[li].from, timeCut) !== 0 && AbsoluteTime.cmp(left[li].from, timeCut) !== 0 &&
@ -186,7 +198,7 @@ export function createDenominationPairTimeline(
right: right[ri].fee, right: right[ri].fee,
from: timeCut, from: timeCut,
until: AbsoluteTime.never(), until: AbsoluteTime.never(),
value: currentValue, group: currentGroup,
}); });
if (left[li].until.t_ms === right[ri].until.t_ms) { if (left[li].until.t_ms === right[ri].until.t_ms) {
@ -204,7 +216,7 @@ export function createDenominationPairTimeline(
if ( if (
li < left.length && li < left.length &&
Amounts.cmp(left[li].value, pairList[pairList.length - 1].value) !== 0 left[li].group !== pairList[pairList.length - 1].group
) { ) {
//value changed, should break //value changed, should break
//this if will catch when both (left and right) change at the same time //this if will catch when both (left and right) change at the same time
@ -217,7 +229,7 @@ export function createDenominationPairTimeline(
if (li < left.length) { if (li < left.length) {
let timeCut = let timeCut =
pairList.length > 0 && pairList.length > 0 &&
Amounts.cmp(pairList[pairList.length - 1].value, left[li].value) === 0 pairList[pairList.length - 1].group === left[li].group
? pairList[pairList.length - 1].until ? pairList[pairList.length - 1].until
: left[li].from; : left[li].from;
while (li < left.length) { while (li < left.length) {
@ -226,7 +238,7 @@ export function createDenominationPairTimeline(
right: undefined, right: undefined,
from: timeCut, from: timeCut,
until: left[li].until, until: left[li].until,
value: left[li].value, group: left[li].group,
}); });
timeCut = left[li].until; timeCut = left[li].until;
li++; li++;
@ -235,7 +247,7 @@ export function createDenominationPairTimeline(
if (ri < right.length) { if (ri < right.length) {
let timeCut = let timeCut =
pairList.length > 0 && pairList.length > 0 &&
Amounts.cmp(pairList[pairList.length - 1].value, right[ri].value) === 0 pairList[pairList.length - 1].group === right[ri].group
? pairList[pairList.length - 1].until ? pairList[pairList.length - 1].until
: right[ri].from; : right[ri].from;
while (ri < right.length) { while (ri < right.length) {
@ -244,7 +256,7 @@ export function createDenominationPairTimeline(
left: undefined, left: undefined,
from: timeCut, from: timeCut,
until: right[ri].until, until: right[ri].until,
value: right[ri].value, group: right[ri].group,
}); });
timeCut = right[ri].until; timeCut = right[ri].until;
ri++; ri++;
@ -254,42 +266,70 @@ export function createDenominationPairTimeline(
} }
/** /**
* Create a usage timeline with the denominations given. * Create a usage timeline with the entity given.
* *
* If there are multiple denominations that can be used, the list will * If there are multiple entities that can be used in the same period,
* contain the one that minimize the fee cost. @see selectBestForOverlappingDenominations * the list will contain the one that minimize the fee cost.
* @see selectBestForOverlappingDenominations
* *
* @param list list of denominations * @param list list of entities
* @param periodProp property of element of the list that will be used as end of the usage period * @param idProp property used for identification
* @param periodStartProp property of element of the list that will be used as start of the usage period
* @param periodEndProp property of element of the list that will be used as end of the usage period
* @param feeProp property of the element of the list that will be used as fee reference * @param feeProp property of the element of the list that will be used as fee reference
* @param groupProp property of the element of the list that will be used for grouping
* @returns list of @type {FeeDescription} sorted by usage period * @returns list of @type {FeeDescription} sorted by usage period
*/ */
export function createDenominationTimeline( export function createTimeline<Type extends object>(
list: DenominationInfo[], list: Type[],
periodProp: PropsWithReturnType<DenominationInfo, TalerProtocolTimestamp>, idProp: PropsWithReturnType<Type, string>,
feeProp: PropsWithReturnType<DenominationInfo, AmountJson>, periodStartProp: PropsWithReturnType<Type, TalerProtocolTimestamp>,
periodEndProp: PropsWithReturnType<Type, TalerProtocolTimestamp>,
feeProp: PropsWithReturnType<Type, AmountJson>,
groupProp: PropsWithReturnType<Type, string> | undefined,
selectBestForOverlapping: (l: Type[]) => Type | undefined,
): FeeDescription[] { ): FeeDescription[] {
const points = list /**
* First we create a list with with point in the timeline sorted
* by time and categorized by starting or ending.
*/
const sortedPointsInTime = list
.reduce((ps, denom) => { .reduce((ps, denom) => {
//exclude denoms with bad configuration //exclude denoms with bad configuration
if (denom.stampStart.t_s >= denom[periodProp].t_s) { const id = denom[idProp] as string;
throw Error(`denom ${denom.denomPubHash} has start after the end`); const stampStart = denom[periodStartProp] as TalerProtocolTimestamp;
// return ps; const stampEnd = denom[periodEndProp] as TalerProtocolTimestamp;
const fee = denom[feeProp] as AmountJson;
const group = !groupProp ? "" : (denom[groupProp] as string);
if (!id) {
throw Error(
`denomination without hash ${JSON.stringify(denom, undefined, 2)}`,
);
}
if (stampStart.t_s >= stampEnd.t_s) {
throw Error(`denom ${id} has start after the end`);
} }
ps.push({ ps.push({
type: "start", type: "start",
moment: AbsoluteTime.fromTimestamp(denom.stampStart), fee,
group,
id,
moment: AbsoluteTime.fromTimestamp(stampStart),
denom, denom,
}); });
ps.push({ ps.push({
type: "end", type: "end",
moment: AbsoluteTime.fromTimestamp(denom[periodProp]), fee,
group,
id,
moment: AbsoluteTime.fromTimestamp(stampEnd),
denom, denom,
}); });
return ps; return ps;
}, [] as TimePoint[]) }, [] as TimePoint<Type>[])
.sort((a, b) => { .sort((a, b) => {
const v = Amounts.cmp(a.denom.value, b.denom.value); const v = a.group == b.group ? 0 : a.group > b.group ? 1 : -1;
if (v != 0) return v; if (v != 0) return v;
const t = AbsoluteTime.cmp(a.moment, b.moment); const t = AbsoluteTime.cmp(a.moment, b.moment);
if (t != 0) return t; if (t != 0) return t;
@ -297,21 +337,15 @@ export function createDenominationTimeline(
return a.type === "start" ? 1 : -1; return a.type === "start" ? 1 : -1;
}); });
const activeAtTheSameTime: DenominationInfo[] = []; const activeAtTheSameTime: Type[] = [];
return points.reduce((result, cursor, idx) => { return sortedPointsInTime.reduce((result, cursor, idx) => {
const hash = cursor.denom.denomPubHash; /**
if (!hash) * Now that we have move one step forward, we should
throw Error( * update the previous element ending period with the
`denomination without hash ${JSON.stringify( * current start time.
cursor.denom, */
undefined,
2,
)}`,
);
let prev = result.length > 0 ? result[result.length - 1] : undefined; let prev = result.length > 0 ? result[result.length - 1] : undefined;
const prevHasSameValue = const prevHasSameValue = prev && prev.group == cursor.group;
prev && Amounts.cmp(prev.value, cursor.denom.value) === 0;
if (prev) { if (prev) {
if (prevHasSameValue) { if (prevHasSameValue) {
prev.until = cursor.moment; prev.until = cursor.moment;
@ -326,11 +360,15 @@ export function createDenominationTimeline(
} }
} }
//update the activeAtTheSameTime list /**
* With the current moment in the iteration we
* should keep updated which entities are current
* active in this period of time.
*/
if (cursor.type === "end") { if (cursor.type === "end") {
const loc = activeAtTheSameTime.findIndex((v) => v.denomPubHash === hash); const loc = activeAtTheSameTime.findIndex((v) => v[idProp] === cursor.id);
if (loc === -1) { if (loc === -1) {
throw Error(`denomination ${hash} has an end but no start`); throw Error(`denomination ${cursor.id} has an end but no start`);
} }
activeAtTheSameTime.splice(loc, 1); activeAtTheSameTime.splice(loc, 1);
} else if (cursor.type === "start") { } else if (cursor.type === "start") {
@ -340,12 +378,16 @@ export function createDenominationTimeline(
throw new Error(`not TimePoint defined for type: ${exhaustiveCheck}`); throw new Error(`not TimePoint defined for type: ${exhaustiveCheck}`);
} }
if (idx == points.length - 1) { if (idx == sortedPointsInTime.length - 1) {
//this is the last element in the list, prevent adding /**
//a gap in the end * This is the last element in the list, if we continue
* a gap will normally be added which is not necessary.
* Also, the last element should be ending and the list of active
* element should be empty
*/
if (cursor.type !== "end") { if (cursor.type !== "end") {
throw Error( throw Error(
`denomination ${hash} starts after ending or doesn't have an ending`, `denomination ${cursor.id} starts after ending or doesn't have an ending`,
); );
} }
if (activeAtTheSameTime.length > 0) { if (activeAtTheSameTime.length > 0) {
@ -356,26 +398,36 @@ export function createDenominationTimeline(
return result; return result;
} }
const current = selectBestForOverlappingDenominations(activeAtTheSameTime); const current = selectBestForOverlapping(activeAtTheSameTime);
if (current) { if (current) {
/**
* We have a candidate to add in the list, check that we are
* not adding a duplicate.
* Next element in the list will defined the ending.
*/
const currentFee = current[feeProp] as AmountJson;
if ( if (
prev === undefined || //is the first prev === undefined || //is the first
!prev.fee || //is a gap !prev.fee || //is a gap
Amounts.cmp(prev.fee, current[feeProp]) !== 0 // prev has the same fee Amounts.cmp(prev.fee, currentFee) !== 0 // prev has different fee
) { ) {
result.push({ result.push({
value: cursor.denom.value, group: cursor.group,
from: cursor.moment, from: cursor.moment,
until: AbsoluteTime.never(), //not yet known until: AbsoluteTime.never(), //not yet known
fee: current[feeProp], fee: currentFee,
}); });
} else { } else {
prev.until = cursor.moment; prev.until = cursor.moment;
} }
} else { } else {
/**
* No active element in this period of time, so we add a gap (no fee)
* Next element in the list will defined the ending.
*/
result.push({ result.push({
value: cursor.denom.value, group: cursor.group,
from: cursor.moment, from: cursor.moment,
until: AbsoluteTime.never(), //not yet known until: AbsoluteTime.never(), //not yet known
}); });

View File

@ -72,6 +72,7 @@ import {
CoinDumpJson, CoinDumpJson,
CoreApiResponse, CoreApiResponse,
DenominationInfo, DenominationInfo,
DenomOperationMap,
Duration, Duration,
durationFromSpec, durationFromSpec,
durationMin, durationMin,
@ -86,11 +87,9 @@ import {
Logger, Logger,
ManualWithdrawalDetails, ManualWithdrawalDetails,
NotificationType, NotificationType,
OperationMap,
parsePaytoUri, parsePaytoUri,
RefreshReason, RefreshReason,
TalerErrorCode, TalerErrorCode,
TalerErrorDetail,
URL, URL,
WalletCoreVersion, WalletCoreVersion,
WalletNotification, WalletNotification,
@ -103,7 +102,6 @@ import {
import { clearDatabase } from "./db-utils.js"; import { clearDatabase } from "./db-utils.js";
import { import {
AuditorTrustRecord, AuditorTrustRecord,
CoinRecord,
CoinSourceType, CoinSourceType,
CoinStatus, CoinStatus,
DenominationRecord, DenominationRecord,
@ -111,11 +109,7 @@ import {
importDb, importDb,
WalletStoresV1, WalletStoresV1,
} from "./db.js"; } from "./db.js";
import { import { getErrorDetailFromException, TalerError } from "./errors.js";
getErrorDetailFromException,
makeErrorDetail,
TalerError,
} from "./errors.js";
import { import {
ActiveLongpollInfo, ActiveLongpollInfo,
ExchangeOperations, ExchangeOperations,
@ -142,11 +136,7 @@ import {
} from "./operations/backup/index.js"; } from "./operations/backup/index.js";
import { setWalletDeviceId } from "./operations/backup/state.js"; import { setWalletDeviceId } from "./operations/backup/state.js";
import { getBalances } from "./operations/balance.js"; import { getBalances } from "./operations/balance.js";
import { import { runOperationWithErrorReporting } from "./operations/common.js";
runOperationWithErrorReporting,
storeOperationError,
storeOperationPending,
} from "./operations/common.js";
import { import {
createDepositGroup, createDepositGroup,
getFeeForDeposit, getFeeForDeposit,
@ -216,23 +206,23 @@ import {
} from "./operations/withdraw.js"; } from "./operations/withdraw.js";
import { PendingTaskInfo, PendingTaskType } from "./pending-types.js"; import { PendingTaskInfo, PendingTaskType } from "./pending-types.js";
import { assertUnreachable } from "./util/assertUnreachable.js"; import { assertUnreachable } from "./util/assertUnreachable.js";
import { createDenominationTimeline } from "./util/denominations.js"; import {
createTimeline,
selectBestForOverlappingDenominations,
selectMinimumFee,
} from "./util/denominations.js";
import { import {
HttpRequestLibrary, HttpRequestLibrary,
readSuccessResponseJsonOrThrow, readSuccessResponseJsonOrThrow,
} from "./util/http.js"; } from "./util/http.js";
import { checkDbInvariant, checkLogicInvariant } from "./util/invariants.js"; import { checkDbInvariant } from "./util/invariants.js";
import { import {
AsyncCondition, AsyncCondition,
OpenedPromise, OpenedPromise,
openPromise, openPromise,
} from "./util/promiseUtils.js"; } from "./util/promiseUtils.js";
import { DbAccess, GetReadWriteAccess } from "./util/query.js"; import { DbAccess, GetReadWriteAccess } from "./util/query.js";
import { import { OperationAttemptResult } from "./util/retries.js";
OperationAttemptResult,
OperationAttemptResultType,
RetryInfo,
} from "./util/retries.js";
import { TimerAPI, TimerGroup } from "./util/timer.js"; import { TimerAPI, TimerGroup } from "./util/timer.js";
import { import {
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
@ -702,6 +692,7 @@ async function getExchangeDetailedInfo(
paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri), paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
auditors: exchangeDetails.auditors, auditors: exchangeDetails.auditors,
wireInfo: exchangeDetails.wireInfo, wireInfo: exchangeDetails.wireInfo,
globalFees: exchangeDetails.globalFees,
}, },
denominations, denominations,
}; };
@ -711,32 +702,111 @@ async function getExchangeDetailedInfo(
throw Error(`exchange with base url "${exchangeBaseurl}" not found`); throw Error(`exchange with base url "${exchangeBaseurl}" not found`);
} }
const feesDescription: OperationMap<FeeDescription[]> = { const denoms = exchange.denominations.map((d) => ({
deposit: createDenominationTimeline( ...d,
exchange.denominations, group: Amounts.stringifyValue(d.value),
}));
const denomFees: DenomOperationMap<FeeDescription[]> = {
deposit: createTimeline(
denoms,
"denomPubHash",
"stampStart",
"stampExpireDeposit", "stampExpireDeposit",
"feeDeposit", "feeDeposit",
"group",
selectBestForOverlappingDenominations,
), ),
refresh: createDenominationTimeline( refresh: createTimeline(
exchange.denominations, denoms,
"denomPubHash",
"stampStart",
"stampExpireWithdraw", "stampExpireWithdraw",
"feeRefresh", "feeRefresh",
"group",
selectBestForOverlappingDenominations,
), ),
refund: createDenominationTimeline( refund: createTimeline(
exchange.denominations, denoms,
"denomPubHash",
"stampStart",
"stampExpireWithdraw", "stampExpireWithdraw",
"feeRefund", "feeRefund",
"group",
selectBestForOverlappingDenominations,
), ),
withdraw: createDenominationTimeline( withdraw: createTimeline(
exchange.denominations, denoms,
"denomPubHash",
"stampStart",
"stampExpireWithdraw", "stampExpireWithdraw",
"feeWithdraw", "feeWithdraw",
"group",
selectBestForOverlappingDenominations,
), ),
}; };
const transferFees = Object.entries(
exchange.info.wireInfo.feesForType,
).reduce((prev, [wireType, infoForType]) => {
const feesByGroup = [
...infoForType.map((w) => ({
...w,
fee: w.closingFee,
group: "closing",
})),
...infoForType.map((w) => ({ ...w, fee: w.wadFee, group: "wad" })),
...infoForType.map((w) => ({ ...w, fee: w.wireFee, group: "wire" })),
];
prev[wireType] = createTimeline(
feesByGroup,
"sig",
"startStamp",
"endStamp",
"fee",
"group",
selectMinimumFee,
);
return prev;
}, {} as Record<string, FeeDescription[]>);
const globalFeesByGroup = [
...exchange.info.globalFees.map((w) => ({
...w,
fee: w.accountFee,
group: "account",
})),
...exchange.info.globalFees.map((w) => ({
...w,
fee: w.historyFee,
group: "history",
})),
...exchange.info.globalFees.map((w) => ({
...w,
fee: w.kycFee,
group: "kyc",
})),
...exchange.info.globalFees.map((w) => ({
...w,
fee: w.purseFee,
group: "purse",
})),
];
const globalFees = createTimeline(
globalFeesByGroup,
"signature",
"startDate",
"endDate",
"fee",
"group",
selectMinimumFee,
);
return { return {
...exchange.info, ...exchange.info,
feesDescription, denomFees,
transferFees,
globalFees,
}; };
} }

View File

@ -46,12 +46,14 @@ const exchanges: ExchangeFullDetails[] = [
denomination_keys: [], denomination_keys: [],
}, },
], ],
feesDescription: { denomFees: {
deposit: [], deposit: [],
refresh: [], refresh: [],
refund: [], refund: [],
withdraw: [], withdraw: [],
}, },
globalFees: [],
transferFees: {},
wireInfo: { wireInfo: {
accounts: [], accounts: [],
feesForType: {}, feesForType: {},

View File

@ -15,15 +15,15 @@
*/ */
import { import {
FeeDescription, DenomOperationMap,
FeeDescriptionPair,
AbsoluteTime,
ExchangeFullDetails, ExchangeFullDetails,
OperationMap, ExchangeListItem, FeeDescriptionPair
ExchangeListItem,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js"; import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";
import {
State as SelectExchangeState
} from "../../hooks/useSelectedExchange.js";
import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js"; import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import * as wxApi from "../../wxApi.js"; import * as wxApi from "../../wxApi.js";
@ -32,7 +32,7 @@ import {
ComparingView, ComparingView,
ErrorLoadingView, ErrorLoadingView,
NoExchangesView, NoExchangesView,
ReadyView, ReadyView
} from "./views.js"; } from "./views.js";
export interface Props { export interface Props {
@ -41,9 +41,6 @@ export interface Props {
onCancel: () => Promise<void>; onCancel: () => Promise<void>;
onSelection: (exchange: string) => Promise<void>; onSelection: (exchange: string) => Promise<void>;
} }
import {
State as SelectExchangeState
} from "../../hooks/useSelectedExchange.js";
export type State = export type State =
| State.Loading | State.Loading
@ -71,13 +68,12 @@ export namespace State {
export interface Ready extends BaseInfo { export interface Ready extends BaseInfo {
status: "ready"; status: "ready";
timeline: OperationMap<FeeDescription[]>;
onClose: ButtonHandler; onClose: ButtonHandler;
} }
export interface Comparing extends BaseInfo { export interface Comparing extends BaseInfo {
status: "comparing"; status: "comparing";
pairTimeline: OperationMap<FeeDescriptionPair[]>; pairTimeline: DenomOperationMap<FeeDescriptionPair[]>;
onReset: ButtonHandler; onReset: ButtonHandler;
onSelect: ButtonHandler; onSelect: ButtonHandler;
} }

View File

@ -14,8 +14,8 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { FeeDescription, OperationMap } from "@gnu-taler/taler-util"; import { DenomOperationMap, FeeDescription } from "@gnu-taler/taler-util";
import { createDenominationPairTimeline } from "@gnu-taler/taler-wallet-core"; import { createPairTimeline } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import * as wxApi from "../../wxApi.js"; import * as wxApi from "../../wxApi.js";
@ -94,27 +94,26 @@ export function useComponentState(
onClick: onCancel, onClick: onCancel,
}, },
selected, selected,
timeline: selected.feesDescription,
}; };
} }
const pairTimeline: OperationMap<FeeDescription[]> = { const pairTimeline: DenomOperationMap<FeeDescription[]> = {
deposit: createDenominationPairTimeline( deposit: createPairTimeline(
selected.feesDescription.deposit, selected.denomFees.deposit,
original.feesDescription.deposit, original.denomFees.deposit,
), ),
refresh: createDenominationPairTimeline( refresh: createPairTimeline(
selected.feesDescription.refresh, selected.denomFees.refresh,
original.feesDescription.refresh, original.denomFees.refresh,
), ),
refund: createDenominationPairTimeline( refund: createPairTimeline(
selected.feesDescription.refund, selected.denomFees.refund,
original.feesDescription.refund, original.denomFees.refund,
),
withdraw: createDenominationPairTimeline(
selected.feesDescription.withdraw,
original.feesDescription.withdraw,
), ),
withdraw: createPairTimeline(
selected.denomFees.withdraw,
original.denomFees.withdraw,
)
}; };
return { return {

View File

@ -28,71 +28,72 @@ export default {
export const Bitcoin1 = createExample(ReadyView, { export const Bitcoin1 = createExample(ReadyView, {
exchanges: { exchanges: {
list: { "http://exchange": "http://exchange" }, list: { "0": "https://exchange.taler.ar" },
value: "http://exchange", value: "0",
}, },
selected: { selected: {
currency: "BITCOINBTC", currency: "BITCOINBTC",
auditors: [], auditors: [],
exchangeBaseUrl: "https://exchange.taler.ar",
denomFees: timelineExample(),
transferFees: {},
globalFees: [],
} as any, } as any,
onClose: {}, onClose: {},
timeline: {
deposit: [],
refresh: [],
refund: [],
withdraw: [],
},
}); });
export const Bitcoin2 = createExample(ReadyView, { export const Bitcoin2 = createExample(ReadyView, {
exchanges: { exchanges: {
list: { "http://exchange": "http://exchange" }, list: {
value: "http://exchange", "https://exchange.taler.ar": "https://exchange.taler.ar",
"https://exchange-btc.taler.ar": "https://exchange-btc.taler.ar",
},
value: "https://exchange.taler.ar",
}, },
selected: { selected: {
currency: "BITCOINBTC", currency: "BITCOINBTC",
auditors: [], auditors: [],
exchangeBaseUrl: "https://exchange.taler.ar",
denomFees: timelineExample(),
transferFees: {},
globalFees: [],
} as any, } as any,
onClose: {}, onClose: {},
timeline: {
deposit: [],
refresh: [],
refund: [],
withdraw: [],
},
}); });
export const Kudos1 = createExample(ReadyView, { export const Kudos1 = createExample(ReadyView, {
exchanges: { exchanges: {
list: { "http://exchange": "http://exchange" }, list: {
value: "http://exchange", "https://exchange-kudos.taler.ar": "https://exchange-kudos.taler.ar",
},
value: "https://exchange-kudos.taler.ar",
}, },
selected: { selected: {
currency: "BITCOINBTC", currency: "BITCOINBTC",
auditors: [], auditors: [],
exchangeBaseUrl: "https://exchange.taler.ar",
denomFees: timelineExample(),
transferFees: {},
globalFees: [],
} as any, } as any,
onClose: {}, onClose: {},
timeline: {
deposit: [],
refresh: [],
refund: [],
withdraw: [],
},
}); });
export const Kudos2 = createExample(ReadyView, { export const Kudos2 = createExample(ReadyView, {
exchanges: { exchanges: {
list: { "http://exchange": "http://exchange" }, list: {
value: "http://exchange", "https://exchange-kudos.taler.ar": "https://exchange-kudos.taler.ar",
"https://exchange-kudos2.taler.ar": "https://exchange-kudos2.taler.ar",
},
value: "https://exchange-kudos.taler.ar",
}, },
selected: { selected: {
currency: "BITCOINBTC", currency: "BITCOINBTC",
auditors: [], auditors: [],
exchangeBaseUrl: "https://exchange.taler.ar",
denomFees: timelineExample(),
transferFees: {},
globalFees: [],
} as any, } as any,
onClose: {}, onClose: {},
timeline: {
deposit: [],
refresh: [],
refund: [],
withdraw: [],
},
}); });
export const ComparingBitcoin = createExample(ComparingView, { export const ComparingBitcoin = createExample(ComparingView, {
exchanges: { exchanges: {
@ -102,6 +103,9 @@ export const ComparingBitcoin = createExample(ComparingView, {
selected: { selected: {
currency: "BITCOINBTC", currency: "BITCOINBTC",
auditors: [], auditors: [],
exchangeBaseUrl: "https://exchange.taler.ar",
transferFees: {},
globalFees: [],
} as any, } as any,
onReset: {}, onReset: {},
onSelect: {}, onSelect: {},
@ -121,6 +125,9 @@ export const ComparingKudos = createExample(ComparingView, {
selected: { selected: {
currency: "KUDOS", currency: "KUDOS",
auditors: [], auditors: [],
exchangeBaseUrl: "https://exchange.taler.ar",
transferFees: {},
globalFees: [],
} as any, } as any,
onReset: {}, onReset: {},
onSelect: {}, onSelect: {},
@ -132,3 +139,400 @@ export const ComparingKudos = createExample(ComparingView, {
withdraw: [], withdraw: [],
}, },
}); });
function timelineExample() {
return {
deposit: [
{
group: "0.1",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1916386904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
{
group: "1",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1916386904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
{
group: "10",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1916386904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
{
group: "1000",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1916386904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
{
group: "2",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1916386904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
{
group: "5",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1916386904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
],
refresh: [
{
group: "0.1",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1758706904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
{
group: "1",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1758706904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
{
group: "10",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1758706904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
{
group: "1000",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1758706904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
{
group: "2",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1758706904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
{
group: "5",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1758706904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
],
refund: [
{
group: "0.1",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1758706904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
{
group: "1",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1758706904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
{
group: "10",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1758706904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
{
group: "1000",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1758706904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
{
group: "2",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1758706904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
{
group: "5",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1758706904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
],
withdraw: [
{
group: "0.1",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1758706904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
{
group: "1",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1758706904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
{
group: "10",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1758706904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
{
group: "1000",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1758706904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
{
group: "2",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1758706904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
{
group: "5",
from: {
t_ms: 1664098904000,
},
until: {
t_ms: 1758706904000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
],
wad: [
{
group: "iban",
from: {
t_ms: 1640995200000,
},
until: {
t_ms: 1798761600000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
],
wire: [
{
group: "iban",
from: {
t_ms: 1640995200000,
},
until: {
t_ms: 1798761600000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
],
closing: [
{
group: "iban",
from: {
t_ms: 1640995200000,
},
until: {
t_ms: 1798761600000,
},
fee: {
currency: "KUDOS",
fraction: 1000000,
value: 0,
},
},
],
};
}

View File

@ -31,9 +31,7 @@ import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js"; import { Button } from "../../mui/Button.js";
import arrowDown from "../../svg/chevron-down.svg"; import arrowDown from "../../svg/chevron-down.svg";
import { State } from "./index.js"; import { State } from "./index.js";
import { import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
State as SelectExchangeState
} from "../../hooks/useSelectedExchange.js";
const ButtonGroup = styled.div` const ButtonGroup = styled.div`
& > button { & > button {
@ -59,7 +57,7 @@ const FeeDescriptionTable = styled.table`
} }
td.value { td.value {
text-align: right; text-align: right;
width: 1%; width: 15%;
white-space: nowrap; white-space: nowrap;
} }
td.icon { td.icon {
@ -109,26 +107,28 @@ export function ErrorLoadingView({ error }: State.LoadingUriError): VNode {
return ( return (
<LoadingError <LoadingError
title={<i18n.Translate>Could not load tip status</i18n.Translate>} title={<i18n.Translate>Could not load exchange fees</i18n.Translate>}
error={error} error={error}
/> />
); );
} }
export function NoExchangesView({
currency,
export function NoExchangesView({currency}: SelectExchangeState.NoExchange): VNode { }: SelectExchangeState.NoExchange): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
if (!currency) { if (!currency) {
return ( return (
<div> <div>
<i18n.Translate>could not find any exchange</i18n.Translate> <i18n.Translate>could not find any exchange</i18n.Translate>
</div> </div>
); );
} }
return ( return (
<div> <div>
<i18n.Translate>could not find any exchange for the currency {currency}</i18n.Translate> <i18n.Translate>
could not find any exchange for the currency {currency}
</i18n.Translate>
</div> </div>
); );
} }
@ -356,7 +356,6 @@ export function ReadyView({
exchanges, exchanges,
selected, selected,
onClose, onClose,
timeline,
}: State.Ready): VNode { }: State.Ready): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
@ -365,7 +364,10 @@ export function ReadyView({
<h2> <h2>
<i18n.Translate>Service fee description</i18n.Translate> <i18n.Translate>Service fee description</i18n.Translate>
</h2> </h2>
<p>
All fee indicated below are in the same and only currency the exchange
works.
</p>
<section> <section>
<div <div
style={{ style={{
@ -375,21 +377,27 @@ export function ReadyView({
justifyContent: "space-between", justifyContent: "space-between",
}} }}
> >
<p> {Object.keys(exchanges.list).length === 1 ? (
<Input> <Fragment>
<SelectList <p>Exchange: {selected.exchangeBaseUrl}</p>
label={ </Fragment>
<i18n.Translate> ) : (
Select {selected.currency} exchange <p>
</i18n.Translate> <Input>
} <SelectList
list={exchanges.list} label={
name="lang" <i18n.Translate>
value={exchanges.value} Select {selected.currency} exchange
onChange={exchanges.onChange} </i18n.Translate>
/> }
</Input> list={exchanges.list}
</p> name="lang"
value={exchanges.value}
onChange={exchanges.onChange}
/>
</Input>
</p>
)}
<Button variant="outlined" onClick={onClose.onClick}> <Button variant="outlined" onClick={onClose.onClick}>
<i18n.Translate>Close</i18n.Translate> <i18n.Translate>Close</i18n.Translate>
</Button> </Button>
@ -411,16 +419,25 @@ export function ReadyView({
<table> <table>
<tr> <tr>
<td> <td>
<i18n.Translate>currency</i18n.Translate> <i18n.Translate>Currency</i18n.Translate>
</td>
<td>
<b>{selected.currency}</b>
</td> </td>
<td>{selected.currency}</td>
</tr> </tr>
</table> </table>
</section> </section>
<section> <section>
<h2> <h2>
<i18n.Translate>Operations</i18n.Translate> <i18n.Translate>Coin operations</i18n.Translate>
</h2> </h2>
<p>
<i18n.Translate>
Every operation in this section may be different by denomination
value and is valid for a period of time. The exchange will charge
the indicated amount every time a coin is used in such operation.
</i18n.Translate>
</p>
<p> <p>
<i18n.Translate>Deposits</i18n.Translate> <i18n.Translate>Deposits</i18n.Translate>
</p> </p>
@ -440,7 +457,10 @@ export function ReadyView({
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<RenderFeeDescriptionByValue first={timeline.deposit} /> <RenderFeeDescriptionByValue
list={selected.denomFees.deposit}
sorting={(a, b) => Number(a) - Number(b)}
/>
</tbody> </tbody>
</FeeDescriptionTable> </FeeDescriptionTable>
<p> <p>
@ -462,7 +482,10 @@ export function ReadyView({
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<RenderFeeDescriptionByValue first={timeline.withdraw} /> <RenderFeeDescriptionByValue
list={selected.denomFees.withdraw}
sorting={(a, b) => Number(a) - Number(b)}
/>
</tbody> </tbody>
</FeeDescriptionTable> </FeeDescriptionTable>
<p> <p>
@ -484,7 +507,10 @@ export function ReadyView({
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<RenderFeeDescriptionByValue first={timeline.refund} /> <RenderFeeDescriptionByValue
list={selected.denomFees.refund}
sorting={(a, b) => Number(a) - Number(b)}
/>
</tbody> </tbody>
</FeeDescriptionTable>{" "} </FeeDescriptionTable>{" "}
<p> <p>
@ -506,53 +532,81 @@ export function ReadyView({
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<RenderFeeDescriptionByValue first={timeline.refresh} /> <RenderFeeDescriptionByValue
list={selected.denomFees.refresh}
sorting={(a, b) => Number(a) - Number(b)}
/>
</tbody> </tbody>
</FeeDescriptionTable>{" "} </FeeDescriptionTable>
</section> </section>
<section> <section>
<table> <h2>
<i18n.Translate>Transfer operations</i18n.Translate>
</h2>
<p>
<i18n.Translate>
Every operation in this section may be different by transfer type
and is valid for a period of time. The exchange will charge the
indicated amount every time a transfer is made.
</i18n.Translate>
</p>
{Object.entries(selected.transferFees).map(([type, fees], idx) => {
return (
<Fragment key={idx}>
<p>{type}</p>
<FeeDescriptionTable>
<thead>
<tr>
<th>&nbsp;</th>
<th>
<i18n.Translate>Operation</i18n.Translate>
</th>
<th class="fee">
<i18n.Translate>Fee</i18n.Translate>
</th>
<th>
<i18n.Translate>Until</i18n.Translate>
</th>
</tr>
</thead>
<tbody>
<RenderFeeDescriptionByValue list={fees} />
</tbody>
</FeeDescriptionTable>
</Fragment>
);
})}
</section>
<section>
<h2>
<i18n.Translate>Wallet operations</i18n.Translate>
</h2>
<p>
<i18n.Translate>
Every operation in this section may be different by transfer type
and is valid for a period of time. The exchange will charge the
indicated amount every time a transfer is made.
</i18n.Translate>
</p>
<FeeDescriptionTable>
<thead> <thead>
<tr> <tr>
<td> <th>&nbsp;</th>
<i18n.Translate>Wallet operations</i18n.Translate> <th>
</td> <i18n.Translate>Feature</i18n.Translate>
<td> </th>
<th class="fee">
<i18n.Translate>Fee</i18n.Translate> <i18n.Translate>Fee</i18n.Translate>
</td> </th>
<th>
<i18n.Translate>Until</i18n.Translate>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <RenderFeeDescriptionByValue list={selected.globalFees} />
<td>history(i) </td>
<td>0.1</td>
</tr>
<tr>
<td>kyc (i) </td>
<td>0.1</td>
</tr>
<tr>
<td>account (i) </td>
<td>0.1</td>
</tr>
<tr>
<td>purse (i) </td>
<td>0.1</td>
</tr>
<tr>
<td>wire SEPA (i) </td>
<td>0.1</td>
</tr>
<tr>
<td>closing SEPA(i) </td>
<td>0.1</td>
</tr>
<tr>
<td>wad SEPA (i) </td>
<td>0.1</td>
</tr>
</tbody> </tbody>
</table> </FeeDescriptionTable>
</section> </section>
<section> <section>
<ButtonGroup> <ButtonGroup>
@ -579,7 +633,7 @@ function FeeDescriptionRowsGroup({
<tr <tr
key={idx} key={idx}
class="value" class="value"
data-hasMore={!hasMoreInfo} data-hasMore={hasMoreInfo}
data-main={main} data-main={main}
data-hidden={!main && !expanded} data-hidden={!main && !expanded}
onClick={() => setExpand((p) => !p)} onClick={() => setExpand((p) => !p)}
@ -594,9 +648,7 @@ function FeeDescriptionRowsGroup({
/> />
) : undefined} ) : undefined}
</td> </td>
<td class="value"> <td class="value">{main ? info.group : ""}</td>
{main ? <Amount value={info.value} hideCurrency /> : ""}
</td>
{info.fee ? ( {info.fee ? (
<td class="fee">{<Amount value={info.fee} hideCurrency />}</td> <td class="fee">{<Amount value={info.fee} hideCurrency />}</td>
) : undefined} ) : undefined}
@ -621,7 +673,7 @@ function FeePairRowsGroup({ infos }: { infos: FeeDescriptionPair[] }): VNode {
<tr <tr
key={idx} key={idx}
class="value" class="value"
data-hasMore={!hasMoreInfo} data-hasMore={hasMoreInfo}
data-main={main} data-main={main}
data-hidden={!main && !expanded} data-hidden={!main && !expanded}
onClick={() => setExpand((p) => !p)} onClick={() => setExpand((p) => !p)}
@ -636,9 +688,7 @@ function FeePairRowsGroup({ infos }: { infos: FeeDescriptionPair[] }): VNode {
/> />
) : undefined} ) : undefined}
</td> </td>
<td class="value"> <td class="value">{main ? info.group : ""}</td>
{main ? <Amount value={info.value} hideCurrency /> : ""}
</td>
{info.left ? ( {info.left ? (
<td class="fee">{<Amount value={info.left} hideCurrency />}</td> <td class="fee">{<Amount value={info.left} hideCurrency />}</td>
) : ( ) : (
@ -673,7 +723,7 @@ function RenderFeePairByValue({ list }: { list: FeeDescriptionPair[] }): VNode {
const next = idx >= list.length - 1 ? undefined : list[idx + 1]; const next = idx >= list.length - 1 ? undefined : list[idx + 1];
const nextIsMoreInfo = const nextIsMoreInfo =
next !== undefined && Amounts.cmp(next.value, info.value) === 0; next !== undefined && next.group === info.group;
prev.rows.push(info); prev.rows.push(info);
@ -681,7 +731,7 @@ function RenderFeePairByValue({ list }: { list: FeeDescriptionPair[] }): VNode {
return prev; return prev;
} }
prev.rows = []; // prev.rows = [];
prev.views.push(<FeePairRowsGroup infos={prev.rows} />); prev.views.push(<FeePairRowsGroup infos={prev.rows} />);
return prev; return prev;
}, },
@ -701,36 +751,21 @@ function RenderFeePairByValue({ list }: { list: FeeDescriptionPair[] }): VNode {
* @returns * @returns
*/ */
function RenderFeeDescriptionByValue({ function RenderFeeDescriptionByValue({
first, list,
sorting,
}: { }: {
first: FeeDescription[]; list: FeeDescription[];
sorting?: (a: string, b: string) => number;
}): VNode { }): VNode {
return ( const grouped = list.reduce((prev, cur) => {
<Fragment> if (!prev[cur.group]) {
{ prev[cur.group] = [];
first.reduce( }
(prev, info, idx) => { prev[cur.group].push(cur);
const next = idx >= first.length - 1 ? undefined : first[idx + 1]; return prev;
}, {} as Record<string, FeeDescription[]>);
const nextIsMoreInfo = const p = Object.keys(grouped)
next !== undefined && Amounts.cmp(next.value, info.value) === 0; .sort(sorting)
.map((i, idx) => <FeeDescriptionRowsGroup key={idx} infos={grouped[i]} />);
prev.rows.push(info); return <Fragment>{p}</Fragment>;
if (nextIsMoreInfo) {
return prev;
}
prev.rows = [];
prev.views.push(<FeeDescriptionRowsGroup infos={prev.rows} />);
return prev;
},
{ rows: [], views: [] } as {
rows: FeeDescription[];
views: h.JSX.Element[];
},
).views
}
</Fragment>
);
} }