gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: support multiple exchange det


From: gnunet
Subject: [taler-wallet-core] branch master updated: support multiple exchange details per base URL
Date: Wed, 02 Jun 2021 13:24:31 +0200

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

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

The following commit(s) were added to refs/heads/master by this push:
     new 02f1d4b0 support multiple exchange details per base URL
02f1d4b0 is described below

commit 02f1d4b08116c24f0af1f32cb6d82be292fa6d10
Author: Florian Dold <florian@dold.me>
AuthorDate: Wed Jun 2 13:23:51 2021 +0200

    support multiple exchange details per base URL
---
 packages/taler-util/src/backupTypes.ts             |  34 +-
 packages/taler-wallet-cli/src/index.ts             |   4 +-
 packages/taler-wallet-core/src/db.ts               | 135 ++---
 .../src/operations/backup/export.ts                |  53 +-
 .../src/operations/backup/import.ts                | 116 ++--
 .../src/operations/backup/index.ts                 |   6 +-
 .../taler-wallet-core/src/operations/currencies.ts |   8 +-
 .../taler-wallet-core/src/operations/deposits.ts   |  14 +-
 .../taler-wallet-core/src/operations/exchanges.ts  | 627 +++++++++++----------
 packages/taler-wallet-core/src/operations/pay.ts   |  21 +-
 .../taler-wallet-core/src/operations/pending.ts    |  36 +-
 .../taler-wallet-core/src/operations/recoup.ts     |  49 +-
 .../taler-wallet-core/src/operations/refresh.ts    |   2 +-
 .../taler-wallet-core/src/operations/reserves.ts   |  25 +-
 .../src/operations/transactions.ts                 |  11 +-
 .../taler-wallet-core/src/operations/withdraw.ts   |  91 ++-
 packages/taler-wallet-core/src/wallet.ts           |  63 ++-
 17 files changed, 699 insertions(+), 596 deletions(-)

