gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: create a fee description time


From: gnunet
Subject: [taler-wallet-core] branch master updated: create a fee description timeline for global fee and wire fees
Date: Wed, 12 Oct 2022 20:58:19 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new 610df1c9c create a fee description timeline for global fee and wire 
fees
610df1c9c is described below

commit 610df1c9cf8ec91815130ac2a426f8f5b7d1ed0c
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Wed Oct 12 15:58:10 2022 -0300

    create a fee description timeline for global fee and wire fees
---
 packages/taler-util/src/backupTypes.ts             |  26 +-
 packages/taler-util/src/walletTypes.ts             |  54 ++-
 .../src/crypto/workers/rpcClient.ts                |   1 -
 packages/taler-wallet-core/src/db.ts               |   3 +-
 .../src/operations/backup/export.ts                |  14 +-
 .../src/operations/backup/import.ts                |  15 +-
 .../taler-wallet-core/src/operations/exchanges.ts  |  19 +-
 .../src/util/denominations.test.ts                 | 197 +++++----
 .../taler-wallet-core/src/util/denominations.ts    | 200 +++++----
 packages/taler-wallet-core/src/wallet.ts           | 130 ++++--
 .../src/cta/Withdraw/test.ts                       |   4 +-
 .../src/wallet/ExchangeSelection/index.ts          |  18 +-
 .../src/wallet/ExchangeSelection/state.ts          |  33 +-
 .../src/wallet/ExchangeSelection/stories.tsx       | 468 +++++++++++++++++++--
 .../src/wallet/ExchangeSelection/views.tsx         | 255 ++++++-----
 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,16 +419,25 @@ 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>
@@ -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>;
 }

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]