aboutsummaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/taler-util/src/backupTypes.ts26
-rw-r--r--packages/taler-util/src/walletTypes.ts54
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/rpcClient.ts1
-rw-r--r--packages/taler-wallet-core/src/db.ts3
-rw-r--r--packages/taler-wallet-core/src/operations/backup/export.ts14
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts15
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts19
-rw-r--r--packages/taler-wallet-core/src/util/denominations.test.ts197
-rw-r--r--packages/taler-wallet-core/src/util/denominations.ts200
-rw-r--r--packages/taler-wallet-core/src/wallet.ts130
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/test.ts4
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts18
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts33
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ExchangeSelection/stories.tsx468
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx255
15 files changed, 1059 insertions, 378 deletions
diff --git a/packages/taler-util/src/backupTypes.ts b/packages/taler-util/src/backupTypes.ts
index 8222bdeab..a1506e90f 100644
--- a/packages/taler-util/src/backupTypes.ts
+++ b/packages/taler-util/src/backupTypes.ts
@@ -1091,17 +1091,21 @@ export interface BackupExchangeWireFee {
*
*/
export interface BackupExchangeGlobalFees {
- start_date: TalerProtocolTimestamp;
- end_date: TalerProtocolTimestamp;
- kyc_fee: BackupAmountString;
- history_fee: BackupAmountString;
- account_fee: BackupAmountString;
- purse_fee: BackupAmountString;
- history_expiration: TalerProtocolDuration;
- account_kyc_timeout: TalerProtocolDuration;
- purse_account_limit: number;
- purse_timeout: TalerProtocolDuration;
- master_sig: string;
+ startDate: TalerProtocolTimestamp;
+ endDate: TalerProtocolTimestamp;
+
+ kycFee: BackupAmountString;
+ historyFee: BackupAmountString;
+ accountFee: BackupAmountString;
+ purseFee: BackupAmountString;
+
+ historyTimeout: TalerProtocolDuration;
+ kycTimeout: TalerProtocolDuration;
+ purseTimeout: TalerProtocolDuration;
+
+ purseLimit: number;
+
+ signature: string;
}
/**
* Structure of one exchange signing key in the /keys response.
diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts
index bb5d4b3a4..7495e02d6 100644
--- a/packages/taler-util/src/walletTypes.ts
+++ b/packages/taler-util/src/walletTypes.ts
@@ -36,6 +36,7 @@ import {
AbsoluteTime,
codecForAbsoluteTime,
codecForTimestamp,
+ TalerProtocolDuration,
TalerProtocolTimestamp,
} from "./time.js";
import {
@@ -673,6 +674,23 @@ export interface WireInfo {
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> =>
buildCodecForObject<ExchangeAccount>()
.property("payto_uri", codecForString())
@@ -752,28 +770,31 @@ export interface DenominationInfo {
exchangeBaseUrl: string;
}
-export type Operation = "deposit" | "withdraw" | "refresh" | "refund";
-export type OperationMap<T> = { [op in Operation]: T };
+export type DenomOperation = "deposit" | "withdraw" | "refresh" | "refund";
+export type DenomOperationMap<T> = { [op in DenomOperation]: T };
export interface FeeDescription {
- value: AmountJson;
+ group: string;
from: AbsoluteTime;
until: AbsoluteTime;
fee?: AmountJson;
}
export interface FeeDescriptionPair {
- value: AmountJson;
+ group: string;
from: AbsoluteTime;
until: AbsoluteTime;
left?: AmountJson;
right?: AmountJson;
}
-export interface TimePoint {
+export interface TimePoint<T> {
+ id: string;
+ group: string;
+ fee: AmountJson;
type: "start" | "end";
moment: AbsoluteTime;
- denom: DenominationInfo;
+ denom: T;
}
export interface ExchangeFullDetails {
@@ -783,7 +804,9 @@ export interface ExchangeFullDetails {
tos: ExchangeTos;
auditors: ExchangeAuditor[];
wireInfo: WireInfo;
- feesDescription: OperationMap<FeeDescription[]>;
+ denomFees: DenomOperationMap<FeeDescription[]>;
+ transferFees: Record<string, FeeDescription[]>;
+ globalFees: FeeDescription[];
}
export interface ExchangeListItem {
@@ -816,7 +839,7 @@ const codecForExchangeTos = (): Codec<ExchangeTos> =>
export const codecForFeeDescriptionPair = (): Codec<FeeDescriptionPair> =>
buildCodecForObject<FeeDescriptionPair>()
- .property("value", codecForAmountJson())
+ .property("group", codecForString())
.property("from", codecForAbsoluteTime)
.property("until", codecForAbsoluteTime)
.property("left", codecOptional(codecForAmountJson()))
@@ -825,21 +848,21 @@ export const codecForFeeDescriptionPair = (): Codec<FeeDescriptionPair> =>
export const codecForFeeDescription = (): Codec<FeeDescription> =>
buildCodecForObject<FeeDescription>()
- .property("value", codecForAmountJson())
+ .property("group", codecForString())
.property("from", codecForAbsoluteTime)
.property("until", codecForAbsoluteTime)
.property("fee", codecOptional(codecForAmountJson()))
.build("FeeDescription");
export const codecForFeesByOperations = (): Codec<
- OperationMap<FeeDescription[]>
+ DenomOperationMap<FeeDescription[]>
> =>
- buildCodecForObject<OperationMap<FeeDescription[]>>()
+ buildCodecForObject<DenomOperationMap<FeeDescription[]>>()
.property("deposit", codecForList(codecForFeeDescription()))
.property("withdraw", codecForList(codecForFeeDescription()))
.property("refresh", codecForList(codecForFeeDescription()))
.property("refund", codecForList(codecForFeeDescription()))
- .build("FeesByOperations");
+ .build("DenomOperationMap");
export const codecForExchangeFullDetails = (): Codec<ExchangeFullDetails> =>
buildCodecForObject<ExchangeFullDetails>()
@@ -849,7 +872,12 @@ export const codecForExchangeFullDetails = (): Codec<ExchangeFullDetails> =>
.property("tos", codecForExchangeTos())
.property("auditors", codecForList(codecForExchangeAuditor()))
.property("wireInfo", codecForWireInfo())
- .property("feesDescription", codecForFeesByOperations())
+ .property("denomFees", codecForFeesByOperations())
+ .property(
+ "transferFees",
+ codecForMap(codecForList(codecForFeeDescription())),
+ )
+ .property("globalFees", codecForList(codecForFeeDescription()))
.build("ExchangeFullDetails");
export const codecForExchangeListItem = (): Codec<ExchangeListItem> =>
diff --git a/packages/taler-wallet-core/src/crypto/workers/rpcClient.ts b/packages/taler-wallet-core/src/crypto/workers/rpcClient.ts
index f3a4fff1c..21d88fffa 100644
--- a/packages/taler-wallet-core/src/crypto/workers/rpcClient.ts
+++ b/packages/taler-wallet-core/src/crypto/workers/rpcClient.ts
@@ -53,7 +53,6 @@ export class CryptoRpcClient {
this.proc.unref();
this.proc.stdout.on("data", (x) => {
- // console.log("got chunk", x.toString("utf-8"));
if (x instanceof Buffer) {
const nlIndex = x.indexOf("\n");
if (nlIndex >= 0) {
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index e266275c1..125e777b8 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -46,6 +46,7 @@ import {
WireInfo,
DenominationInfo,
GlobalFees,
+ ExchangeGlobalFees,
} from "@gnu-taler/taler-util";
import { RetryInfo, RetryTags } from "./util/retries.js";
import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
@@ -428,7 +429,7 @@ export interface ExchangeDetailsRecord {
/**
* Fees for exchange services
*/
- globalFees: GlobalFees[];
+ globalFees: ExchangeGlobalFees[];
/**
* Signing keys we got from the exchange, can also contain
* older signing keys that are not returned by /keys anymore.
diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts
index f611a2380..a3c4c8d99 100644
--- a/packages/taler-wallet-core/src/operations/backup/export.ts
+++ b/packages/taler-wallet-core/src/operations/backup/export.ts
@@ -345,7 +345,19 @@ export async function exportBackup(
stamp_expire: x.stamp_expire,
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_timestamp: ex.termsOfServiceAcceptedTimestamp,
denominations:
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts
index ee8cb6f6c..e631845f6 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -405,7 +405,20 @@ export async function importBackup(
masterPublicKey: backupExchangeDetails.master_public_key,
protocolVersion: backupExchangeDetails.protocol_version,
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) => ({
key: x.key,
master_sig: x.master_sig,
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
index a26c14fcc..3da16e303 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -30,6 +30,7 @@ import {
encodeCrock,
ExchangeAuditor,
ExchangeDenomination,
+ ExchangeGlobalFees,
ExchangeSignKeyJson,
ExchangeWireJson,
GlobalFees,
@@ -274,7 +275,8 @@ async function validateGlobalFees(
ws: InternalWalletState,
fees: GlobalFees[],
masterPub: string,
-): Promise<GlobalFees[]> {
+): Promise<ExchangeGlobalFees[]> {
+ const egf: ExchangeGlobalFees[] = [];
for (const gf of fees) {
logger.trace("validating exchange global fees");
let isValid = false;
@@ -291,9 +293,22 @@ async function validateGlobalFees(
if (!isValid) {
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 {
diff --git a/packages/taler-wallet-core/src/util/denominations.test.ts b/packages/taler-wallet-core/src/util/denominations.test.ts
index 31c561e88..9c93331a3 100644
--- a/packages/taler-wallet-core/src/util/denominations.test.ts
+++ b/packages/taler-wallet-core/src/util/denominations.test.ts
@@ -28,8 +28,9 @@ import {
} from "@gnu-taler/taler-util";
// import { expect } from "chai";
import {
- createDenominationPairTimeline,
- createDenominationTimeline,
+ createPairTimeline,
+ createTimeline,
+ selectBestForOverlappingDenominations,
} from "./denominations.js";
import test, { ExecutionContext } from "ava";
@@ -42,8 +43,14 @@ const VALUES = Array.from({ length: 10 }).map((undef, t) =>
const TIMESTAMPS = Array.from({ length: 20 }).map((undef, t_s) => ({ t_s }));
const ABS_TIME = TIMESTAMPS.map((m) => AbsoluteTime.fromTimestamp(m));
-function normalize(list: DenominationInfo[]): DenominationInfo[] {
- return list.map((e, idx) => ({ ...e, denomPubHash: `id${idx}` }));
+function normalize(
+ list: DenominationInfo[],
+): (DenominationInfo & { group: string })[] {
+ return list.map((e, idx) => ({
+ ...e,
+ denomPubHash: `id${idx}`,
+ group: Amounts.stringifyValue(e.value),
+ }));
}
//Avoiding to make an error-prone/time-consuming refactor
@@ -61,7 +68,7 @@ function expect(t: ExecutionContext, thing: any): any {
// describe("single value example", (t) => {
test("should have one row with start and exp", (t) => {
- const timeline = createDenominationTimeline(
+ const timeline = createTimeline(
normalize([
{
value: VALUES[1],
@@ -70,13 +77,17 @@ test("should have one row with start and exp", (t) => {
feeDeposit: VALUES[1],
} as Partial<DenominationInfo> as DenominationInfo,
]),
+ "denomPubHash",
+ "stampStart",
"stampExpireDeposit",
"feeDeposit",
+ "group",
+ selectBestForOverlappingDenominations,
);
expect(t, timeline).deep.equal([
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1],
until: ABS_TIME[2],
fee: VALUES[1],
@@ -85,7 +96,7 @@ test("should have one row with start and exp", (t) => {
});
test("should have two rows with the second denom in the middle if second is better", (t) => {
- const timeline = createDenominationTimeline(
+ const timeline = createTimeline(
normalize([
{
value: VALUES[1],
@@ -100,19 +111,23 @@ test("should have two rows with the second denom in the middle if second is bett
feeDeposit: VALUES[2],
} as Partial<DenominationInfo> as DenominationInfo,
]),
+ "denomPubHash",
+ "stampStart",
"stampExpireDeposit",
"feeDeposit",
+ "group",
+ selectBestForOverlappingDenominations,
);
expect(t, timeline).deep.equal([
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1],
until: ABS_TIME[3],
fee: VALUES[1],
},
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[3],
until: ABS_TIME[4],
fee: VALUES[2],
@@ -121,7 +136,7 @@ test("should have two rows with the second denom in the middle if second is bett
});
test("should have two rows with the first denom in the middle if second is worse", (t) => {
- const timeline = createDenominationTimeline(
+ const timeline = createTimeline(
normalize([
{
value: VALUES[1],
@@ -136,19 +151,23 @@ test("should have two rows with the first denom in the middle if second is worse
feeDeposit: VALUES[1],
} as Partial<DenominationInfo> as DenominationInfo,
]),
+ "denomPubHash",
+ "stampStart",
"stampExpireDeposit",
"feeDeposit",
+ "group",
+ selectBestForOverlappingDenominations,
);
expect(t, timeline).deep.equal([
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1],
until: ABS_TIME[2],
fee: VALUES[2],
},
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[2],
until: ABS_TIME[4],
fee: VALUES[1],
@@ -157,7 +176,7 @@ test("should have two rows with the first denom in the middle if second is worse
});
test("should add a gap when there no fee", (t) => {
- const timeline = createDenominationTimeline(
+ const timeline = createTimeline(
normalize([
{
value: VALUES[1],
@@ -172,24 +191,28 @@ test("should add a gap when there no fee", (t) => {
feeDeposit: VALUES[1],
} as Partial<DenominationInfo> as DenominationInfo,
]),
+ "denomPubHash",
+ "stampStart",
"stampExpireDeposit",
"feeDeposit",
+ "group",
+ selectBestForOverlappingDenominations,
);
expect(t, timeline).deep.equal([
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1],
until: ABS_TIME[2],
fee: VALUES[2],
},
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[2],
until: ABS_TIME[3],
},
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[3],
until: ABS_TIME[4],
fee: VALUES[1],
@@ -198,7 +221,7 @@ test("should add a gap when there no fee", (t) => {
});
test("should have three rows when first denom is between second and second is worse", (t) => {
- const timeline = createDenominationTimeline(
+ const timeline = createTimeline(
normalize([
{
value: VALUES[1],
@@ -213,24 +236,28 @@ test("should have three rows when first denom is between second and second is wo
feeDeposit: VALUES[2],
} as Partial<DenominationInfo> as DenominationInfo,
]),
+ "denomPubHash",
+ "stampStart",
"stampExpireDeposit",
"feeDeposit",
+ "group",
+ selectBestForOverlappingDenominations,
);
expect(t, timeline).deep.equal([
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1],
until: ABS_TIME[2],
fee: VALUES[2],
},
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[2],
until: ABS_TIME[3],
fee: VALUES[1],
},
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[3],
until: ABS_TIME[4],
fee: VALUES[2],
@@ -239,7 +266,7 @@ test("should have three rows when first denom is between second and second is wo
});
test("should have one row when first denom is between second and second is better", (t) => {
- const timeline = createDenominationTimeline(
+ const timeline = createTimeline(
normalize([
{
value: VALUES[1],
@@ -254,13 +281,17 @@ test("should have one row when first denom is between second and second is bette
feeDeposit: VALUES[1],
} as Partial<DenominationInfo> as DenominationInfo,
]),
+ "denomPubHash",
+ "stampStart",
"stampExpireDeposit",
"feeDeposit",
+ "group",
+ selectBestForOverlappingDenominations,
);
expect(t, timeline).deep.equal([
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1],
until: ABS_TIME[4],
fee: VALUES[1],
@@ -269,7 +300,7 @@ test("should have one row when first denom is between second and second is bette
});
test("should only add the best1", (t) => {
- const timeline = createDenominationTimeline(
+ const timeline = createTimeline(
normalize([
{
value: VALUES[1],
@@ -290,19 +321,23 @@ test("should only add the best1", (t) => {
feeDeposit: VALUES[2],
} as Partial<DenominationInfo> as DenominationInfo,
]),
+ "denomPubHash",
+ "stampStart",
"stampExpireDeposit",
"feeDeposit",
+ "group",
+ selectBestForOverlappingDenominations,
);
expect(t, timeline).deep.equal([
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1],
until: ABS_TIME[2],
fee: VALUES[2],
},
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[2],
until: ABS_TIME[4],
fee: VALUES[1],
@@ -311,7 +346,7 @@ test("should only add the best1", (t) => {
});
test("should only add the best2", (t) => {
- const timeline = createDenominationTimeline(
+ const timeline = createTimeline(
normalize([
{
value: VALUES[1],
@@ -338,25 +373,29 @@ test("should only add the best2", (t) => {
feeDeposit: VALUES[3],
} as Partial<DenominationInfo> as DenominationInfo,
]),
+ "denomPubHash",
+ "stampStart",
"stampExpireDeposit",
"feeDeposit",
+ "group",
+ selectBestForOverlappingDenominations,
);
expect(t, timeline).deep.equal([
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1],
until: ABS_TIME[2],
fee: VALUES[2],
},
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[2],
until: ABS_TIME[5],
fee: VALUES[1],
},
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[5],
until: ABS_TIME[6],
fee: VALUES[3],
@@ -365,7 +404,7 @@ test("should only add the best2", (t) => {
});
test("should only add the best3", (t) => {
- const timeline = createDenominationTimeline(
+ const timeline = createTimeline(
normalize([
{
value: VALUES[1],
@@ -386,13 +425,17 @@ test("should only add the best3", (t) => {
feeDeposit: VALUES[2],
} as Partial<DenominationInfo> as DenominationInfo,
]),
+ "denomPubHash",
+ "stampStart",
"stampExpireDeposit",
"feeDeposit",
+ "group",
+ selectBestForOverlappingDenominations,
);
expect(t, timeline).deep.equal([
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[2],
until: ABS_TIME[5],
fee: VALUES[1],
@@ -406,7 +449,7 @@ test("should only add the best3", (t) => {
//TODO: test the same start but different value
test("should not merge when there is different value", (t) => {
- const timeline = createDenominationTimeline(
+ const timeline = createTimeline(
normalize([
{
value: VALUES[1],
@@ -421,19 +464,23 @@ test("should not merge when there is different value", (t) => {
feeDeposit: VALUES[2],
} as Partial<DenominationInfo> as DenominationInfo,
]),
+ "denomPubHash",
+ "stampStart",
"stampExpireDeposit",
"feeDeposit",
+ "group",
+ selectBestForOverlappingDenominations,
);
expect(t, timeline).deep.equal([
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1],
until: ABS_TIME[3],
fee: VALUES[1],
},
{
- value: VALUES[2],
+ group: Amounts.stringifyValue(VALUES[2]),
from: ABS_TIME[2],
until: ABS_TIME[4],
fee: VALUES[2],
@@ -442,7 +489,7 @@ test("should not merge when there is different value", (t) => {
});
test("should not merge when there is different value (with duplicates)", (t) => {
- const timeline = createDenominationTimeline(
+ const timeline = createTimeline(
normalize([
{
value: VALUES[1],
@@ -469,19 +516,23 @@ test("should not merge when there is different value (with duplicates)", (t) =>
feeDeposit: VALUES[2],
} as Partial<DenominationInfo> as DenominationInfo,
]),
+ "denomPubHash",
+ "stampStart",
"stampExpireDeposit",
"feeDeposit",
+ "group",
+ selectBestForOverlappingDenominations,
);
expect(t, timeline).deep.equal([
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1],
until: ABS_TIME[3],
fee: VALUES[1],
},
{
- value: VALUES[2],
+ group: Amounts.stringifyValue(VALUES[2]),
from: ABS_TIME[2],
until: ABS_TIME[4],
fee: VALUES[2],
@@ -519,7 +570,7 @@ test("should return empty", (t) => {
const left = [] as FeeDescription[];
const right = [] as FeeDescription[];
- const pairs = createDenominationPairTimeline(left, right);
+ const pairs = createPairTimeline(left, right);
expect(t, pairs).deep.equals([]);
});
@@ -527,7 +578,7 @@ test("should return empty", (t) => {
test("should return first element", (t) => {
const left = [
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1],
until: ABS_TIME[3],
fee: VALUES[1],
@@ -537,24 +588,24 @@ test("should return first element", (t) => {
const right = [] as FeeDescription[];
{
- const pairs = createDenominationPairTimeline(left, right);
+ const pairs = createPairTimeline(left, right);
expect(t, pairs).deep.equals([
{
from: ABS_TIME[1],
until: ABS_TIME[3],
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
left: VALUES[1],
right: undefined,
},
] as FeeDescriptionPair[]);
}
{
- const pairs = createDenominationPairTimeline(right, left);
+ const pairs = createPairTimeline(right, left);
expect(t, pairs).deep.equals([
{
from: ABS_TIME[1],
until: ABS_TIME[3],
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
right: VALUES[1],
left: undefined,
},
@@ -565,7 +616,7 @@ test("should return first element", (t) => {
test("should add both to the same row", (t) => {
const left = [
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1],
until: ABS_TIME[3],
fee: VALUES[1],
@@ -574,7 +625,7 @@ test("should add both to the same row", (t) => {
const right = [
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1],
until: ABS_TIME[3],
fee: VALUES[2],
@@ -582,24 +633,24 @@ test("should add both to the same row", (t) => {
] as FeeDescription[];
{
- const pairs = createDenominationPairTimeline(left, right);
+ const pairs = createPairTimeline(left, right);
expect(t, pairs).deep.equals([
{
from: ABS_TIME[1],
until: ABS_TIME[3],
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
left: VALUES[1],
right: VALUES[2],
},
] as FeeDescriptionPair[]);
}
{
- const pairs = createDenominationPairTimeline(right, left);
+ const pairs = createPairTimeline(right, left);
expect(t, pairs).deep.equals([
{
from: ABS_TIME[1],
until: ABS_TIME[3],
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
left: VALUES[2],
right: VALUES[1],
},
@@ -610,7 +661,7 @@ test("should add both to the same row", (t) => {
test("should repeat the first and change the second", (t) => {
const left = [
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1],
until: ABS_TIME[5],
fee: VALUES[1],
@@ -619,18 +670,18 @@ test("should repeat the first and change the second", (t) => {
const right = [
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1],
until: ABS_TIME[2],
fee: VALUES[2],
},
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[2],
until: ABS_TIME[3],
},
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[3],
until: ABS_TIME[4],
fee: VALUES[3],
@@ -638,33 +689,33 @@ test("should repeat the first and change the second", (t) => {
] as FeeDescription[];
{
- const pairs = createDenominationPairTimeline(left, right);
+ const pairs = createPairTimeline(left, right);
expect(t, pairs).deep.equals([
{
from: ABS_TIME[1],
until: ABS_TIME[2],
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
left: VALUES[1],
right: VALUES[2],
},
{
from: ABS_TIME[2],
until: ABS_TIME[3],
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
left: VALUES[1],
right: undefined,
},
{
from: ABS_TIME[3],
until: ABS_TIME[4],
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
left: VALUES[1],
right: VALUES[3],
},
{
from: ABS_TIME[4],
until: ABS_TIME[5],
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
left: VALUES[1],
right: undefined,
},
@@ -679,7 +730,7 @@ test("should repeat the first and change the second", (t) => {
test("should separate denominations of different value", (t) => {
const left = [
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1],
until: ABS_TIME[3],
fee: VALUES[1],
@@ -688,7 +739,7 @@ test("should separate denominations of different value", (t) => {
const right = [
{
- value: VALUES[2],
+ group: Amounts.stringifyValue(VALUES[2]),
from: ABS_TIME[1],
until: ABS_TIME[3],
fee: VALUES[2],
@@ -696,38 +747,38 @@ test("should separate denominations of different value", (t) => {
] as FeeDescription[];
{
- const pairs = createDenominationPairTimeline(left, right);
+ const pairs = createPairTimeline(left, right);
expect(t, pairs).deep.equals([
{
from: ABS_TIME[1],
until: ABS_TIME[3],
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
left: VALUES[1],
right: undefined,
},
{
from: ABS_TIME[1],
until: ABS_TIME[3],
- value: VALUES[2],
+ group: Amounts.stringifyValue(VALUES[2]),
left: undefined,
right: VALUES[2],
},
] as FeeDescriptionPair[]);
}
{
- const pairs = createDenominationPairTimeline(right, left);
+ const pairs = createPairTimeline(right, left);
expect(t, pairs).deep.equals([
{
from: ABS_TIME[1],
until: ABS_TIME[3],
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
left: undefined,
right: VALUES[1],
},
{
from: ABS_TIME[1],
until: ABS_TIME[3],
- value: VALUES[2],
+ group: Amounts.stringifyValue(VALUES[2]),
left: VALUES[2],
right: undefined,
},
@@ -738,13 +789,13 @@ test("should separate denominations of different value", (t) => {
test("should separate denominations of different value2", (t) => {
const left = [
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[1],
until: ABS_TIME[2],
fee: VALUES[1],
},
{
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
from: ABS_TIME[2],
until: ABS_TIME[4],
fee: VALUES[2],
@@ -753,7 +804,7 @@ test("should separate denominations of different value2", (t) => {
const right = [
{
- value: VALUES[2],
+ group: Amounts.stringifyValue(VALUES[2]),
from: ABS_TIME[1],
until: ABS_TIME[3],
fee: VALUES[2],
@@ -761,26 +812,26 @@ test("should separate denominations of different value2", (t) => {
] as FeeDescription[];
{
- const pairs = createDenominationPairTimeline(left, right);
+ const pairs = createPairTimeline(left, right);
expect(t, pairs).deep.equals([
{
from: ABS_TIME[1],
until: ABS_TIME[2],
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
left: VALUES[1],
right: undefined,
},
{
from: ABS_TIME[2],
until: ABS_TIME[4],
- value: VALUES[1],
+ group: Amounts.stringifyValue(VALUES[1]),
left: VALUES[2],
right: undefined,
},
{
from: ABS_TIME[1],
until: ABS_TIME[3],
- value: VALUES[2],
+ group: Amounts.stringifyValue(VALUES[2]),
left: undefined,
right: VALUES[2],
},
diff --git a/packages/taler-wallet-core/src/util/denominations.ts b/packages/taler-wallet-core/src/util/denominations.ts
index 4efb902c8..9cd931acd 100644
--- a/packages/taler-wallet-core/src/util/denominations.ts
+++ b/packages/taler-wallet-core/src/util/denominations.ts
@@ -23,6 +23,7 @@ import {
FeeDescriptionPair,
TalerProtocolTimestamp,
TimePoint,
+ WireFee,
} from "@gnu-taler/taler-util";
/**
@@ -33,9 +34,9 @@ import {
* @param list denominations of same value
* @returns
*/
-function selectBestForOverlappingDenominations(
- list: DenominationInfo[],
-): DenominationInfo | undefined {
+export function selectBestForOverlappingDenominations<
+ T extends DenominationInfo,
+>(list: T[]): T | undefined {
let minDeposit: DenominationInfo | undefined = undefined;
//TODO: improve denomination selection, this is a trivial implementation
list.forEach((e) => {
@@ -50,6 +51,23 @@ function selectBestForOverlappingDenominations(
return minDeposit;
}
+export function selectMinimumFee<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<
{
[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.
- * For any element in the position "p" on the left or right "list", then
- * list[p].until should be equal to list[p+1].from
+ * Takes two timelines and create one to compare them.
+ *
+ * For both lists the next condition should be true:
+ * for any element in the position "idx" then
+ * list[idx].until === list[idx+1].from
*
- * @see {createDenominationTimeline}
+ * @see {createTimeline}
*
* @param left list denominations @type {FeeDescription}
* @param right list denominations @type {FeeDescription}
* @returns list of pairs for the same time
*/
-export function createDenominationPairTimeline(
+export function createPairTimeline(
left: FeeDescription[],
right: FeeDescription[],
): FeeDescriptionPair[] {
@@ -81,23 +101,15 @@ export function createDenominationPairTimeline(
let ri = 0;
while (li < left.length && ri < right.length) {
- const currentValue =
- Amounts.cmp(left[li].value, right[ri].value) < 0
- ? left[li].value
- : right[ri].value;
+ const currentGroup =
+ left[li].group < right[ri].group ? left[li].group : right[ri].group;
let ll = 0; //left length (until next value)
- while (
- li + ll < left.length &&
- Amounts.cmp(left[li + ll].value, currentValue) === 0
- ) {
+ while (li + ll < left.length && left[li + ll].group === currentGroup) {
ll++;
}
let rl = 0; //right length (until next value)
- while (
- ri + rl < right.length &&
- Amounts.cmp(right[ri + rl].value, currentValue) === 0
- ) {
+ while (ri + rl < right.length && right[ri + rl].group === currentGroup) {
rl++;
}
const leftIsEmpty = ll === 0;
@@ -120,7 +132,7 @@ export function createDenominationPairTimeline(
right.splice(ri, 0, {
from: leftStarts,
until: ends,
- value: left[li].value,
+ group: left[li].group,
});
rl++;
@@ -132,7 +144,7 @@ export function createDenominationPairTimeline(
left.splice(li, 0, {
from: rightStarts,
until: ends,
- value: right[ri].value,
+ group: right[ri].group,
});
ll++;
@@ -148,7 +160,7 @@ export function createDenominationPairTimeline(
right.splice(ri + rl, 0, {
from: rightEnds,
until: leftEnds,
- value: left[0].value,
+ group: left[0].group,
});
rl++;
}
@@ -156,7 +168,7 @@ export function createDenominationPairTimeline(
left.splice(li + ll, 0, {
from: leftEnds,
until: rightEnds,
- value: right[0].value,
+ group: right[0].group,
});
ll++;
}
@@ -165,7 +177,7 @@ export function createDenominationPairTimeline(
while (
li < left.length &&
ri < right.length &&
- Amounts.cmp(left[li].value, right[ri].value) === 0
+ left[li].group === right[ri].group
) {
if (
AbsoluteTime.cmp(left[li].from, timeCut) !== 0 &&
@@ -186,7 +198,7 @@ export function createDenominationPairTimeline(
right: right[ri].fee,
from: timeCut,
until: AbsoluteTime.never(),
- value: currentValue,
+ group: currentGroup,
});
if (left[li].until.t_ms === right[ri].until.t_ms) {
@@ -204,7 +216,7 @@ export function createDenominationPairTimeline(
if (
li < left.length &&
- Amounts.cmp(left[li].value, pairList[pairList.length - 1].value) !== 0
+ left[li].group !== pairList[pairList.length - 1].group
) {
//value changed, should break
//this if will catch when both (left and right) change at the same time
@@ -217,7 +229,7 @@ export function createDenominationPairTimeline(
if (li < left.length) {
let timeCut =
pairList.length > 0 &&
- Amounts.cmp(pairList[pairList.length - 1].value, left[li].value) === 0
+ pairList[pairList.length - 1].group === left[li].group
? pairList[pairList.length - 1].until
: left[li].from;
while (li < left.length) {
@@ -226,7 +238,7 @@ export function createDenominationPairTimeline(
right: undefined,
from: timeCut,
until: left[li].until,
- value: left[li].value,
+ group: left[li].group,
});
timeCut = left[li].until;
li++;
@@ -235,7 +247,7 @@ export function createDenominationPairTimeline(
if (ri < right.length) {
let timeCut =
pairList.length > 0 &&
- Amounts.cmp(pairList[pairList.length - 1].value, right[ri].value) === 0
+ pairList[pairList.length - 1].group === right[ri].group
? pairList[pairList.length - 1].until
: right[ri].from;
while (ri < right.length) {
@@ -244,7 +256,7 @@ export function createDenominationPairTimeline(
left: undefined,
from: timeCut,
until: right[ri].until,
- value: right[ri].value,
+ group: right[ri].group,
});
timeCut = right[ri].until;
ri++;
@@ -254,42 +266,70 @@ export function createDenominationPairTimeline(
}
/**
- * Create a usage timeline with the denominations given.
+ * Create a usage timeline with the entity given.
*
- * If there are multiple denominations that can be used, the list will
- * contain the one that minimize the fee cost. @see selectBestForOverlappingDenominations
+ * If there are multiple entities that can be used in the same period,
+ * the list will contain the one that minimize the fee cost.
+ * @see selectBestForOverlappingDenominations
*
- * @param list list of denominations
- * @param periodProp property of element of the list that will be used as end of the usage period
+ * @param list list of entities
+ * @param idProp property used for identification
+ * @param periodStartProp property of element of the list that will be used as start of the usage period
+ * @param periodEndProp property of element of the list that will be used as end of the usage period
* @param feeProp property of the element of the list that will be used as fee reference
+ * @param groupProp property of the element of the list that will be used for grouping
* @returns list of @type {FeeDescription} sorted by usage period
*/
-export function createDenominationTimeline(
- list: DenominationInfo[],
- periodProp: PropsWithReturnType<DenominationInfo, TalerProtocolTimestamp>,
- feeProp: PropsWithReturnType<DenominationInfo, AmountJson>,
+export function createTimeline<Type extends object>(
+ list: Type[],
+ idProp: PropsWithReturnType<Type, string>,
+ periodStartProp: PropsWithReturnType<Type, TalerProtocolTimestamp>,
+ periodEndProp: PropsWithReturnType<Type, TalerProtocolTimestamp>,
+ feeProp: PropsWithReturnType<Type, AmountJson>,
+ groupProp: PropsWithReturnType<Type, string> | undefined,
+ selectBestForOverlapping: (l: Type[]) => Type | undefined,
): FeeDescription[] {
- const points = list
+ /**
+ * First we create a list with with point in the timeline sorted
+ * by time and categorized by starting or ending.
+ */
+ const sortedPointsInTime = list
.reduce((ps, denom) => {
//exclude denoms with bad configuration
- if (denom.stampStart.t_s >= denom[periodProp].t_s) {
- throw Error(`denom ${denom.denomPubHash} has start after the end`);
- // return ps;
+ const id = denom[idProp] as string;
+ const stampStart = denom[periodStartProp] as TalerProtocolTimestamp;
+ const stampEnd = denom[periodEndProp] as TalerProtocolTimestamp;
+ const fee = denom[feeProp] as AmountJson;
+ const group = !groupProp ? "" : (denom[groupProp] as string);
+
+ if (!id) {
+ throw Error(
+ `denomination without hash ${JSON.stringify(denom, undefined, 2)}`,
+ );
+ }
+ if (stampStart.t_s >= stampEnd.t_s) {
+ throw Error(`denom ${id} has start after the end`);
}
ps.push({
type: "start",
- moment: AbsoluteTime.fromTimestamp(denom.stampStart),
+ fee,
+ group,
+ id,
+ moment: AbsoluteTime.fromTimestamp(stampStart),
denom,
});
ps.push({
type: "end",
- moment: AbsoluteTime.fromTimestamp(denom[periodProp]),
+ fee,
+ group,
+ id,
+ moment: AbsoluteTime.fromTimestamp(stampEnd),
denom,
});
return ps;
- }, [] as TimePoint[])
+ }, [] as TimePoint<Type>[])
.sort((a, b) => {
- const v = Amounts.cmp(a.denom.value, b.denom.value);
+ const v = a.group == b.group ? 0 : a.group > b.group ? 1 : -1;
if (v != 0) return v;
const t = AbsoluteTime.cmp(a.moment, b.moment);
if (t != 0) return t;
@@ -297,21 +337,15 @@ export function createDenominationTimeline(
return a.type === "start" ? 1 : -1;
});
- const activeAtTheSameTime: DenominationInfo[] = [];
- return points.reduce((result, cursor, idx) => {
- const hash = cursor.denom.denomPubHash;
- if (!hash)
- throw Error(
- `denomination without hash ${JSON.stringify(
- cursor.denom,
- undefined,
- 2,
- )}`,
- );
-
+ const activeAtTheSameTime: Type[] = [];
+ return sortedPointsInTime.reduce((result, cursor, idx) => {
+ /**
+ * Now that we have move one step forward, we should
+ * update the previous element ending period with the
+ * current start time.
+ */
let prev = result.length > 0 ? result[result.length - 1] : undefined;
- const prevHasSameValue =
- prev && Amounts.cmp(prev.value, cursor.denom.value) === 0;
+ const prevHasSameValue = prev && prev.group == cursor.group;
if (prev) {
if (prevHasSameValue) {
prev.until = cursor.moment;
@@ -326,11 +360,15 @@ export function createDenominationTimeline(
}
}
- //update the activeAtTheSameTime list
+ /**
+ * With the current moment in the iteration we
+ * should keep updated which entities are current
+ * active in this period of time.
+ */
if (cursor.type === "end") {
- const loc = activeAtTheSameTime.findIndex((v) => v.denomPubHash === hash);
+ const loc = activeAtTheSameTime.findIndex((v) => v[idProp] === cursor.id);
if (loc === -1) {
- throw Error(`denomination ${hash} has an end but no start`);
+ throw Error(`denomination ${cursor.id} has an end but no start`);
}
activeAtTheSameTime.splice(loc, 1);
} else if (cursor.type === "start") {
@@ -340,12 +378,16 @@ export function createDenominationTimeline(
throw new Error(`not TimePoint defined for type: ${exhaustiveCheck}`);
}
- if (idx == points.length - 1) {
- //this is the last element in the list, prevent adding
- //a gap in the end
+ if (idx == sortedPointsInTime.length - 1) {
+ /**
+ * This is the last element in the list, if we continue
+ * a gap will normally be added which is not necessary.
+ * Also, the last element should be ending and the list of active
+ * element should be empty
+ */
if (cursor.type !== "end") {
throw Error(
- `denomination ${hash} starts after ending or doesn't have an ending`,
+ `denomination ${cursor.id} starts after ending or doesn't have an ending`,
);
}
if (activeAtTheSameTime.length > 0) {
@@ -356,26 +398,36 @@ export function createDenominationTimeline(
return result;
}
- const current = selectBestForOverlappingDenominations(activeAtTheSameTime);
+ const current = selectBestForOverlapping(activeAtTheSameTime);
if (current) {
+ /**
+ * We have a candidate to add in the list, check that we are
+ * not adding a duplicate.
+ * Next element in the list will defined the ending.
+ */
+ const currentFee = current[feeProp] as AmountJson;
if (
prev === undefined || //is the first
!prev.fee || //is a gap
- Amounts.cmp(prev.fee, current[feeProp]) !== 0 // prev has the same fee
+ Amounts.cmp(prev.fee, currentFee) !== 0 // prev has different fee
) {
result.push({
- value: cursor.denom.value,
+ group: cursor.group,
from: cursor.moment,
until: AbsoluteTime.never(), //not yet known
- fee: current[feeProp],
+ fee: currentFee,
});
} else {
prev.until = cursor.moment;
}
} else {
+ /**
+ * No active element in this period of time, so we add a gap (no fee)
+ * Next element in the list will defined the ending.
+ */
result.push({
- value: cursor.denom.value,
+ group: cursor.group,
from: cursor.moment,
until: AbsoluteTime.never(), //not yet known
});
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 07dd1fcda..357dd586a 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -72,6 +72,7 @@ import {
CoinDumpJson,
CoreApiResponse,
DenominationInfo,
+ DenomOperationMap,
Duration,
durationFromSpec,
durationMin,
@@ -86,11 +87,9 @@ import {
Logger,
ManualWithdrawalDetails,
NotificationType,
- OperationMap,
parsePaytoUri,
RefreshReason,
TalerErrorCode,
- TalerErrorDetail,
URL,
WalletCoreVersion,
WalletNotification,
@@ -103,7 +102,6 @@ import {
import { clearDatabase } from "./db-utils.js";
import {
AuditorTrustRecord,
- CoinRecord,
CoinSourceType,
CoinStatus,
DenominationRecord,
@@ -111,11 +109,7 @@ import {
importDb,
WalletStoresV1,
} from "./db.js";
-import {
- getErrorDetailFromException,
- makeErrorDetail,
- TalerError,
-} from "./errors.js";
+import { getErrorDetailFromException, TalerError } from "./errors.js";
import {
ActiveLongpollInfo,
ExchangeOperations,
@@ -142,11 +136,7 @@ import {
} from "./operations/backup/index.js";
import { setWalletDeviceId } from "./operations/backup/state.js";
import { getBalances } from "./operations/balance.js";
-import {
- runOperationWithErrorReporting,
- storeOperationError,
- storeOperationPending,
-} from "./operations/common.js";
+import { runOperationWithErrorReporting } from "./operations/common.js";
import {
createDepositGroup,
getFeeForDeposit,
@@ -216,23 +206,23 @@ import {
} from "./operations/withdraw.js";
import { PendingTaskInfo, PendingTaskType } from "./pending-types.js";
import { assertUnreachable } from "./util/assertUnreachable.js";
-import { createDenominationTimeline } from "./util/denominations.js";
+import {
+ createTimeline,
+ selectBestForOverlappingDenominations,
+ selectMinimumFee,
+} from "./util/denominations.js";
import {
HttpRequestLibrary,
readSuccessResponseJsonOrThrow,
} from "./util/http.js";
-import { checkDbInvariant, checkLogicInvariant } from "./util/invariants.js";
+import { checkDbInvariant } from "./util/invariants.js";
import {
AsyncCondition,
OpenedPromise,
openPromise,
} from "./util/promiseUtils.js";
import { DbAccess, GetReadWriteAccess } from "./util/query.js";
-import {
- OperationAttemptResult,
- OperationAttemptResultType,
- RetryInfo,
-} from "./util/retries.js";
+import { OperationAttemptResult } from "./util/retries.js";
import { TimerAPI, TimerGroup } from "./util/timer.js";
import {
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
@@ -702,6 +692,7 @@ async function getExchangeDetailedInfo(
paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
auditors: exchangeDetails.auditors,
wireInfo: exchangeDetails.wireInfo,
+ globalFees: exchangeDetails.globalFees,
},
denominations,
};
@@ -711,32 +702,111 @@ async function getExchangeDetailedInfo(
throw Error(`exchange with base url "${exchangeBaseurl}" not found`);
}
- const feesDescription: OperationMap<FeeDescription[]> = {
- deposit: createDenominationTimeline(
- exchange.denominations,
+ const denoms = exchange.denominations.map((d) => ({
+ ...d,
+ group: Amounts.stringifyValue(d.value),
+ }));
+ const denomFees: DenomOperationMap<FeeDescription[]> = {
+ deposit: createTimeline(
+ denoms,
+ "denomPubHash",
+ "stampStart",
"stampExpireDeposit",
"feeDeposit",
+ "group",
+ selectBestForOverlappingDenominations,
),
- refresh: createDenominationTimeline(
- exchange.denominations,
+ refresh: createTimeline(
+ denoms,
+ "denomPubHash",
+ "stampStart",
"stampExpireWithdraw",
"feeRefresh",
+ "group",
+ selectBestForOverlappingDenominations,
),
- refund: createDenominationTimeline(
- exchange.denominations,
+ refund: createTimeline(
+ denoms,
+ "denomPubHash",
+ "stampStart",
"stampExpireWithdraw",
"feeRefund",
+ "group",
+ selectBestForOverlappingDenominations,
),
- withdraw: createDenominationTimeline(
- exchange.denominations,
+ withdraw: createTimeline(
+ denoms,
+ "denomPubHash",
+ "stampStart",
"stampExpireWithdraw",
"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 {
...exchange.info,
- feesDescription,
+ denomFees,
+ transferFees,
+ globalFees,
};
}
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
index 5c62671fe..7ccf7f606 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
@@ -46,12 +46,14 @@ const exchanges: ExchangeFullDetails[] = [
denomination_keys: [],
},
],
- feesDescription: {
+ denomFees: {
deposit: [],
refresh: [],
refund: [],
withdraw: [],
},
+ globalFees: [],
+ transferFees: {},
wireInfo: {
accounts: [],
feesForType: {},
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts
index 4b28904fb..9603b3d2c 100644
--- a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts
@@ -15,15 +15,15 @@
*/
import {
- FeeDescription,
- FeeDescriptionPair,
- AbsoluteTime,
+ DenomOperationMap,
ExchangeFullDetails,
- OperationMap,
- ExchangeListItem,
+ ExchangeListItem, FeeDescriptionPair
} from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js";
+import {
+ State as SelectExchangeState
+} from "../../hooks/useSelectedExchange.js";
import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
import * as wxApi from "../../wxApi.js";
@@ -32,7 +32,7 @@ import {
ComparingView,
ErrorLoadingView,
NoExchangesView,
- ReadyView,
+ ReadyView
} from "./views.js";
export interface Props {
@@ -41,9 +41,6 @@ export interface Props {
onCancel: () => Promise<void>;
onSelection: (exchange: string) => Promise<void>;
}
-import {
- State as SelectExchangeState
-} from "../../hooks/useSelectedExchange.js";
export type State =
| State.Loading
@@ -71,13 +68,12 @@ export namespace State {
export interface Ready extends BaseInfo {
status: "ready";
- timeline: OperationMap<FeeDescription[]>;
onClose: ButtonHandler;
}
export interface Comparing extends BaseInfo {
status: "comparing";
- pairTimeline: OperationMap<FeeDescriptionPair[]>;
+ pairTimeline: DenomOperationMap<FeeDescriptionPair[]>;
onReset: ButtonHandler;
onSelect: ButtonHandler;
}
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts
index 0279f6514..954e52239 100644
--- a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts
@@ -14,8 +14,8 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { FeeDescription, OperationMap } from "@gnu-taler/taler-util";
-import { createDenominationPairTimeline } from "@gnu-taler/taler-wallet-core";
+import { DenomOperationMap, FeeDescription } from "@gnu-taler/taler-util";
+import { createPairTimeline } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import * as wxApi from "../../wxApi.js";
@@ -94,27 +94,26 @@ export function useComponentState(
onClick: onCancel,
},
selected,
- timeline: selected.feesDescription,
};
}
- const pairTimeline: OperationMap<FeeDescription[]> = {
- deposit: createDenominationPairTimeline(
- selected.feesDescription.deposit,
- original.feesDescription.deposit,
+ const pairTimeline: DenomOperationMap<FeeDescription[]> = {
+ deposit: createPairTimeline(
+ selected.denomFees.deposit,
+ original.denomFees.deposit,
),
- refresh: createDenominationPairTimeline(
- selected.feesDescription.refresh,
- original.feesDescription.refresh,
+ refresh: createPairTimeline(
+ selected.denomFees.refresh,
+ original.denomFees.refresh,
),
- refund: createDenominationPairTimeline(
- selected.feesDescription.refund,
- original.feesDescription.refund,
- ),
- withdraw: createDenominationPairTimeline(
- selected.feesDescription.withdraw,
- original.feesDescription.withdraw,
+ refund: createPairTimeline(
+ selected.denomFees.refund,
+ original.denomFees.refund,
),
+ withdraw: createPairTimeline(
+ selected.denomFees.withdraw,
+ original.denomFees.withdraw,
+ )
};
return {
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/stories.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/stories.tsx
index 43a147e28..38b63e615 100644
--- a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/stories.tsx
@@ -28,71 +28,72 @@ export default {
export const Bitcoin1 = createExample(ReadyView, {
exchanges: {
- list: { "http://exchange": "http://exchange" },
- value: "http://exchange",
+ list: { "0": "https://exchange.taler.ar" },
+ value: "0",
},
selected: {
currency: "BITCOINBTC",
auditors: [],
+ exchangeBaseUrl: "https://exchange.taler.ar",
+ denomFees: timelineExample(),
+ transferFees: {},
+ globalFees: [],
} as any,
onClose: {},
- timeline: {
- deposit: [],
- refresh: [],
- refund: [],
- withdraw: [],
- },
});
export const Bitcoin2 = createExample(ReadyView, {
exchanges: {
- list: { "http://exchange": "http://exchange" },
- value: "http://exchange",
+ list: {
+ "https://exchange.taler.ar": "https://exchange.taler.ar",
+ "https://exchange-btc.taler.ar": "https://exchange-btc.taler.ar",
+ },
+ value: "https://exchange.taler.ar",
},
selected: {
currency: "BITCOINBTC",
auditors: [],
+ exchangeBaseUrl: "https://exchange.taler.ar",
+ denomFees: timelineExample(),
+ transferFees: {},
+ globalFees: [],
} as any,
onClose: {},
- timeline: {
- deposit: [],
- refresh: [],
- refund: [],
- withdraw: [],
- },
});
+
export const Kudos1 = createExample(ReadyView, {
exchanges: {
- list: { "http://exchange": "http://exchange" },
- value: "http://exchange",
+ list: {
+ "https://exchange-kudos.taler.ar": "https://exchange-kudos.taler.ar",
+ },
+ value: "https://exchange-kudos.taler.ar",
},
selected: {
currency: "BITCOINBTC",
auditors: [],
+ exchangeBaseUrl: "https://exchange.taler.ar",
+ denomFees: timelineExample(),
+ transferFees: {},
+ globalFees: [],
} as any,
onClose: {},
- timeline: {
- deposit: [],
- refresh: [],
- refund: [],
- withdraw: [],
- },
});
export const Kudos2 = createExample(ReadyView, {
exchanges: {
- list: { "http://exchange": "http://exchange" },
- value: "http://exchange",
+ list: {
+ "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: {
currency: "BITCOINBTC",
auditors: [],
+ exchangeBaseUrl: "https://exchange.taler.ar",
+ denomFees: timelineExample(),
+ transferFees: {},
+ globalFees: [],
} as any,
onClose: {},
- timeline: {
- deposit: [],
- refresh: [],
- refund: [],
- withdraw: [],
- },
});
export const ComparingBitcoin = createExample(ComparingView, {
exchanges: {
@@ -102,6 +103,9 @@ export const ComparingBitcoin = createExample(ComparingView, {
selected: {
currency: "BITCOINBTC",
auditors: [],
+ exchangeBaseUrl: "https://exchange.taler.ar",
+ transferFees: {},
+ globalFees: [],
} as any,
onReset: {},
onSelect: {},
@@ -121,6 +125,9 @@ export const ComparingKudos = createExample(ComparingView, {
selected: {
currency: "KUDOS",
auditors: [],
+ exchangeBaseUrl: "https://exchange.taler.ar",
+ transferFees: {},
+ globalFees: [],
} as any,
onReset: {},
onSelect: {},
@@ -132,3 +139,400 @@ export const ComparingKudos = createExample(ComparingView, {
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,
+ },
+ },
+ ],
+ };
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx
index 47554bfcd..6b753e215 100644
--- a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx
@@ -31,9 +31,7 @@ import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js";
import arrowDown from "../../svg/chevron-down.svg";
import { State } from "./index.js";
-import {
- State as SelectExchangeState
-} from "../../hooks/useSelectedExchange.js";
+import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
const ButtonGroup = styled.div`
& > button {
@@ -59,7 +57,7 @@ const FeeDescriptionTable = styled.table`
}
td.value {
text-align: right;
- width: 1%;
+ width: 15%;
white-space: nowrap;
}
td.icon {
@@ -109,26 +107,28 @@ export function ErrorLoadingView({ error }: State.LoadingUriError): VNode {
return (
<LoadingError
- title={<i18n.Translate>Could not load tip status</i18n.Translate>}
+ title={<i18n.Translate>Could not load exchange fees</i18n.Translate>}
error={error}
/>
);
}
-
-
-export function NoExchangesView({currency}: SelectExchangeState.NoExchange): VNode {
+export function NoExchangesView({
+ currency,
+}: SelectExchangeState.NoExchange): VNode {
const { i18n } = useTranslationContext();
if (!currency) {
return (
<div>
<i18n.Translate>could not find any exchange</i18n.Translate>
</div>
- );
+ );
}
return (
<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>
);
}
@@ -356,7 +356,6 @@ export function ReadyView({
exchanges,
selected,
onClose,
- timeline,
}: State.Ready): VNode {
const { i18n } = useTranslationContext();
@@ -365,7 +364,10 @@ export function ReadyView({
<h2>
<i18n.Translate>Service fee description</i18n.Translate>
</h2>
-
+ <p>
+ All fee indicated below are in the same and only currency the exchange
+ works.
+ </p>
<section>
<div
style={{
@@ -375,21 +377,27 @@ export function ReadyView({
justifyContent: "space-between",
}}
>
- <p>
- <Input>
- <SelectList
- label={
- <i18n.Translate>
- Select {selected.currency} exchange
- </i18n.Translate>
- }
- list={exchanges.list}
- name="lang"
- value={exchanges.value}
- onChange={exchanges.onChange}
- />
- </Input>
- </p>
+ {Object.keys(exchanges.list).length === 1 ? (
+ <Fragment>
+ <p>Exchange: {selected.exchangeBaseUrl}</p>
+ </Fragment>
+ ) : (
+ <p>
+ <Input>
+ <SelectList
+ label={
+ <i18n.Translate>
+ Select {selected.currency} exchange
+ </i18n.Translate>
+ }
+ list={exchanges.list}
+ name="lang"
+ value={exchanges.value}
+ onChange={exchanges.onChange}
+ />
+ </Input>
+ </p>
+ )}
<Button variant="outlined" onClick={onClose.onClick}>
<i18n.Translate>Close</i18n.Translate>
</Button>
@@ -411,17 +419,26 @@ export function ReadyView({
<table>
<tr>
<td>
- <i18n.Translate>currency</i18n.Translate>
+ <i18n.Translate>Currency</i18n.Translate>
+ </td>
+ <td>
+ <b>{selected.currency}</b>
</td>
- <td>{selected.currency}</td>
</tr>
</table>
</section>
<section>
<h2>
- <i18n.Translate>Operations</i18n.Translate>
+ <i18n.Translate>Coin operations</i18n.Translate>
</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>
<i18n.Translate>Deposits</i18n.Translate>
</p>
<FeeDescriptionTable>
@@ -440,7 +457,10 @@ export function ReadyView({
</tr>
</thead>
<tbody>
- <RenderFeeDescriptionByValue first={timeline.deposit} />
+ <RenderFeeDescriptionByValue
+ list={selected.denomFees.deposit}
+ sorting={(a, b) => Number(a) - Number(b)}
+ />
</tbody>
</FeeDescriptionTable>
<p>
@@ -462,7 +482,10 @@ export function ReadyView({
</tr>
</thead>
<tbody>
- <RenderFeeDescriptionByValue first={timeline.withdraw} />
+ <RenderFeeDescriptionByValue
+ list={selected.denomFees.withdraw}
+ sorting={(a, b) => Number(a) - Number(b)}
+ />
</tbody>
</FeeDescriptionTable>
<p>
@@ -484,7 +507,10 @@ export function ReadyView({
</tr>
</thead>
<tbody>
- <RenderFeeDescriptionByValue first={timeline.refund} />
+ <RenderFeeDescriptionByValue
+ list={selected.denomFees.refund}
+ sorting={(a, b) => Number(a) - Number(b)}
+ />
</tbody>
</FeeDescriptionTable>{" "}
<p>
@@ -506,53 +532,81 @@ export function ReadyView({
</tr>
</thead>
<tbody>
- <RenderFeeDescriptionByValue first={timeline.refresh} />
+ <RenderFeeDescriptionByValue
+ list={selected.denomFees.refresh}
+ sorting={(a, b) => Number(a) - Number(b)}
+ />
</tbody>
- </FeeDescriptionTable>{" "}
+ </FeeDescriptionTable>
</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>
<tr>
- <td>
- <i18n.Translate>Wallet operations</i18n.Translate>
- </td>
- <td>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Feature</i18n.Translate>
+ </th>
+ <th class="fee">
<i18n.Translate>Fee</i18n.Translate>
- </td>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
</tr>
</thead>
<tbody>
- <tr>
- <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>
+ <RenderFeeDescriptionByValue list={selected.globalFees} />
</tbody>
- </table>
+ </FeeDescriptionTable>
</section>
<section>
<ButtonGroup>
@@ -579,7 +633,7 @@ function FeeDescriptionRowsGroup({
<tr
key={idx}
class="value"
- data-hasMore={!hasMoreInfo}
+ data-hasMore={hasMoreInfo}
data-main={main}
data-hidden={!main && !expanded}
onClick={() => setExpand((p) => !p)}
@@ -594,9 +648,7 @@ function FeeDescriptionRowsGroup({
/>
) : undefined}
</td>
- <td class="value">
- {main ? <Amount value={info.value} hideCurrency /> : ""}
- </td>
+ <td class="value">{main ? info.group : ""}</td>
{info.fee ? (
<td class="fee">{<Amount value={info.fee} hideCurrency />}</td>
) : undefined}
@@ -621,7 +673,7 @@ function FeePairRowsGroup({ infos }: { infos: FeeDescriptionPair[] }): VNode {
<tr
key={idx}
class="value"
- data-hasMore={!hasMoreInfo}
+ data-hasMore={hasMoreInfo}
data-main={main}
data-hidden={!main && !expanded}
onClick={() => setExpand((p) => !p)}
@@ -636,9 +688,7 @@ function FeePairRowsGroup({ infos }: { infos: FeeDescriptionPair[] }): VNode {
/>
) : undefined}
</td>
- <td class="value">
- {main ? <Amount value={info.value} hideCurrency /> : ""}
- </td>
+ <td class="value">{main ? info.group : ""}</td>
{info.left ? (
<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 nextIsMoreInfo =
- next !== undefined && Amounts.cmp(next.value, info.value) === 0;
+ next !== undefined && next.group === info.group;
prev.rows.push(info);
@@ -681,7 +731,7 @@ function RenderFeePairByValue({ list }: { list: FeeDescriptionPair[] }): VNode {
return prev;
}
- prev.rows = [];
+ // prev.rows = [];
prev.views.push(<FeePairRowsGroup infos={prev.rows} />);
return prev;
},
@@ -701,36 +751,21 @@ function RenderFeePairByValue({ list }: { list: FeeDescriptionPair[] }): VNode {
* @returns
*/
function RenderFeeDescriptionByValue({
- first,
+ list,
+ sorting,
}: {
- first: FeeDescription[];
+ list: FeeDescription[];
+ sorting?: (a: string, b: string) => number;
}): VNode {
- return (
- <Fragment>
- {
- first.reduce(
- (prev, info, idx) => {
- const next = idx >= first.length - 1 ? undefined : first[idx + 1];
-
- const nextIsMoreInfo =
- next !== undefined && Amounts.cmp(next.value, info.value) === 0;
-
- prev.rows.push(info);
-
- 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>
- );
+ const grouped = list.reduce((prev, cur) => {
+ if (!prev[cur.group]) {
+ prev[cur.group] = [];
+ }
+ prev[cur.group].push(cur);
+ return prev;
+ }, {} as Record<string, FeeDescription[]>);
+ const p = Object.keys(grouped)
+ .sort(sorting)
+ .map((i, idx) => <FeeDescriptionRowsGroup key={idx} infos={grouped[i]} />);
+ return <Fragment>{p}</Fragment>;
}