diff --git a/packages/taler-util/src/backupTypes.ts 
b/packages/taler-util/src/backupTypes.ts
index ce2eb6b7..dc344ee2 100644
--- a/packages/taler-util/src/backupTypes.ts
+++ b/packages/taler-util/src/backupTypes.ts
@@ -128,6 +128,8 @@ export interface WalletBackupContentV1 {
    */
   exchanges: BackupExchange[];
 
+  exchange_details: BackupExchangeDetails[];
+
   /**
    * Grouped refresh sessions.
    *
@@ -1090,9 +1092,34 @@ export class BackupExchangeAuditor {
 }
 
 /**
- * Backup information about an exchange.
+ * Backup information for an exchange.  Serves effectively
+ * as a pointer to the exchange details identified by
+ * the base URL, master public key and currency.
  */
 export interface BackupExchange {
+  base_url: string;
+
+  master_public_key: string;
+
+  currency: string;
+
+  /**
+   * Time when the pointer to the exchange details
+   * was last updated.
+   *
+   * Used to facilitate automatic merging.
+   */
+  update_clock: Timestamp;
+}
+
+/**
+ * Backup information about an exchange's details.
+ *
+ * Note that one base URL can have multiple exchange
+ * details.  The BackupExchange stores a pointer
+ * to the current exchange details.
+ */
+export interface BackupExchangeDetails {
   /**
    * Canonicalized base url of the exchange.
    */
@@ -1158,11 +1185,6 @@ export interface BackupExchange {
    * ETag for last terms of service download.
    */
   tos_etag_accepted: string | undefined;
-
-  /**
-   * Should this exchange be considered defective?
-   */
-  defective?: boolean;
 }
 
 export enum BackupProposalStatus {
diff --git a/packages/taler-wallet-cli/src/index.ts 
b/packages/taler-wallet-cli/src/index.ts
index aad034b8..db6c0a9f 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -598,10 +598,10 @@ advancedCli
   })
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      const exchange = await wallet.updateExchangeFromUrl(
+      const { exchange, exchangeDetails } = await wallet.updateExchangeFromUrl(
         args.withdrawManually.exchange,
       );
-      const acct = exchange.wireInfo?.accounts[0];
+      const acct = exchangeDetails.wireInfo.accounts[0];
       if (!acct) {
         console.log("exchange has no accounts");
         return;
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index 0ff34d3c..c457d0ff 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -513,26 +513,43 @@ export interface DenominationRecord {
   exchangeBaseUrl: string;
 }
 
-/**
- * Details about the exchange that we only know after
- * querying /keys and /wire.
- */
-export interface ExchangeDetails {
+export enum ExchangeUpdateStatus {
+  FetchKeys = "fetch-keys",
+  FetchWire = "fetch-wire",
+  FetchTerms = "fetch-terms",
+  FinalizeUpdate = "finalize-update",
+  Finished = "finished",
+}
+
+export interface ExchangeBankAccount {
+  payto_uri: string;
+  master_sig: string;
+}
+
+export enum ExchangeUpdateReason {
+  Initial = "initial",
+  Forced = "forced",
+  Scheduled = "scheduled",
+}
+
+export interface ExchangeDetailsRecord {
   /**
    * Master public key of the exchange.
    */
   masterPublicKey: string;
 
-  /**
-   * Auditors (partially) auditing the exchange.
-   */
-  auditors: Auditor[];
+  exchangeBaseUrl: string;
 
   /**
    * Currency that the exchange offers.
    */
   currency: string;
 
+  /**
+   * Auditors (partially) auditing the exchange.
+   */
+  auditors: Auditor[];
+
   /**
    * Last observed protocol version.
    */
@@ -546,6 +563,23 @@ export interface ExchangeDetails {
    */
   signingKeys: ExchangeSignKeyJson[];
 
+  /**
+   * Terms of service text or undefined if not downloaded yet.
+   *
+   * This is just used as a cache of the last downloaded ToS.
+   */
+  termsOfServiceText: string | undefined;
+
+  /**
+   * ETag for last terms of service download.
+   */
+  termsOfServiceLastEtag: string | undefined;
+
+  /**
+   * ETag for last terms of service download.
+   */
+  termsOfServiceAcceptedEtag: string | undefined;
+
   /**
    * Timestamp for last update.
    */
@@ -555,30 +589,25 @@ export interface ExchangeDetails {
    * When should we next update the information about the exchange?
    */
   nextUpdateTime: Timestamp;
-}
 
-export enum ExchangeUpdateStatus {
-  FetchKeys = "fetch-keys",
-  FetchWire = "fetch-wire",
-  FetchTerms = "fetch-terms",
-  FinalizeUpdate = "finalize-update",
-  Finished = "finished",
+  wireInfo: WireInfo;
 }
 
-export interface ExchangeBankAccount {
-  payto_uri: string;
-  master_sig: string;
-}
-
-export interface ExchangeWireInfo {
+export interface WireInfo {
   feesForType: { [wireMethod: string]: WireFee[] };
+
   accounts: ExchangeBankAccount[];
 }
 
-export enum ExchangeUpdateReason {
-  Initial = "initial",
-  Forced = "forced",
-  Scheduled = "scheduled",
+export interface ExchangeDetailsPointer {
+  masterPublicKey: string;
+  currency: string;
+
+  /**
+   * Timestamp when the (masterPublicKey, currency) pointer
+   * has been updated.
+   */
+  updateClock: Timestamp;
 }
 
 /**
@@ -590,48 +619,13 @@ export interface ExchangeRecord {
    */
   baseUrl: string;
 
-  /**
-   * Did we finish adding the exchange?
-   */
-  addComplete: boolean;
+  detailsPointer: ExchangeDetailsPointer | undefined;
 
   /**
    * Is this a permanent or temporary exchange record?
    */
   permanent: boolean;
 
-  /**
-   * Was the exchange added as a built-in exchange?
-   */
-  builtIn: boolean;
-
-  /**
-   * Details, once known.
-   */
-  details: ExchangeDetails | undefined;
-
-  /**
-   * Mapping from wire method type to the wire fee.
-   */
-  wireInfo: ExchangeWireInfo | undefined;
-
-  /**
-   * Terms of service text or undefined if not downloaded yet.
-   *
-   * This is just used as a cache of the last downloaded ToS.
-   */
-  termsOfServiceText: string | undefined;
-
-  /**
-   * ETag for last terms of service download.
-   */
-  termsOfServiceLastEtag: string | undefined;
-
-  /**
-   * ETag for last terms of service download.
-   */
-  termsOfServiceAcceptedEtag: string | undefined;
-
   /**
    * Time when the update to the exchange has been started or
    * undefined if no update is in progress.
@@ -640,6 +634,9 @@ export interface ExchangeRecord {
 
   /**
    * Status of updating the info about the exchange.
+   * 
+   * FIXME:  Adapt this to recent changes regarding how
+   * updating exchange details works.
    */
   updateStatus: ExchangeUpdateStatus;
 
@@ -1548,7 +1545,7 @@ export interface BackupProviderTerms {
 export interface BackupProviderRecord {
   /**
    * Base URL of the provider.
-   * 
+   *
    * Primary key for the record.
    */
   baseUrl: string;
@@ -1692,6 +1689,17 @@ class ExchangesStore extends Store<"exchanges", 
ExchangeRecord> {
   }
 }
 
+class ExchangeDetailsStore extends Store<
+  "exchangeDetails",
+  ExchangeDetailsRecord
+> {
+  constructor() {
+    super("exchangeDetails", {
+      keyPath: ["exchangeBaseUrl", "currency", "masterPublicKey"],
+    });
+  }
+}
+
 class CoinsStore extends Store<"coins", CoinRecord> {
   constructor() {
     super("coins", { keyPath: "coinPub" });
@@ -1924,6 +1932,7 @@ export const Stores = {
   exchangeTrustStore: new ExchangeTrustStore(),
   denominations: new DenominationsStore(),
   exchanges: new ExchangesStore(),
+  exchangeDetails: new ExchangeDetailsStore(),
   proposals: new ProposalsStore(),
   refreshGroups: new Store<"refreshGroups", RefreshGroupRecord>(
     "refreshGroups",
diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts 
b/packages/taler-wallet-core/src/operations/backup/export.ts
index 70d249ab..fa0af1b0 100644
--- a/packages/taler-wallet-core/src/operations/backup/export.ts
+++ b/packages/taler-wallet-core/src/operations/backup/export.ts
@@ -47,6 +47,7 @@ import {
   BackupProposalStatus,
   BackupRefreshOldCoin,
   BackupRefreshSession,
+  BackupExchangeDetails,
 } from "@gnu-taler/taler-util";
 import { InternalWalletState } from "../state";
 import {
@@ -65,6 +66,7 @@ import {
 } from "../../db.js";
 import { encodeCrock, stringToBytes, getRandomBytes } from "../../index.js";
 import { canonicalizeBaseUrl, canonicalJson } from "@gnu-taler/taler-util";
+import { getExchangeDetails } from "../exchanges.js";
 
 export async function exportBackup(
   ws: InternalWalletState,
@@ -74,6 +76,7 @@ export async function exportBackup(
     [
       Stores.config,
       Stores.exchanges,
+      Stores.exchangeDetails,
       Stores.coins,
       Stores.denominations,
       Stores.purchases,
@@ -88,6 +91,7 @@ export async function exportBackup(
     async (tx) => {
       const bs = await getWalletBackupState(ws, tx);
 
+      const backupExchangeDetails: BackupExchangeDetails[] = [];
       const backupExchanges: BackupExchange[] = [];
       const backupCoinsByDenom: { [dph: string]: BackupCoin[] } = {};
       const backupDenominationsByExchange: {
@@ -254,21 +258,22 @@ export async function exportBackup(
         });
       });
 
-      await tx.iter(Stores.exchanges).forEach((ex) => {
-        // Only back up permanently added exchanges.
-
-        if (!ex.details) {
-          return;
-        }
-        if (!ex.wireInfo) {
-          return;
-        }
-        if (!ex.addComplete) {
-          return;
-        }
-        if (!ex.permanent) {
+      await tx.iter(Stores.exchanges).forEachAsync(async (ex) => {
+        const dp = ex.detailsPointer;
+        if (!dp) {
           return;
         }
+        backupExchanges.push({
+          base_url: ex.baseUrl,
+          currency: dp.currency,
+          master_public_key: dp.masterPublicKey,
+          update_clock: dp.updateClock,
+        });
+      });
+
+      await tx.iter(Stores.exchangeDetails).forEachAsync(async (ex) => {
+        // Only back up permanently added exchanges.
+
         const wi = ex.wireInfo;
         const wireFees: BackupExchangeWireFee[] = [];
 
@@ -285,23 +290,23 @@ export async function exportBackup(
           }
         });
 
-        backupExchanges.push({
-          base_url: ex.baseUrl,
-          reserve_closing_delay: ex.details.reserveClosingDelay,
+        backupExchangeDetails.push({
+          base_url: ex.exchangeBaseUrl,
+          reserve_closing_delay: ex.reserveClosingDelay,
           accounts: ex.wireInfo.accounts.map((x) => ({
             payto_uri: x.payto_uri,
             master_sig: x.master_sig,
           })),
-          auditors: ex.details.auditors.map((x) => ({
+          auditors: ex.auditors.map((x) => ({
             auditor_pub: x.auditor_pub,
             auditor_url: x.auditor_url,
             denomination_keys: x.denomination_keys,
           })),
-          master_public_key: ex.details.masterPublicKey,
-          currency: ex.details.currency,
-          protocol_version: ex.details.protocolVersion,
+          master_public_key: ex.masterPublicKey,
+          currency: ex.currency,
+          protocol_version: ex.protocolVersion,
           wire_fees: wireFees,
-          signing_keys: ex.details.signingKeys.map((x) => ({
+          signing_keys: ex.signingKeys.map((x) => ({
             key: x.key,
             master_sig: x.master_sig,
             stamp_end: x.stamp_end,
@@ -310,8 +315,9 @@ export async function exportBackup(
           })),
           tos_etag_accepted: ex.termsOfServiceAcceptedEtag,
           tos_etag_last: ex.termsOfServiceLastEtag,
-          denominations: backupDenominationsByExchange[ex.baseUrl] ?? [],
-          reserves: backupReservesByExchange[ex.baseUrl] ?? [],
+          denominations:
+            backupDenominationsByExchange[ex.exchangeBaseUrl] ?? [],
+          reserves: backupReservesByExchange[ex.exchangeBaseUrl] ?? [],
         });
       });
 
@@ -451,6 +457,7 @@ export async function exportBackup(
         schema_id: "gnu-taler-wallet-backup-content",
         schema_version: 1,
         exchanges: backupExchanges,
+        exchange_details: backupExchangeDetails,
         wallet_root_pub: bs.walletRootPub,
         backup_providers: backupBackupProviders,
         current_device_id: bs.deviceId,
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts 
b/packages/taler-wallet-core/src/operations/backup/import.ts
index 1bbba6e2..f0a944a2 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -32,7 +32,6 @@ import {
   Stores,
   WalletContractData,
   DenomSelectionState,
-  ExchangeWireInfo,
   ExchangeUpdateStatus,
   DenominationStatus,
   CoinSource,
@@ -46,6 +45,7 @@ import {
   RefundState,
   AbortStatus,
   RefreshSessionRecord,
+  WireInfo,
 } from "../../db.js";
 import { TransactionHandle } from "../../index.js";
 import { PayCoinSelection } from "../../util/coinSelection";
@@ -56,6 +56,7 @@ import { initRetryInfo } from "../../util/retries";
 import { InternalWalletState } from "../state";
 import { provideBackupState } from "./state";
 import { makeEventId, TombstoneTag } from "../transactions.js";
+import { getExchangeDetails } from "../exchanges.js";
 
 const logger = new Logger("operations/backup/import.ts");
 
@@ -102,13 +103,13 @@ async function recoverPayCoinSelection(
     totalDepositFees = Amounts.add(totalDepositFees, denom.feeDeposit).amount;
 
     if (!coveredExchanges.has(coinRecord.exchangeBaseUrl)) {
-      const exchange = await tx.get(
-        Stores.exchanges,
+      const exchangeDetails = await getExchangeDetails(
+        tx,
         coinRecord.exchangeBaseUrl,
       );
-      checkBackupInvariant(!!exchange);
+      checkBackupInvariant(!!exchangeDetails);
       let wireFee: AmountJson | undefined;
-      const feesForType = exchange.wireInfo?.feesForType;
+      const feesForType = exchangeDetails.wireInfo.feesForType;
       checkBackupInvariant(!!feesForType);
       for (const fee of feesForType[contractData.wireMethod] || []) {
         if (
@@ -218,6 +219,7 @@ export async function importBackup(
     [
       Stores.config,
       Stores.exchanges,
+      Stores.exchangeDetails,
       Stores.coins,
       Stores.denominations,
       Stores.purchases,
@@ -245,21 +247,46 @@ export async function importBackup(
 
       const tombstoneSet = new Set(backupBlob.tombstones);
 
+      // FIXME:  Validate that the "details pointer" is correct
+
       for (const backupExchange of backupBlob.exchanges) {
         const existingExchange = await tx.get(
           Stores.exchanges,
           backupExchange.base_url,
         );
+        if (existingExchange) {
+          continue;
+        }
+        await tx.put(Stores.exchanges, {
+          baseUrl: backupExchange.base_url,
+          detailsPointer: {
+            currency: backupExchange.currency,
+            masterPublicKey: backupExchange.master_public_key,
+            updateClock: backupExchange.update_clock,
+          },
+          permanent: true,
+          retryInfo: initRetryInfo(false),
+          updateStarted: { t_ms: "never" },
+          updateStatus: ExchangeUpdateStatus.Finished,
+        });
+      }
+
+      for (const backupExchangeDetails of backupBlob.exchange_details) {
+        const existingExchangeDetails = await tx.get(Stores.exchangeDetails, [
+          backupExchangeDetails.base_url,
+          backupExchangeDetails.currency,
+          backupExchangeDetails.master_public_key,
+        ]);
 
-        if (!existingExchange) {
-          const wireInfo: ExchangeWireInfo = {
-            accounts: backupExchange.accounts.map((x) => ({
+        if (!existingExchangeDetails) {
+          const wireInfo: WireInfo = {
+            accounts: backupExchangeDetails.accounts.map((x) => ({
               master_sig: x.master_sig,
               payto_uri: x.payto_uri,
             })),
             feesForType: {},
           };
-          for (const fee of backupExchange.wire_fees) {
+          for (const fee of backupExchangeDetails.wire_fees) {
             const w = (wireInfo.feesForType[fee.wire_type] ??= []);
             w.push({
               closingFee: Amounts.parseOrThrow(fee.closing_fee),
@@ -269,48 +296,39 @@ export async function importBackup(
               wireFee: Amounts.parseOrThrow(fee.wire_fee),
             });
           }
-          await tx.put(Stores.exchanges, {
-            addComplete: true,
-            baseUrl: backupExchange.base_url,
-            builtIn: false,
-            updateReason: undefined,
-            permanent: true,
-            retryInfo: initRetryInfo(),
-            termsOfServiceAcceptedEtag: backupExchange.tos_etag_accepted,
+          await tx.put(Stores.exchangeDetails, {
+            exchangeBaseUrl: backupExchangeDetails.base_url,
+            termsOfServiceAcceptedEtag: 
backupExchangeDetails.tos_etag_accepted,
             termsOfServiceText: undefined,
-            termsOfServiceLastEtag: backupExchange.tos_etag_last,
-            updateStarted: getTimestampNow(),
-            updateStatus: ExchangeUpdateStatus.FetchKeys,
+            termsOfServiceLastEtag: backupExchangeDetails.tos_etag_last,
             wireInfo,
-            details: {
-              currency: backupExchange.currency,
-              reserveClosingDelay: backupExchange.reserve_closing_delay,
-              auditors: backupExchange.auditors.map((x) => ({
-                auditor_pub: x.auditor_pub,
-                auditor_url: x.auditor_url,
-                denomination_keys: x.denomination_keys,
-              })),
-              lastUpdateTime: { t_ms: "never" },
-              masterPublicKey: backupExchange.master_public_key,
-              nextUpdateTime: { t_ms: "never" },
-              protocolVersion: backupExchange.protocol_version,
-              signingKeys: backupExchange.signing_keys.map((x) => ({
-                key: x.key,
-                master_sig: x.master_sig,
-                stamp_end: x.stamp_end,
-                stamp_expire: x.stamp_expire,
-                stamp_start: x.stamp_start,
-              })),
-            },
+            currency: backupExchangeDetails.currency,
+            auditors: backupExchangeDetails.auditors.map((x) => ({
+              auditor_pub: x.auditor_pub,
+              auditor_url: x.auditor_url,
+              denomination_keys: x.denomination_keys,
+            })),
+            lastUpdateTime: { t_ms: "never" },
+            masterPublicKey: backupExchangeDetails.master_public_key,
+            nextUpdateTime: { t_ms: "never" },
+            protocolVersion: backupExchangeDetails.protocol_version,
+            reserveClosingDelay: backupExchangeDetails.reserve_closing_delay,
+            signingKeys: backupExchangeDetails.signing_keys.map((x) => ({
+              key: x.key,
+              master_sig: x.master_sig,
+              stamp_end: x.stamp_end,
+              stamp_expire: x.stamp_expire,
+              stamp_start: x.stamp_start,
+            })),
           });
         }
 
-        for (const backupDenomination of backupExchange.denominations) {
+        for (const backupDenomination of backupExchangeDetails.denominations) {
           const denomPubHash =
             cryptoComp.denomPubToHash[backupDenomination.denom_pub];
           checkLogicInvariant(!!denomPubHash);
           const existingDenom = await tx.get(Stores.denominations, [
-            backupExchange.base_url,
+            backupExchangeDetails.base_url,
             denomPubHash,
           ]);
           if (!existingDenom) {
@@ -321,7 +339,7 @@ export async function importBackup(
             await tx.put(Stores.denominations, {
               denomPub: backupDenomination.denom_pub,
               denomPubHash: denomPubHash,
-              exchangeBaseUrl: backupExchange.base_url,
+              exchangeBaseUrl: backupExchangeDetails.base_url,
               feeDeposit: Amounts.parseOrThrow(backupDenomination.fee_deposit),
               feeRefresh: Amounts.parseOrThrow(backupDenomination.fee_refresh),
               feeRefund: Amounts.parseOrThrow(backupDenomination.fee_refund),
@@ -378,7 +396,7 @@ export async function importBackup(
                 denomSig: backupCoin.denom_sig,
                 coinPub: compCoin.coinPub,
                 suspended: false,
-                exchangeBaseUrl: backupExchange.base_url,
+                exchangeBaseUrl: backupExchangeDetails.base_url,
                 denomPub: backupDenomination.denom_pub,
                 denomPubHash,
                 status: backupCoin.fresh
@@ -390,7 +408,7 @@ export async function importBackup(
           }
         }
 
-        for (const backupReserve of backupExchange.reserves) {
+        for (const backupReserve of backupExchangeDetails.reserves) {
           const reservePub =
             cryptoComp.reservePrivToPub[backupReserve.reserve_priv];
           const ts = makeEventId(TombstoneTag.DeleteReserve, reservePub);
@@ -414,7 +432,7 @@ export async function importBackup(
             await tx.put(Stores.reserves, {
               currency: instructedAmount.currency,
               instructedAmount,
-              exchangeBaseUrl: backupExchange.base_url,
+              exchangeBaseUrl: backupExchangeDetails.base_url,
               reservePub,
               reservePriv: backupReserve.reserve_priv,
               requestedQuery: false,
@@ -436,7 +454,7 @@ export async function importBackup(
               reserveStatus: ReserveRecordStatus.QUERYING_STATUS,
               initialDenomSel: await getDenomSelStateFromBackup(
                 tx,
-                backupExchange.base_url,
+                backupExchangeDetails.base_url,
                 backupReserve.initial_selected_denoms,
               ),
             });
@@ -457,10 +475,10 @@ export async function importBackup(
               await tx.put(Stores.withdrawalGroups, {
                 denomsSel: await getDenomSelStateFromBackup(
                   tx,
-                  backupExchange.base_url,
+                  backupExchangeDetails.base_url,
                   backupWg.selected_denoms,
                 ),
-                exchangeBaseUrl: backupExchange.base_url,
+                exchangeBaseUrl: backupExchangeDetails.base_url,
                 lastError: undefined,
                 rawWithdrawalAmount: Amounts.parseOrThrow(
                   backupWg.raw_withdrawal_amount,
diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts 
b/packages/taler-wallet-core/src/operations/backup/index.ts
index 110e7659..2314c730 100644
--- a/packages/taler-wallet-core/src/operations/backup/index.ts
+++ b/packages/taler-wallet-core/src/operations/backup/index.ts
@@ -155,8 +155,8 @@ async function computeBackupCryptoData(
     proposalNoncePrivToPub: {},
     reservePrivToPub: {},
   };
-  for (const backupExchange of backupContent.exchanges) {
-    for (const backupDenom of backupExchange.denominations) {
+  for (const backupExchangeDetails of backupContent.exchange_details) {
+    for (const backupDenom of backupExchangeDetails.denominations) {
       for (const backupCoin of backupDenom.coins) {
         const coinPub = encodeCrock(
           eddsaGetPublic(decodeCrock(backupCoin.coin_priv)),
@@ -175,7 +175,7 @@ async function computeBackupCryptoData(
         hash(decodeCrock(backupDenom.denom_pub)),
       );
     }
-    for (const backupReserve of backupExchange.reserves) {
+    for (const backupReserve of backupExchangeDetails.reserves) {
       cryptoData.reservePrivToPub[backupReserve.reserve_priv] = encodeCrock(
         eddsaGetPublic(decodeCrock(backupReserve.reserve_priv)),
       );
diff --git a/packages/taler-wallet-core/src/operations/currencies.ts 
b/packages/taler-wallet-core/src/operations/currencies.ts
index 1af30dfb..5371d4a5 100644
--- a/packages/taler-wallet-core/src/operations/currencies.ts
+++ b/packages/taler-wallet-core/src/operations/currencies.ts
@@ -19,6 +19,7 @@
  */
 import { ExchangeRecord, Stores } from "../db.js";
 import { Logger } from "../index.js";
+import { getExchangeDetails } from "./exchanges.js";
 import { InternalWalletState } from "./state.js";
 
 const logger = new Logger("currencies.ts");
@@ -37,7 +38,12 @@ export async function getExchangeTrust(
 ): Promise<TrustInfo> {
   let isTrusted = false;
   let isAudited = false;
-  const exchangeDetails = exchangeInfo.details;
+  const exchangeDetails = await ws.db.runWithReadTransaction(
+    [Stores.exchangeDetails, Stores.exchanges],
+    async (tx) => {
+      return getExchangeDetails(tx, exchangeInfo.baseUrl);
+    },
+  );
   if (!exchangeDetails) {
     throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
   }
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts 
b/packages/taler-wallet-core/src/operations/deposits.ts
index 4c87f122..59c27b9c 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -58,6 +58,7 @@ import { InternalWalletState } from "./state";
 import { Logger } from "../util/logging.js";
 import { DepositGroupRecord, Stores } from "../db.js";
 import { guardOperationException } from "./errors.js";
+import { getExchangeDetails } from "./exchanges.js";
 
 /**
  * Logger.
@@ -308,14 +309,17 @@ export async function createDepositGroup(
   const allExchanges = await ws.db.iter(Stores.exchanges).toArray();
   const exchangeInfos: { url: string; master_pub: string }[] = [];
   for (const e of allExchanges) {
-    if (!e.details) {
-      continue;
-    }
-    if (e.details.currency != amount.currency) {
+    const details = await ws.db.runWithReadTransaction(
+      [Stores.exchanges, Stores.exchangeDetails],
+      async (tx) => {
+        return getExchangeDetails(tx, e.baseUrl);
+      },
+    );
+    if (!details) {
       continue;
     }
     exchangeInfos.push({
-      master_pub: e.details.masterPublicKey,
+      master_pub: details.masterPublicKey,
       url: e.baseUrl,
     });
   }
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts 
b/packages/taler-wallet-core/src/operations/exchanges.ts
index e8833699..be9a383d 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -19,18 +19,23 @@
  */
 import {
   Amounts,
+  Auditor,
   codecForExchangeKeysJson,
   codecForExchangeWireJson,
   compare,
   Denomination,
   Duration,
   durationFromSpec,
+  ExchangeSignKeyJson,
+  ExchangeWireJson,
   getTimestampNow,
   isTimestampExpired,
   NotificationType,
   parsePaytoUri,
+  Recoup,
   TalerErrorCode,
   TalerErrorDetails,
+  Timestamp,
 } from "@gnu-taler/taler-util";
 import {
   DenominationRecord,
@@ -40,6 +45,8 @@ import {
   ExchangeUpdateStatus,
   WireFee,
   ExchangeUpdateReason,
+  ExchangeDetailsRecord,
+  WireInfo,
 } from "../db.js";
 import {
   Logger,
@@ -47,14 +54,16 @@ import {
   readSuccessResponseJsonOrThrow,
   getExpiryTimestamp,
   readSuccessResponseTextOrThrow,
+  encodeCrock,
+  hash,
+  decodeCrock,
 } from "../index.js";
 import { j2s, canonicalizeBaseUrl } from "@gnu-taler/taler-util";
-import { checkDbInvariant } from "../util/invariants.js";
 import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries.js";
 import {
   makeErrorDetails,
-  OperationFailedAndReportedError,
   guardOperationException,
+  OperationFailedError,
 } from "./errors.js";
 import { createRecoupGroup, processRecoupGroup } from "./recoup.js";
 import { InternalWalletState } from "./state.js";
@@ -62,15 +71,17 @@ import {
   WALLET_CACHE_BREAKER_CLIENT_VERSION,
   WALLET_EXCHANGE_PROTOCOL_VERSION,
 } from "./versions.js";
+import { HttpRequestLibrary } from "../util/http.js";
+import { CryptoApi } from "../crypto/workers/cryptoApi.js";
+import { TransactionHandle } from "../util/query.js";
 
 const logger = new Logger("exchanges.ts");
 
-async function denominationRecordFromKeys(
-  ws: InternalWalletState,
+function denominationRecordFromKeys(
   exchangeBaseUrl: string,
   denomIn: Denomination,
-): Promise<DenominationRecord> {
-  const denomPubHash = await ws.cryptoApi.hashEncoded(denomIn.denom_pub);
+): DenominationRecord {
+  const denomPubHash = encodeCrock(hash(decodeCrock(denomIn.denom_pub)));
   const d: DenominationRecord = {
     denomPub: denomIn.denom_pub,
     denomPubHash,
@@ -115,29 +126,206 @@ function getExchangeRequestTimeout(e: ExchangeRecord): 
Duration {
   return { d_ms: 5000 };
 }
 
+interface ExchangeTosDownloadResult {
+  tosText: string;
+  tosEtag: string;
+}
+
+async function downloadExchangeWithTermsOfService(
+  exchangeBaseUrl: string,
+  http: HttpRequestLibrary,
+  timeout: Duration,
+): Promise<ExchangeTosDownloadResult> {
+  const reqUrl = new URL("terms", exchangeBaseUrl);
+  reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
+  const headers = {
+    Accept: "text/plain",
+  };
+
+  const resp = await http.get(reqUrl.href, {
+    headers,
+    timeout,
+  });
+  const tosText = await readSuccessResponseTextOrThrow(resp);
+  const tosEtag = resp.headers.get("etag") || "unknown";
+
+  return { tosText, tosEtag };
+}
+
+export async function getExchangeDetails(
+  tx: TransactionHandle<
+    typeof Stores.exchanges | typeof Stores.exchangeDetails
+  >,
+  exchangeBaseUrl: string,
+): Promise<ExchangeDetailsRecord | undefined> {
+  const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
+  if (!r) {
+    return;
+  }
+  const dp = r.detailsPointer;
+  if (!dp) {
+    return;
+  }
+  const { currency, masterPublicKey } = dp;
+  return await tx.get(Stores.exchangeDetails, [
+    r.baseUrl,
+    currency,
+    masterPublicKey,
+  ]);
+}
+
+export async function acceptExchangeTermsOfService(
+  ws: InternalWalletState,
+  exchangeBaseUrl: string,
+  etag: string | undefined,
+): Promise<void> {
+  await ws.db.runWithWriteTransaction(
+    [Stores.exchanges, Stores.exchangeDetails],
+    async (tx) => {
+      const d = await getExchangeDetails(tx, exchangeBaseUrl);
+      if (d) {
+        d.termsOfServiceAcceptedEtag = etag;
+        await tx.put(Stores.exchangeDetails, d);
+      }
+    },
+  );
+}
+
+async function validateWireInfo(
+  wireInfo: ExchangeWireJson,
+  masterPublicKey: string,
+  cryptoApi: CryptoApi,
+): Promise<WireInfo> {
+  for (const a of wireInfo.accounts) {
+    logger.trace("validating exchange acct");
+    const isValid = await cryptoApi.isValidWireAccount(
+      a.payto_uri,
+      a.master_sig,
+      masterPublicKey,
+    );
+    if (!isValid) {
+      throw Error("exchange acct signature invalid");
+    }
+  }
+  const feesForType: { [wireMethod: string]: WireFee[] } = {};
+  for (const wireMethod of Object.keys(wireInfo.fees)) {
+    const feeList: WireFee[] = [];
+    for (const x of wireInfo.fees[wireMethod]) {
+      const startStamp = x.start_date;
+      const endStamp = x.end_date;
+      const fee: WireFee = {
+        closingFee: Amounts.parseOrThrow(x.closing_fee),
+        endStamp,
+        sig: x.sig,
+        startStamp,
+        wireFee: Amounts.parseOrThrow(x.wire_fee),
+      };
+      const isValid = await cryptoApi.isValidWireFee(
+        wireMethod,
+        fee,
+        masterPublicKey,
+      );
+      if (!isValid) {
+        throw Error("exchange wire fee signature invalid");
+      }
+      feeList.push(fee);
+    }
+    feesForType[wireMethod] = feeList;
+  }
+
+  return {
+    accounts: wireInfo.accounts,
+    feesForType,
+  };
+}
+
 /**
- * Fetch the exchange's /keys and update our database accordingly.
+ * Fetch wire information for an exchange.
  *
- * Exceptions thrown in this method must be caught and reported
- * in the pending operations.
+ * @param exchangeBaseUrl Exchange base URL, assumed to be already normalized.
  */
-async function updateExchangeWithKeys(
+async function downloadExchangeWithWireInfo(
+  exchangeBaseUrl: string,
+  http: HttpRequestLibrary,
+  timeout: Duration,
+): Promise<ExchangeWireJson> {
+  const reqUrl = new URL("wire", exchangeBaseUrl);
+  reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
+
+  const resp = await http.get(reqUrl.href, {
+    timeout,
+  });
+  const wireInfo = await readSuccessResponseJsonOrThrow(
+    resp,
+    codecForExchangeWireJson(),
+  );
+
+  return wireInfo;
+}
+
+export async function updateExchangeFromUrl(
   ws: InternalWalletState,
   baseUrl: string,
-): Promise<void> {
-  const existingExchangeRecord = await ws.db.get(Stores.exchanges, baseUrl);
+  forceNow = false,
+): Promise<{
+  exchange: ExchangeRecord;
+  exchangeDetails: ExchangeDetailsRecord;
+}> {
+  const onOpErr = (e: TalerErrorDetails): Promise<void> =>
+    handleExchangeUpdateError(ws, baseUrl, e);
+  return await guardOperationException(
+    () => updateExchangeFromUrlImpl(ws, baseUrl, forceNow),
+    onOpErr,
+  );
+}
 
-  if (existingExchangeRecord?.updateStatus != ExchangeUpdateStatus.FetchKeys) {
-    return;
+async function provideExchangeRecord(
+  ws: InternalWalletState,
+  baseUrl: string,
+  now: Timestamp,
+): Promise<ExchangeRecord> {
+  let r = await ws.db.get(Stores.exchanges, baseUrl);
+  if (!r) {
+    const newExchangeRecord: ExchangeRecord = {
+      permanent: true,
+      baseUrl: baseUrl,
+      updateStatus: ExchangeUpdateStatus.FetchKeys,
+      updateStarted: now,
+      updateReason: ExchangeUpdateReason.Initial,
+      retryInfo: initRetryInfo(false),
+      detailsPointer: undefined,
+    };
+    await ws.db.put(Stores.exchanges, newExchangeRecord);
+    r = newExchangeRecord;
   }
+  return r;
+}
 
-  logger.info("updating exchange /keys info");
+interface ExchangeKeysDownloadResult {
+  masterPublicKey: string;
+  currency: string;
+  auditors: Auditor[];
+  currentDenominations: DenominationRecord[];
+  protocolVersion: string;
+  signingKeys: ExchangeSignKeyJson[];
+  reserveClosingDelay: Duration;
+  expiry: Timestamp;
+  recoup: Recoup[];
+}
 
+/**
+ * Download and validate an exchange's /keys data.
+ */
+async function downloadKeysInfo(
+  baseUrl: string,
+  http: HttpRequestLibrary,
+  timeout: Duration,
+): Promise<ExchangeKeysDownloadResult> {
   const keysUrl = new URL("keys", baseUrl);
   keysUrl.searchParams.set("cacheBreaker", 
WALLET_CACHE_BREAKER_CLIENT_VERSION);
 
-  const resp = await ws.http.get(keysUrl.href, {
-    timeout: getExchangeRequestTimeout(existingExchangeRecord),
+  const resp = await http.get(keysUrl.href, {
+    timeout,
   });
   const exchangeKeysJson = await readSuccessResponseJsonOrThrow(
     resp,
@@ -155,8 +343,7 @@ async function updateExchangeWithKeys(
         exchangeBaseUrl: baseUrl,
       },
     );
-    await handleExchangeUpdateError(ws, baseUrl, opErr);
-    throw new OperationFailedAndReportedError(opErr);
+    throw new OperationFailedError(opErr);
   }
 
   const protocolVersion = exchangeKeysJson.version;
@@ -171,70 +358,138 @@ async function updateExchangeWithKeys(
         walletProtocolVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
       },
     );
-    await handleExchangeUpdateError(ws, baseUrl, opErr);
-    throw new OperationFailedAndReportedError(opErr);
+    throw new OperationFailedError(opErr);
   }
 
-  const currency = Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value)
-    .currency;
-
-  logger.trace("processing denominations");
+  const currency = Amounts.parseOrThrow(
+    exchangeKeysJson.denoms[0].value,
+  ).currency.toUpperCase();
 
-  const newDenominations = await Promise.all(
-    exchangeKeysJson.denoms.map((d) =>
-      denominationRecordFromKeys(ws, baseUrl, d),
+  return {
+    masterPublicKey: exchangeKeysJson.master_public_key,
+    currency,
+    auditors: exchangeKeysJson.auditors,
+    currentDenominations: exchangeKeysJson.denoms.map((d) =>
+      denominationRecordFromKeys(baseUrl, d),
     ),
+    protocolVersion: exchangeKeysJson.version,
+    signingKeys: exchangeKeysJson.signkeys,
+    reserveClosingDelay: exchangeKeysJson.reserve_closing_delay,
+    expiry: getExpiryTimestamp(resp, {
+      minDuration: durationFromSpec({ hours: 1 }),
+    }),
+    recoup: exchangeKeysJson.recoup ?? [],
+  };
+}
+
+/**
+ * Update or add exchange DB entry by fetching the /keys and /wire information.
+ * Optionally link the reserve entry to the new or existing
+ * exchange entry in then DB.
+ */
+async function updateExchangeFromUrlImpl(
+  ws: InternalWalletState,
+  baseUrl: string,
+  forceNow = false,
+): Promise<{
+  exchange: ExchangeRecord;
+  exchangeDetails: ExchangeDetailsRecord;
+}> {
+  logger.trace(`updating exchange info for ${baseUrl}`);
+  const now = getTimestampNow();
+  baseUrl = canonicalizeBaseUrl(baseUrl);
+
+  const r = await provideExchangeRecord(ws, baseUrl, now);
+
+  logger.info("updating exchange /keys info");
+
+  const timeout = getExchangeRequestTimeout(r);
+
+  const keysInfo = await downloadKeysInfo(baseUrl, ws.http, timeout);
+
+  const wireInfoDownload = await downloadExchangeWithWireInfo(
+    baseUrl,
+    ws.http,
+    timeout,
   );
 
-  logger.trace("done with processing denominations");
+  const wireInfo = await validateWireInfo(
+    wireInfoDownload,
+    keysInfo.masterPublicKey,
+    ws.cryptoApi,
+  );
 
-  const lastUpdateTimestamp = getTimestampNow();
+  const tosDownload = await downloadExchangeWithTermsOfService(
+    baseUrl,
+    ws.http,
+    timeout,
+  );
 
-  const recoupGroupId: string | undefined = undefined;
+  let recoupGroupId: string | undefined = undefined;
 
-  await ws.db.runWithWriteTransaction(
-    [Stores.exchanges, Stores.denominations, Stores.recoupGroups, 
Stores.coins],
+  const updated = await ws.db.runWithWriteTransaction(
+    [
+      Stores.exchanges,
+      Stores.exchangeDetails,
+      Stores.denominations,
+      Stores.recoupGroups,
+      Stores.coins,
+    ],
     async (tx) => {
       const r = await tx.get(Stores.exchanges, baseUrl);
       if (!r) {
         logger.warn(`exchange ${baseUrl} no longer present`);
         return;
       }
-      if (r.details) {
+      let details = await getExchangeDetails(tx, r.baseUrl);
+      if (details) {
         // FIXME: We need to do some consistency checks!
       }
       // FIXME: validate signing keys and merge with old set
-      r.details = {
-        auditors: exchangeKeysJson.auditors,
-        currency: currency,
-        lastUpdateTime: lastUpdateTimestamp,
-        masterPublicKey: exchangeKeysJson.master_public_key,
-        protocolVersion: protocolVersion,
-        signingKeys: exchangeKeysJson.signkeys,
-        nextUpdateTime: getExpiryTimestamp(resp, {
-          minDuration: durationFromSpec({ hours: 1 }),
-        }),
-        reserveClosingDelay: exchangeKeysJson.reserve_closing_delay,
+      details = {
+        auditors: keysInfo.auditors,
+        currency: keysInfo.currency,
+        lastUpdateTime: now,
+        masterPublicKey: keysInfo.masterPublicKey,
+        protocolVersion: keysInfo.protocolVersion,
+        signingKeys: keysInfo.signingKeys,
+        nextUpdateTime: keysInfo.expiry,
+        reserveClosingDelay: keysInfo.reserveClosingDelay,
+        exchangeBaseUrl: r.baseUrl,
+        wireInfo,
+        termsOfServiceText: tosDownload.tosText,
+        termsOfServiceAcceptedEtag: undefined,
+        termsOfServiceLastEtag: tosDownload.tosEtag,
       };
       r.updateStatus = ExchangeUpdateStatus.FetchWire;
+      // FIXME: only update if pointer got updated
       r.lastError = undefined;
       r.retryInfo = initRetryInfo(false);
+      // New denominations might be available.
+      r.nextRefreshCheck = undefined;
+      r.detailsPointer = {
+        currency: details.currency,
+        masterPublicKey: details.masterPublicKey,
+        // FIXME: only change if pointer really changed
+        updateClock: getTimestampNow(),
+      };
       await tx.put(Stores.exchanges, r);
+      await tx.put(Stores.exchangeDetails, details);
 
-      for (const newDenom of newDenominations) {
+      for (const currentDenom of keysInfo.currentDenominations) {
         const oldDenom = await tx.get(Stores.denominations, [
           baseUrl,
-          newDenom.denomPubHash,
+          currentDenom.denomPubHash,
         ]);
         if (oldDenom) {
           // FIXME: Do consistency check
         } else {
-          await tx.put(Stores.denominations, newDenom);
+          await tx.put(Stores.denominations, currentDenom);
         }
       }
 
       // Handle recoup
-      const recoupDenomList = exchangeKeysJson.recoup ?? [];
+      const recoupDenomList = keysInfo.recoup;
       const newlyRevokedCoinPubs: string[] = [];
       logger.trace("recoup list from exchange", recoupDenomList);
       for (const recoupInfo of recoupDenomList) {
@@ -264,8 +519,12 @@ async function updateExchangeWithKeys(
       }
       if (newlyRevokedCoinPubs.length != 0) {
         logger.trace("recouping coins", newlyRevokedCoinPubs);
-        await createRecoupGroup(ws, tx, newlyRevokedCoinPubs);
+        recoupGroupId = await createRecoupGroup(ws, tx, newlyRevokedCoinPubs);
       }
+      return {
+        exchange: r,
+        exchangeDetails: details,
+      };
     },
   );
 
@@ -277,257 +536,16 @@ async function updateExchangeWithKeys(
     });
   }
 
-  logger.trace("done updating exchange /keys");
-}
-
-async function updateExchangeFinalize(
-  ws: InternalWalletState,
-  exchangeBaseUrl: string,
-): Promise<void> {
-  const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
-  if (!exchange) {
-    return;
-  }
-  if (exchange.updateStatus != ExchangeUpdateStatus.FinalizeUpdate) {
-    return;
+  if (!updated) {
+    throw Error("something went wrong with updating the exchange");
   }
-  await ws.db.runWithWriteTransaction([Stores.exchanges], async (tx) => {
-    const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
-    if (!r) {
-      return;
-    }
-    if (r.updateStatus != ExchangeUpdateStatus.FinalizeUpdate) {
-      return;
-    }
-    r.addComplete = true;
-    r.updateStatus = ExchangeUpdateStatus.Finished;
-    // Reset time to next auto refresh check,
-    // as now new denominations might be available.
-    r.nextRefreshCheck = undefined;
-    await tx.put(Stores.exchanges, r);
-  });
-}
 
-async function updateExchangeWithTermsOfService(
-  ws: InternalWalletState,
-  exchangeBaseUrl: string,
-): Promise<void> {
-  const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
-  if (!exchange) {
-    return;
-  }
-  if (exchange.updateStatus != ExchangeUpdateStatus.FetchTerms) {
-    return;
-  }
-  const reqUrl = new URL("terms", exchangeBaseUrl);
-  reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
-  const headers = {
-    Accept: "text/plain",
+  return {
+    exchange: updated.exchange,
+    exchangeDetails: updated.exchangeDetails,
   };
-
-  const resp = await ws.http.get(reqUrl.href, {
-    headers,
-    timeout: getExchangeRequestTimeout(exchange),
-  });
-  const tosText = await readSuccessResponseTextOrThrow(resp);
-  const tosEtag = resp.headers.get("etag") || undefined;
-
-  await ws.db.runWithWriteTransaction([Stores.exchanges], async (tx) => {
-    const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
-    if (!r) {
-      return;
-    }
-    if (r.updateStatus != ExchangeUpdateStatus.FetchTerms) {
-      return;
-    }
-    r.termsOfServiceText = tosText;
-    r.termsOfServiceLastEtag = tosEtag;
-    r.updateStatus = ExchangeUpdateStatus.FinalizeUpdate;
-    await tx.put(Stores.exchanges, r);
-  });
-}
-
-export async function acceptExchangeTermsOfService(
-  ws: InternalWalletState,
-  exchangeBaseUrl: string,
-  etag: string | undefined,
-): Promise<void> {
-  await ws.db.runWithWriteTransaction([Stores.exchanges], async (tx) => {
-    const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
-    if (!r) {
-      return;
-    }
-    r.termsOfServiceAcceptedEtag = etag;
-    await tx.put(Stores.exchanges, r);
-  });
-}
-
-/**
- * Fetch wire information for an exchange and store it in the database.
- *
- * @param exchangeBaseUrl Exchange base URL, assumed to be already normalized.
- */
-async function updateExchangeWithWireInfo(
-  ws: InternalWalletState,
-  exchangeBaseUrl: string,
-): Promise<void> {
-  const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
-  if (!exchange) {
-    return;
-  }
-  if (exchange.updateStatus != ExchangeUpdateStatus.FetchWire) {
-    return;
-  }
-  const details = exchange.details;
-  if (!details) {
-    throw Error("invalid exchange state");
-  }
-  const reqUrl = new URL("wire", exchangeBaseUrl);
-  reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
-
-  const resp = await ws.http.get(reqUrl.href, {
-    timeout: getExchangeRequestTimeout(exchange),
-  });
-  const wireInfo = await readSuccessResponseJsonOrThrow(
-    resp,
-    codecForExchangeWireJson(),
-  );
-
-  for (const a of wireInfo.accounts) {
-    logger.trace("validating exchange acct");
-    const isValid = await ws.cryptoApi.isValidWireAccount(
-      a.payto_uri,
-      a.master_sig,
-      details.masterPublicKey,
-    );
-    if (!isValid) {
-      throw Error("exchange acct signature invalid");
-    }
-  }
-  const feesForType: { [wireMethod: string]: WireFee[] } = {};
-  for (const wireMethod of Object.keys(wireInfo.fees)) {
-    const feeList: WireFee[] = [];
-    for (const x of wireInfo.fees[wireMethod]) {
-      const startStamp = x.start_date;
-      const endStamp = x.end_date;
-      const fee: WireFee = {
-        closingFee: Amounts.parseOrThrow(x.closing_fee),
-        endStamp,
-        sig: x.sig,
-        startStamp,
-        wireFee: Amounts.parseOrThrow(x.wire_fee),
-      };
-      const isValid = await ws.cryptoApi.isValidWireFee(
-        wireMethod,
-        fee,
-        details.masterPublicKey,
-      );
-      if (!isValid) {
-        throw Error("exchange wire fee signature invalid");
-      }
-      feeList.push(fee);
-    }
-    feesForType[wireMethod] = feeList;
-  }
-
-  await ws.db.runWithWriteTransaction([Stores.exchanges], async (tx) => {
-    const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
-    if (!r) {
-      return;
-    }
-    if (r.updateStatus != ExchangeUpdateStatus.FetchWire) {
-      return;
-    }
-    r.wireInfo = {
-      accounts: wireInfo.accounts,
-      feesForType: feesForType,
-    };
-    r.updateStatus = ExchangeUpdateStatus.FetchTerms;
-    r.lastError = undefined;
-    r.retryInfo = initRetryInfo(false);
-    await tx.put(Stores.exchanges, r);
-  });
 }
 
-export async function updateExchangeFromUrl(
-  ws: InternalWalletState,
-  baseUrl: string,
-  forceNow = false,
-): Promise<ExchangeRecord> {
-  const onOpErr = (e: TalerErrorDetails): Promise<void> =>
-    handleExchangeUpdateError(ws, baseUrl, e);
-  return await guardOperationException(
-    () => updateExchangeFromUrlImpl(ws, baseUrl, forceNow),
-    onOpErr,
-  );
-}
-
-/**
- * Update or add exchange DB entry by fetching the /keys and /wire information.
- * Optionally link the reserve entry to the new or existing
- * exchange entry in then DB.
- */
-async function updateExchangeFromUrlImpl(
-  ws: InternalWalletState,
-  baseUrl: string,
-  forceNow = false,
-): Promise<ExchangeRecord> {
-  logger.trace(`updating exchange info for ${baseUrl}`);
-  const now = getTimestampNow();
-  baseUrl = canonicalizeBaseUrl(baseUrl);
-
-  let r = await ws.db.get(Stores.exchanges, baseUrl);
-  if (!r) {
-    const newExchangeRecord: ExchangeRecord = {
-      builtIn: false,
-      addComplete: false,
-      permanent: true,
-      baseUrl: baseUrl,
-      details: undefined,
-      wireInfo: undefined,
-      updateStatus: ExchangeUpdateStatus.FetchKeys,
-      updateStarted: now,
-      updateReason: ExchangeUpdateReason.Initial,
-      termsOfServiceAcceptedEtag: undefined,
-      termsOfServiceLastEtag: undefined,
-      termsOfServiceText: undefined,
-      retryInfo: initRetryInfo(false),
-    };
-    await ws.db.put(Stores.exchanges, newExchangeRecord);
-  } else {
-    await ws.db.runWithWriteTransaction([Stores.exchanges], async (t) => {
-      const rec = await t.get(Stores.exchanges, baseUrl);
-      if (!rec) {
-        return;
-      }
-      if (rec.updateStatus != ExchangeUpdateStatus.FetchKeys) {
-        const t = rec.details?.nextUpdateTime;
-        if (!forceNow && t && !isTimestampExpired(t)) {
-          return;
-        }
-      }
-      if (rec.updateStatus != ExchangeUpdateStatus.FetchKeys && forceNow) {
-        rec.updateReason = ExchangeUpdateReason.Forced;
-      }
-      rec.updateStarted = now;
-      rec.updateStatus = ExchangeUpdateStatus.FetchKeys;
-      rec.lastError = undefined;
-      rec.retryInfo = initRetryInfo(false);
-      t.put(Stores.exchanges, rec);
-    });
-  }
-
-  await updateExchangeWithKeys(ws, baseUrl);
-  await updateExchangeWithWireInfo(ws, baseUrl);
-  await updateExchangeWithTermsOfService(ws, baseUrl);
-  await updateExchangeFinalize(ws, baseUrl);
-
-  const updatedExchange = await ws.db.get(Stores.exchanges, baseUrl);
-  checkDbInvariant(!!updatedExchange);
-  return updatedExchange;
-}
-
-
 export async function getExchangePaytoUri(
   ws: InternalWalletState,
   exchangeBaseUrl: string,
@@ -535,15 +553,14 @@ export async function getExchangePaytoUri(
 ): Promise<string> {
   // We do the update here, since the exchange might not even exist
   // yet in our database.
-  const exchangeRecord = await updateExchangeFromUrl(ws, exchangeBaseUrl);
-  if (!exchangeRecord) {
-    throw Error(`Exchange '${exchangeBaseUrl}' not found.`);
-  }
-  const exchangeWireInfo = exchangeRecord.wireInfo;
-  if (!exchangeWireInfo) {
-    throw Error(`Exchange wire info for '${exchangeBaseUrl}' not found.`);
-  }
-  for (const account of exchangeWireInfo.accounts) {
+  const details = await ws.db.runWithReadTransaction(
+    [Stores.exchangeDetails, Stores.exchanges],
+    async (tx) => {
+      return getExchangeDetails(tx, exchangeBaseUrl);
+    },
+  );
+  const accounts = details?.wireInfo.accounts ?? [];
+  for (const account of accounts) {
     const res = parsePaytoUri(account.payto_uri);
     if (!res) {
       continue;
diff --git a/packages/taler-wallet-core/src/operations/pay.ts 
b/packages/taler-wallet-core/src/operations/pay.ts
index 1ed8d72b..dad460b8 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -94,6 +94,7 @@ import {
 import { getTotalRefreshCost, createRefreshGroup } from "./refresh.js";
 import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state.js";
 import { ContractTermsUtil } from "../util/contractTerms.js";
+import { getExchangeDetails } from "./exchanges.js";
 
 /**
  * Logger.
@@ -170,11 +171,16 @@ export async function getEffectiveDepositAmount(
     exchangeSet.add(coin.exchangeBaseUrl);
   }
   for (const exchangeUrl of exchangeSet.values()) {
-    const exchange = await ws.db.get(Stores.exchanges, exchangeUrl);
-    if (!exchange?.wireInfo) {
+    const exchangeDetails = await ws.db.runWithReadTransaction(
+      [Stores.exchanges, Stores.exchangeDetails],
+      async (tx) => {
+        return getExchangeDetails(tx, exchangeUrl);
+      },
+    );
+    if (!exchangeDetails) {
       continue;
     }
-    const fee = exchange.wireInfo.feesForType[wireType].find((x) => {
+    const fee = exchangeDetails.wireInfo.feesForType[wireType].find((x) => {
       return timestampIsBetween(getTimestampNow(), x.startStamp, x.endStamp);
     })?.wireFee;
     if (fee) {
@@ -240,11 +246,16 @@ export async function getCandidatePayCoins(
   const exchanges = await ws.db.iter(Stores.exchanges).toArray();
   for (const exchange of exchanges) {
     let isOkay = false;
-    const exchangeDetails = exchange.details;
+    const exchangeDetails = await ws.db.runWithReadTransaction(
+      [Stores.exchanges, Stores.exchangeDetails],
+      async (tx) => {
+        return getExchangeDetails(tx, exchange.baseUrl);
+      },
+    );
     if (!exchangeDetails) {
       continue;
     }
-    const exchangeFees = exchange.wireInfo;
+    const exchangeFees = exchangeDetails.wireInfo;
     if (!exchangeFees) {
       continue;
     }
diff --git a/packages/taler-wallet-core/src/operations/pending.ts 
b/packages/taler-wallet-core/src/operations/pending.ts
index 01920a85..85f8faa1 100644
--- a/packages/taler-wallet-core/src/operations/pending.ts
+++ b/packages/taler-wallet-core/src/operations/pending.ts
@@ -37,9 +37,10 @@ import {
   getDurationRemaining,
   durationMin,
 } from "@gnu-taler/taler-util";
-import { Store, TransactionHandle } from "../util/query";
+import { TransactionHandle } from "../util/query";
 import { InternalWalletState } from "./state";
 import { getBalancesInsideTransaction } from "./balance";
+import { getExchangeDetails } from "./exchanges.js";
 
 function updateRetryDelay(
   oldDelay: Duration,
@@ -52,12 +53,14 @@ function updateRetryDelay(
 }
 
 async function gatherExchangePending(
-  tx: TransactionHandle<typeof Stores.exchanges>,
+  tx: TransactionHandle<
+    typeof Stores.exchanges | typeof Stores.exchangeDetails
+  >,
   now: Timestamp,
   resp: PendingOperationsResponse,
   onlyDue = false,
 ): Promise<void> {
-  await tx.iter(Stores.exchanges).forEach((e) => {
+  await tx.iter(Stores.exchanges).forEachAsync(async (e) => {
     switch (e.updateStatus) {
       case ExchangeUpdateStatus.Finished:
         if (e.lastError) {
@@ -71,30 +74,9 @@ async function gatherExchangePending(
             },
           });
         }
-        if (!e.details) {
-          resp.pendingOperations.push({
-            type: PendingOperationType.Bug,
-            givesLifeness: false,
-            message:
-              "Exchange record does not have details, but no update finished.",
-            details: {
-              exchangeBaseUrl: e.baseUrl,
-            },
-          });
-        }
-        if (!e.wireInfo) {
-          resp.pendingOperations.push({
-            type: PendingOperationType.Bug,
-            givesLifeness: false,
-            message:
-              "Exchange record does not have wire info, but no update 
finished.",
-            details: {
-              exchangeBaseUrl: e.baseUrl,
-            },
-          });
-        }
+        const details = await getExchangeDetails(tx, e.baseUrl);
         const keysUpdateRequired =
-          e.details && e.details.nextUpdateTime.t_ms < now.t_ms;
+          details && details.nextUpdateTime.t_ms < now.t_ms;
         if (keysUpdateRequired) {
           resp.pendingOperations.push({
             type: PendingOperationType.ExchangeUpdate,
@@ -106,7 +88,7 @@ async function gatherExchangePending(
           });
         }
         if (
-          e.details &&
+          details &&
           (!e.nextRefreshCheck || e.nextRefreshCheck.t_ms < now.t_ms)
         ) {
           resp.pendingOperations.push({
diff --git a/packages/taler-wallet-core/src/operations/recoup.ts 
b/packages/taler-wallet-core/src/operations/recoup.ts
index 337892f7..aa551e8d 100644
--- a/packages/taler-wallet-core/src/operations/recoup.ts
+++ b/packages/taler-wallet-core/src/operations/recoup.ts
@@ -24,9 +24,25 @@
 /**
  * Imports.
  */
-import { Amounts, codecForRecoupConfirmation, getTimestampNow, 
NotificationType, RefreshReason, TalerErrorDetails } from 
"@gnu-taler/taler-util";
+import {
+  Amounts,
+  codecForRecoupConfirmation,
+  getTimestampNow,
+  NotificationType,
+  RefreshReason,
+  TalerErrorDetails,
+} from "@gnu-taler/taler-util";
 import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
-import { CoinRecord, CoinSourceType, CoinStatus, RecoupGroupRecord, 
RefreshCoinSource, ReserveRecordStatus, Stores, WithdrawCoinSource } from 
"../db.js";
+import {
+  CoinRecord,
+  CoinSourceType,
+  CoinStatus,
+  RecoupGroupRecord,
+  RefreshCoinSource,
+  ReserveRecordStatus,
+  Stores,
+  WithdrawCoinSource,
+} from "../db.js";
 
 import { readSuccessResponseJsonOrThrow } from "../util/http";
 import { Logger } from "../util/logging";
@@ -34,6 +50,7 @@ import { TransactionHandle } from "../util/query";
 import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries";
 import { URL } from "../util/url";
 import { guardOperationException } from "./errors";
+import { getExchangeDetails } from "./exchanges.js";
 import { createRefreshGroup, processRefreshGroup } from "./refresh";
 import { getReserveRequestTimeout, processReserve } from "./reserves";
 import { InternalWalletState } from "./state";
@@ -155,12 +172,13 @@ async function recoupWithdrawCoin(
     throw Error(`Coin's reserve doesn't match reserve on recoup`);
   }
 
-  const exchange = await ws.db.get(Stores.exchanges, coin.exchangeBaseUrl);
-  if (!exchange) {
-    // FIXME: report inconsistency?
-    return;
-  }
-  const exchangeDetails = exchange.details;
+  const exchangeDetails = await ws.db.runWithReadTransaction(
+    [Stores.exchanges, Stores.exchangeDetails],
+    async (tx) => {
+      return getExchangeDetails(tx, reserve.exchangeBaseUrl);
+    },
+  );
+
   if (!exchangeDetails) {
     // FIXME: report inconsistency?
     return;
@@ -232,13 +250,14 @@ async function recoupRefreshCoin(
     throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`);
   }
 
-  const exchange = await ws.db.get(Stores.exchanges, coin.exchangeBaseUrl);
-  if (!exchange) {
-    logger.warn("exchange for recoup does not exist anymore");
-    // FIXME: report inconsistency?
-    return;
-  }
-  const exchangeDetails = exchange.details;
+  const exchangeDetails = await ws.db.runWithReadTransaction(
+    [Stores.exchanges, Stores.exchangeDetails],
+    async (tx) => {
+      // FIXME:  Get the exchange details based on the
+      // exchange master public key instead of via just the URL.
+      return getExchangeDetails(tx, coin.exchangeBaseUrl);
+    },
+  );
   if (!exchangeDetails) {
     // FIXME: report inconsistency?
     logger.warn("exchange details for recoup not found");
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts 
b/packages/taler-wallet-core/src/operations/refresh.ts
index 84460fb8..9d4390ab 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -122,7 +122,7 @@ async function refreshCreateSession(
     throw Error("Can't refresh, coin not found");
   }
 
-  const exchange = await updateExchangeFromUrl(ws, coin.exchangeBaseUrl);
+  const { exchange } = await updateExchangeFromUrl(ws, coin.exchangeBaseUrl);
   if (!exchange) {
     throw Error("db inconsistent: exchange of coin not found");
   }
diff --git a/packages/taler-wallet-core/src/operations/reserves.ts 
b/packages/taler-wallet-core/src/operations/reserves.ts
index d8821d56..d06ce31e 100644
--- a/packages/taler-wallet-core/src/operations/reserves.ts
+++ b/packages/taler-wallet-core/src/operations/reserves.ts
@@ -58,7 +58,11 @@ import {
   updateRetryInfoTimeout,
 } from "../util/retries.js";
 import { guardOperationException, OperationFailedError } from "./errors.js";
-import { updateExchangeFromUrl, getExchangePaytoUri } from "./exchanges.js";
+import {
+  updateExchangeFromUrl,
+  getExchangePaytoUri,
+  getExchangeDetails,
+} from "./exchanges.js";
 import { InternalWalletState } from "./state.js";
 import {
   updateWithdrawalDenoms,
@@ -148,12 +152,15 @@ export async function createReserve(
   };
 
   const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange);
-  const exchangeDetails = exchangeInfo.details;
+  const exchangeDetails = exchangeInfo.exchangeDetails;
   if (!exchangeDetails) {
     logger.trace(exchangeDetails);
     throw Error("exchange not updated");
   }
-  const { isAudited, isTrusted } = await getExchangeTrust(ws, exchangeInfo);
+  const { isAudited, isTrusted } = await getExchangeTrust(
+    ws,
+    exchangeInfo.exchange,
+  );
 
   const resp = await ws.db.runWithWriteTransaction(
     [Stores.exchangeTrustStore, Stores.reserves, Stores.bankWithdrawUris],
@@ -728,7 +735,11 @@ export async function createTalerWithdrawReserve(
  * Get payto URIs needed to fund a reserve.
  */
 export async function getFundingPaytoUris(
-  tx: TransactionHandle<typeof Stores.reserves | typeof Stores.exchanges>,
+  tx: TransactionHandle<
+    | typeof Stores.reserves
+    | typeof Stores.exchanges
+    | typeof Stores.exchangeDetails
+  >,
   reservePub: string,
 ): Promise<string[]> {
   const r = await tx.get(Stores.reserves, reservePub);
@@ -736,13 +747,13 @@ export async function getFundingPaytoUris(
     logger.error(`reserve ${reservePub} not found (DB corrupted?)`);
     return [];
   }
-  const exchange = await tx.get(Stores.exchanges, r.exchangeBaseUrl);
-  if (!exchange) {
+  const exchangeDetails = await getExchangeDetails(tx, r.exchangeBaseUrl);
+  if (!exchangeDetails) {
     logger.error(`exchange ${r.exchangeBaseUrl} not found (DB corrupted?)`);
     return [];
   }
   const plainPaytoUris =
-    exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
+    exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
   if (!plainPaytoUris) {
     logger.error(`exchange ${r.exchangeBaseUrl} has no wire info`);
     return [];
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts 
b/packages/taler-wallet-core/src/operations/transactions.ts
index 1df7c7be..42ed2d2e 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -38,6 +38,7 @@ import {
   OrderShortInfo,
 } from "@gnu-taler/taler-util";
 import { getFundingPaytoUris } from "./reserves";
+import { getExchangeDetails } from "./exchanges.js";
 
 /**
  * Create an event ID from the type and the primary key for the event.
@@ -89,6 +90,7 @@ export async function getTransactions(
       Stores.coins,
       Stores.denominations,
       Stores.exchanges,
+      Stores.exchangeDetails,
       Stores.proposals,
       Stores.purchases,
       Stores.refreshGroups,
@@ -134,15 +136,18 @@ export async function getTransactions(
             bankConfirmationUrl: r.bankInfo.confirmUrl,
           };
         } else {
-          const exchange = await tx.get(Stores.exchanges, r.exchangeBaseUrl);
-          if (!exchange) {
+          const exchangeDetails = await getExchangeDetails(
+            tx,
+            wsr.exchangeBaseUrl,
+          );
+          if (!exchangeDetails) {
             // FIXME: report somehow
             return;
           }
           withdrawalDetails = {
             type: WithdrawalType.ManualTransfer,
             exchangePaytoUris:
-              exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [],
+              exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [],
           };
         }
         transactions.push({
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts 
b/packages/taler-wallet-core/src/operations/withdraw.ts
index 0ff69cb5..5f050620 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -35,7 +35,7 @@ import {
   PlanchetRecord,
   DenomSelectionState,
   ExchangeRecord,
-  ExchangeWireInfo,
+  ExchangeDetailsRecord,
 } from "../db";
 import {
   BankWithdrawDetails,
@@ -51,7 +51,7 @@ import {
 } from "@gnu-taler/taler-util";
 import { InternalWalletState } from "./state";
 import { Logger } from "../util/logging";
-import { updateExchangeFromUrl } from "./exchanges";
+import { getExchangeDetails, updateExchangeFromUrl } from "./exchanges";
 import {
   WALLET_EXCHANGE_PROTOCOL_VERSION,
   WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
@@ -94,6 +94,8 @@ interface ExchangeWithdrawDetails {
    */
   exchangeInfo: ExchangeRecord;
 
+  exchangeDetails: ExchangeDetailsRecord;
+
   /**
    * Filtered wire info to send to the bank.
    */
@@ -114,11 +116,6 @@ interface ExchangeWithdrawDetails {
    */
   overhead: AmountJson;
 
-  /**
-   * Wire fees from the exchange.
-   */
-  wireFees: ExchangeWireInfo;
-
   /**
    * Does the wallet know about an auditor for
    * the exchange that the reserve.
@@ -639,12 +636,12 @@ export async function updateWithdrawalDenoms(
   ws: InternalWalletState,
   exchangeBaseUrl: string,
 ): Promise<void> {
-  const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
-  if (!exchange) {
-    logger.error("exchange not found");
-    throw Error(`exchange ${exchangeBaseUrl} not found`);
-  }
-  const exchangeDetails = exchange.details;
+  const exchangeDetails = await ws.db.runWithReadTransaction(
+    [Stores.exchanges, Stores.exchangeDetails],
+    async (tx) => {
+      return getExchangeDetails(tx, exchangeBaseUrl);
+    },
+  );
   if (!exchangeDetails) {
     logger.error("exchange details not available");
     throw Error(`exchange ${exchangeBaseUrl} details not available`);
@@ -849,25 +846,19 @@ export async function getExchangeWithdrawalInfo(
   baseUrl: string,
   amount: AmountJson,
 ): Promise<ExchangeWithdrawDetails> {
-  const exchangeInfo = await updateExchangeFromUrl(ws, baseUrl);
-  const exchangeDetails = exchangeInfo.details;
-  if (!exchangeDetails) {
-    throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
-  }
-  const exchangeWireInfo = exchangeInfo.wireInfo;
-  if (!exchangeWireInfo) {
-    throw Error(`exchange ${exchangeInfo.baseUrl} wire details not available`);
-  }
-
+  const { exchange, exchangeDetails } = await updateExchangeFromUrl(
+    ws,
+    baseUrl,
+  );
   await updateWithdrawalDenoms(ws, baseUrl);
   const denoms = await getCandidateWithdrawalDenoms(ws, baseUrl);
   const selectedDenoms = selectWithdrawalDenominations(amount, denoms);
   const exchangeWireAccounts: string[] = [];
-  for (const account of exchangeWireInfo.accounts) {
+  for (const account of exchangeDetails.wireInfo.accounts) {
     exchangeWireAccounts.push(account.payto_uri);
   }
 
-  const { isTrusted, isAudited } = await getExchangeTrust(ws, exchangeInfo);
+  const { isTrusted, isAudited } = await getExchangeTrust(ws, exchange);
 
   let earliestDepositExpiration =
     selectedDenoms.selectedDenoms[0].denom.stampExpireDeposit;
@@ -904,10 +895,10 @@ export async function getExchangeWithdrawalInfo(
 
   let tosAccepted = false;
 
-  if (exchangeInfo.termsOfServiceLastEtag) {
+  if (exchangeDetails.termsOfServiceLastEtag) {
     if (
-      exchangeInfo.termsOfServiceAcceptedEtag ===
-      exchangeInfo.termsOfServiceLastEtag
+      exchangeDetails.termsOfServiceAcceptedEtag ===
+      exchangeDetails.termsOfServiceLastEtag
     ) {
       tosAccepted = true;
     }
@@ -920,7 +911,8 @@ export async function getExchangeWithdrawalInfo(
 
   const ret: ExchangeWithdrawDetails = {
     earliestDepositExpiration,
-    exchangeInfo,
+    exchangeInfo: exchange,
+    exchangeDetails,
     exchangeWireAccounts,
     exchangeVersion: exchangeDetails.protocolVersion || "unknown",
     isAudited,
@@ -932,7 +924,6 @@ export async function getExchangeWithdrawalInfo(
     trustedAuditorPubs: [],
     versionMatch,
     walletVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
-    wireFees: exchangeWireInfo,
     withdrawFee,
     termsOfServiceAccepted: tosAccepted,
   };
@@ -960,29 +951,25 @@ export async function getWithdrawalDetailsForUri(
     }
   }
 
-  const exchangesRes: (ExchangeListItem | undefined)[] = await ws.db
-    .iter(Stores.exchanges)
-    .map((x) => {
-      const details = x.details;
-      if (!details) {
-        return undefined;
-      }
-      if (!x.addComplete) {
-        return undefined;
-      }
-      if (!x.wireInfo) {
-        return undefined;
-      }
-      if (details.currency !== info.amount.currency) {
-        return undefined;
-      }
-      return {
-        exchangeBaseUrl: x.baseUrl,
+  const exchanges: ExchangeListItem[] = [];
+
+  const exchangeRecords = await ws.db.iter(Stores.exchanges).toArray();
+
+  for (const r of exchangeRecords) {
+    const details = await ws.db.runWithReadTransaction(
+      [Stores.exchanges, Stores.exchangeDetails],
+      async (tx) => {
+        return getExchangeDetails(tx, r.baseUrl);
+      },
+    );
+    if (details) {
+      exchanges.push({
+        exchangeBaseUrl: details.exchangeBaseUrl,
         currency: details.currency,
-        paytoUris: x.wireInfo.accounts.map((x) => x.payto_uri),
-      };
-    });
-  const exchanges = exchangesRes.filter((x) => !!x) as ExchangeListItem[];
+        paytoUris: details.wireInfo.accounts.map((x) => x.payto_uri),
+      });
+    }
+  }
 
   return {
     amount: Amounts.stringify(info.amount),
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index 317d81ce..d968fea4 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -105,6 +105,7 @@ import {
   CoinRecord,
   CoinSourceType,
   DenominationRecord,
+  ExchangeDetailsRecord,
   ExchangeRecord,
   PurchaseRecord,
   RefundState,
@@ -232,7 +233,7 @@ export class Wallet {
       exchangeBaseUrl,
       amount,
     );
-    const paytoUris = wi.exchangeInfo.wireInfo?.accounts.map(
+    const paytoUris = wi.exchangeDetails.wireInfo.accounts.map(
       (x) => x.payto_uri,
     );
     if (!paytoUris) {
@@ -586,13 +587,14 @@ export class Wallet {
 
   /**
    * Update or add exchange DB entry by fetching the /keys and /wire 
information.
-   * Optionally link the reserve entry to the new or existing
-   * exchange entry in then DB.
    */
   async updateExchangeFromUrl(
     baseUrl: string,
     force = false,
-  ): Promise<ExchangeRecord> {
+  ): Promise<{
+    exchange: ExchangeRecord;
+    exchangeDetails: ExchangeDetailsRecord;
+  }> {
     try {
       return updateExchangeFromUrl(this.ws, baseUrl, force);
     } finally {
@@ -601,14 +603,16 @@ export class Wallet {
   }
 
   async getExchangeTos(exchangeBaseUrl: string): Promise<GetExchangeTosResult> 
{
-    const exchange = await this.updateExchangeFromUrl(exchangeBaseUrl);
-    const tos = exchange.termsOfServiceText;
-    const currentEtag = exchange.termsOfServiceLastEtag;
+    const { exchange, exchangeDetails } = await this.updateExchangeFromUrl(
+      exchangeBaseUrl,
+    );
+    const tos = exchangeDetails.termsOfServiceText;
+    const currentEtag = exchangeDetails.termsOfServiceLastEtag;
     if (!tos || !currentEtag) {
       throw Error("exchange is in invalid state");
     }
     return {
-      acceptedEtag: exchange.termsOfServiceAcceptedEtag,
+      acceptedEtag: exchangeDetails.termsOfServiceAcceptedEtag,
       currentEtag,
       tos,
     };
@@ -678,28 +682,29 @@ export class Wallet {
   }
 
   async getExchanges(): Promise<ExchangesListRespose> {
-    const exchanges: (ExchangeListItem | undefined)[] = await this.db
-      .iter(Stores.exchanges)
-      .map((x) => {
-        const details = x.details;
-        if (!details) {
-          return undefined;
-        }
-        if (!x.addComplete) {
-          return undefined;
-        }
-        if (!x.wireInfo) {
-          return undefined;
-        }
-        return {
-          exchangeBaseUrl: x.baseUrl,
-          currency: details.currency,
-          paytoUris: x.wireInfo.accounts.map((x) => x.payto_uri),
-        };
+    const exchangeRecords = await this.db.iter(Stores.exchanges).toArray();
+    const exchanges: ExchangeListItem[] = [];
+    for (const r of exchangeRecords) {
+      const dp = r.detailsPointer;
+      if (!dp) {
+        continue;
+      }
+      const { currency, masterPublicKey } = dp;
+      const exchangeDetails = await this.db.get(Stores.exchangeDetails, [
+        r.baseUrl,
+        currency,
+        masterPublicKey,
+      ]);
+      if (!exchangeDetails) {
+        continue;
+      }
+      exchanges.push({
+        exchangeBaseUrl: r.baseUrl,
+        currency,
+        paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
       });
-    return {
-      exchanges: exchanges.filter((x) => !!x) as ExchangeListItem[],
-    };
+    }
+    return { exchanges };
   }
 
   async getCurrencies(): Promise<WalletCurrencyInfo> {

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