gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: separate wallet state from wa


From: gnunet
Subject: [taler-wallet-core] branch master updated: separate wallet state from wallet client
Date: Tue, 15 Jun 2021 18:52:48 +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 d41ae5eb separate wallet state from wallet client
d41ae5eb is described below

commit d41ae5eb97a5264b1d61321354eac049ca317c97
Author: Florian Dold <florian@dold.me>
AuthorDate: Tue Jun 15 18:52:43 2021 +0200

    separate wallet state from wallet client
---
 packages/taler-util/src/walletTypes.ts             |   14 +
 packages/taler-wallet-android/src/index.ts         |   40 +-
 packages/taler-wallet-cli/src/index.ts             |  362 ++--
 .../src/integrationtests/harness.ts                |    5 +-
 .../src/integrationtests/test-payment-fault.ts     |    2 +-
 packages/taler-wallet-core/src/db.ts               |    2 +-
 packages/taler-wallet-core/src/headless/helpers.ts |    7 +-
 packages/taler-wallet-core/src/index.ts            |   34 +-
 .../src/operations/backup/index.ts                 |   16 +-
 .../taler-wallet-core/src/operations/exchanges.ts  |    7 +-
 .../taler-wallet-core/src/operations/refresh.ts    |    5 +-
 packages/taler-wallet-core/src/operations/state.ts |   26 +-
 .../taler-wallet-core/src/operations/testing.ts    |   88 +-
 .../src/operations/transactions.ts                 |    2 +-
 .../src/util/coinSelection.test.ts                 |    2 +-
 packages/taler-wallet-core/src/util/http.ts        |    5 +-
 packages/taler-wallet-core/src/wallet.ts           | 1929 ++++++++++----------
 .../taler-wallet-webextension/src/wxBackend.ts     |   16 +-
 18 files changed, 1284 insertions(+), 1278 deletions(-)

diff --git a/packages/taler-util/src/walletTypes.ts 
b/packages/taler-util/src/walletTypes.ts
index 6a0c5713..4d49db02 100644
--- a/packages/taler-util/src/walletTypes.ts
+++ b/packages/taler-util/src/walletTypes.ts
@@ -622,11 +622,13 @@ export const codecForIntegrationTestArgs = (): 
Codec<IntegrationTestArgs> =>
 
 export interface AddExchangeRequest {
   exchangeBaseUrl: string;
+  forceUpdate?: boolean;
 }
 
 export const codecForAddExchangeRequest = (): Codec<AddExchangeRequest> =>
   buildCodecForObject<AddExchangeRequest>()
     .property("exchangeBaseUrl", codecForString())
+    .property("forceUpdate", codecOptional(codecForBoolean()))
     .build("AddExchangeRequest");
 
 export interface ForceExchangeUpdateRequest {
@@ -962,3 +964,15 @@ export const codecForRetryTransactionRequest = (): 
Codec<RetryTransactionRequest
   buildCodecForObject<RetryTransactionRequest>()
     .property("transactionId", codecForString())
     .build("RetryTransactionRequest");
+
+export interface SetWalletDeviceIdRequest {
+  /**
+   * New wallet device ID to set.
+   */
+  walletDeviceId: string;
+}
+
+export const codecForSetWalletDeviceIdRequest = (): 
Codec<SetWalletDeviceIdRequest> =>
+  buildCodecForObject<SetWalletDeviceIdRequest>()
+    .property("walletDeviceId", codecForString())
+    .build("SetWalletDeviceIdRequest");
diff --git a/packages/taler-wallet-android/src/index.ts 
b/packages/taler-wallet-android/src/index.ts
index 0be45ae7..6f6439fb 100644
--- a/packages/taler-wallet-android/src/index.ts
+++ b/packages/taler-wallet-android/src/index.ts
@@ -18,7 +18,6 @@
  * Imports.
  */
 import {
-  Wallet,
   getDefaultNodeWallet,
   DefaultNodeWalletArgs,
   NodeHttpLib,
@@ -33,7 +32,10 @@ import {
   Headers,
   WALLET_EXCHANGE_PROTOCOL_VERSION,
   WALLET_MERCHANT_PROTOCOL_VERSION,
+  runRetryLoop,
+  handleCoreApiRequest,
 } from "@gnu-taler/taler-wallet-core";
+import { InternalWalletState } from 
"@gnu-taler/taler-wallet-core/lib/operations/state";
 
 import fs from "fs";
 import { WalletNotification } from 
"../../taler-wallet-core/node_modules/@gnu-taler/taler-util/lib/notifications.js";
@@ -154,8 +156,8 @@ function sendAkonoMessage(ev: CoreApiEnvelope): void {
 
 class AndroidWalletMessageHandler {
   walletArgs: DefaultNodeWalletArgs | undefined;
-  maybeWallet: Wallet | undefined;
-  wp = openPromise<Wallet>();
+  maybeWallet: InternalWalletState | undefined;
+  wp = openPromise<InternalWalletState>();
   httpLib = new NodeHttpLib();
 
   /**
@@ -174,6 +176,17 @@ class AndroidWalletMessageHandler {
         result,
       };
     };
+
+    const reinit = async () => {
+      const w = await getDefaultNodeWallet(this.walletArgs);
+      this.maybeWallet = w;
+      await handleCoreApiRequest(w, "initWallet", "akono-init", {});
+      runRetryLoop(w).catch((e) => {
+        console.error("Error during wallet retry loop", e);
+      });
+      this.wp.resolve(w);
+    };
+
     switch (operation) {
       case "init": {
         this.walletArgs = {
@@ -183,12 +196,7 @@ class AndroidWalletMessageHandler {
           persistentStoragePath: args.persistentStoragePath,
           httpLib: this.httpLib,
         };
-        const w = await getDefaultNodeWallet(this.walletArgs);
-        this.maybeWallet = w;
-        w.runRetryLoop().catch((e) => {
-          console.error("Error during wallet retry loop", e);
-        });
-        this.wp.resolve(w);
+        await reinit();
         return wrapResponse({
           supported_protocol_versions: {
             exchange: WALLET_EXCHANGE_PROTOCOL_VERSION,
@@ -196,9 +204,6 @@ class AndroidWalletMessageHandler {
           },
         });
       }
-      case "getHistory": {
-        return wrapResponse({ history: [] });
-      }
       case "startTunnel": {
         // this.httpLib.useNfcTunnel = true;
         throw Error("not implemented");
@@ -225,19 +230,14 @@ class AndroidWalletMessageHandler {
         }
         const wallet = await this.wp.promise;
         wallet.stop();
-        this.wp = openPromise<Wallet>();
+        this.wp = openPromise<InternalWalletState>();
         this.maybeWallet = undefined;
-        const w = await getDefaultNodeWallet(this.walletArgs);
-        this.maybeWallet = w;
-        w.runRetryLoop().catch((e) => {
-          console.error("Error during wallet retry loop", e);
-        });
-        this.wp.resolve(w);
+        await reinit();
         return wrapResponse({});
       }
       default: {
         const wallet = await this.wp.promise;
-        return await wallet.handleCoreApiRequest(operation, id, args);
+        return await handleCoreApiRequest(wallet, operation, id, args);
       }
     }
   }
diff --git a/packages/taler-wallet-cli/src/index.ts 
b/packages/taler-wallet-cli/src/index.ts
index c98ece94..63b969f1 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -33,9 +33,9 @@ import {
   codecForList,
   codecForString,
   Logger,
+  WithdrawalType,
 } from "@gnu-taler/taler-util";
 import {
-  Wallet,
   NodeHttpLib,
   getDefaultNodeWallet,
   OperationFailedAndReportedError,
@@ -45,7 +45,14 @@ import {
   NodeThreadCryptoWorkerFactory,
   CryptoApi,
   walletCoreDebugFlags,
+  WalletCoreApiClient,
+  WalletApiOperation,
+  handleCoreApiRequest,
+  runPending,
+  runUntilDone,
+  getClientFromWalletState,
 } from "@gnu-taler/taler-wallet-core";
+import { InternalWalletState } from 
"@gnu-taler/taler-wallet-core/src/operations/state";
 
 // This module also serves as the entry point for the crypto
 // thread worker, and thus must expose these two handlers.
@@ -63,11 +70,13 @@ function assertUnreachable(x: never): never {
 }
 
 async function doPay(
-  wallet: Wallet,
+  wallet: WalletCoreApiClient,
   payUrl: string,
   options: { alwaysYes: boolean } = { alwaysYes: true },
 ): Promise<void> {
-  const result = await wallet.preparePayForUri(payUrl);
+  const result = await wallet.call(WalletApiOperation.PreparePayForUri, {
+    talerPayUri: payUrl,
+  });
   if (result.status === PreparePayResultType.InsufficientBalance) {
     console.log("contract", result.contractTerms);
     console.error("insufficient balance");
@@ -111,7 +120,9 @@ async function doPay(
   }
 
   if (pay) {
-    await wallet.confirmPay(result.proposalId, undefined);
+    await wallet.call(WalletApiOperation.ConfirmPay, {
+      proposalId: result.proposalId,
+    });
   } else {
     console.log("not paying");
   }
@@ -161,7 +172,10 @@ type WalletCliArgsType = clk.GetArgType<typeof walletCli>;
 
 async function withWallet<T>(
   walletCliArgs: WalletCliArgsType,
-  f: (w: Wallet) => Promise<T>,
+  f: (w: {
+    client: WalletCoreApiClient;
+    ws: InternalWalletState;
+  }) => Promise<T>,
 ): Promise<T> {
   const dbPath = walletCliArgs.wallet.walletDbFile ?? defaultWalletDbPath;
   const myHttpLib = new NodeHttpLib();
@@ -174,8 +188,11 @@ async function withWallet<T>(
   });
   applyVerbose(walletCliArgs.wallet.verbose);
   try {
-    await wallet.fillDefaults();
-    const ret = await f(wallet);
+    const w = {
+      ws: wallet,
+      client: await getClientFromWalletState(wallet),
+    };
+    const ret = await f(w);
     return ret;
   } catch (e) {
     if (
@@ -204,7 +221,10 @@ walletCli
   })
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      const balance = await wallet.getBalances();
+      const balance = await wallet.client.call(
+        WalletApiOperation.GetBalances,
+        {},
+      );
       console.log(JSON.stringify(balance, undefined, 2));
     });
   });
@@ -222,7 +242,8 @@ walletCli
         console.error("Invalid JSON");
         process.exit(1);
       }
-      const resp = await wallet.handleCoreApiRequest(
+      const resp = await handleCoreApiRequest(
+        wallet.ws,
         args.api.operation,
         "reqid-1",
         requestJson,
@@ -235,7 +256,10 @@ walletCli
   .subcommand("", "pending", { help: "Show pending operations." })
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      const pending = await wallet.getPendingOperations();
+      const pending = await wallet.client.call(
+        WalletApiOperation.GetPendingOperations,
+        {},
+      );
       console.log(JSON.stringify(pending, undefined, 2));
     });
   });
@@ -246,10 +270,13 @@ walletCli
   .maybeOption("search", ["--search"], clk.STRING)
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      const pending = await wallet.getTransactions({
-        currency: args.transactions.currency,
-        search: args.transactions.search,
-      });
+      const pending = await wallet.client.call(
+        WalletApiOperation.GetTransactions,
+        {
+          currency: args.transactions.currency,
+          search: args.transactions.search,
+        },
+      );
       console.log(JSON.stringify(pending, undefined, 2));
     });
   });
@@ -267,7 +294,20 @@ walletCli
   .flag("forceNow", ["-f", "--force-now"])
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      await wallet.runPending(args.runPendingOpt.forceNow);
+      await runPending(wallet.ws, args.runPendingOpt.forceNow);
+    });
+  });
+
+walletCli
+  .subcommand("retryTransaction", "retry-transaction", {
+    help: "Retry a transaction.",
+  })
+  .requiredArgument("transactionId", clk.STRING)
+  .action(async (args) => {
+    await withWallet(args, async (wallet) => {
+      await wallet.client.call(WalletApiOperation.RetryTransaction, {
+        transactionId: args.retryTransaction.transactionId,
+      });
     });
   });
 
@@ -278,10 +318,10 @@ walletCli
   .maybeOption("maxRetries", ["--max-retries"], clk.INT)
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      await wallet.runUntilDone({
+      await runUntilDone(wallet.ws, {
         maxRetries: args.finishPendingOpt.maxRetries,
       });
-      wallet.stop();
+      wallet.ws.stop();
     });
   });
 
@@ -294,7 +334,7 @@ walletCli
   })
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      await wallet.deleteTransaction({
+      await wallet.client.call(WalletApiOperation.DeleteTransaction, {
         transactionId: args.deleteTransaction.transactionId,
       });
     });
@@ -312,29 +352,51 @@ walletCli
       const uriType = classifyTalerUri(uri);
       switch (uriType) {
         case TalerUriType.TalerPay:
-          await doPay(wallet, uri, { alwaysYes: args.handleUri.autoYes });
+          await doPay(wallet.client, uri, {
+            alwaysYes: args.handleUri.autoYes,
+          });
           break;
         case TalerUriType.TalerTip:
           {
-            const res = await wallet.prepareTip(uri);
+            const res = await wallet.client.call(
+              WalletApiOperation.PrepareTip,
+              {
+                talerTipUri: uri,
+              },
+            );
             console.log("tip status", res);
-            await wallet.acceptTip(res.walletTipId);
+            await wallet.client.call(WalletApiOperation.AcceptTip, {
+              walletTipId: res.walletTipId,
+            });
           }
           break;
         case TalerUriType.TalerRefund:
-          await wallet.applyRefund(uri);
+          await wallet.client.call(WalletApiOperation.ApplyRefund, {
+            talerRefundUri: uri,
+          });
           break;
         case TalerUriType.TalerWithdraw:
           {
-            const withdrawInfo = await wallet.getWithdrawalDetailsForUri(uri);
+            const withdrawInfo = await wallet.client.call(
+              WalletApiOperation.GetWithdrawalDetailsForUri,
+              {
+                talerWithdrawUri: uri,
+              },
+            );
+            console.log("withdrawInfo", withdrawInfo);
             const selectedExchange = withdrawInfo.defaultExchangeBaseUrl;
             if (!selectedExchange) {
               console.error("no suggested exchange!");
               process.exit(1);
               return;
             }
-            const res = await wallet.acceptWithdrawal(uri, selectedExchange);
-            await wallet.processReserve(res.reservePub);
+            const res = await wallet.client.call(
+              WalletApiOperation.AcceptBankIntegratedWithdrawal,
+              {
+                exchangeBaseUrl: selectedExchange,
+                talerWithdrawUri: uri,
+              },
+            );
           }
           break;
         default:
@@ -356,7 +418,10 @@ exchangesCli
   .action(async (args) => {
     console.log("Listing exchanges ...");
     await withWallet(args, async (wallet) => {
-      const exchanges = await wallet.getExchanges();
+      const exchanges = await wallet.client.call(
+        WalletApiOperation.ListExchanges,
+        {},
+      );
       console.log(JSON.stringify(exchanges, undefined, 2));
     });
   });
@@ -371,10 +436,10 @@ exchangesCli
   .flag("force", ["-f", "--force"])
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      await wallet.updateExchangeFromUrl(
-        args.exchangesUpdateCmd.url,
-        args.exchangesUpdateCmd.force,
-      );
+      await wallet.client.call(WalletApiOperation.AddExchange, {
+        exchangeBaseUrl: args.exchangesUpdateCmd.url,
+        forceUpdate: args.exchangesUpdateCmd.force,
+      });
     });
   });
 
@@ -387,7 +452,9 @@ exchangesCli
   })
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      await wallet.updateExchangeFromUrl(args.exchangesAddCmd.url);
+      await wallet.client.call(WalletApiOperation.AddExchange, {
+        exchangeBaseUrl: args.exchangesAddCmd.url,
+      });
     });
   });
 
@@ -403,10 +470,10 @@ exchangesCli
   })
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      await wallet.acceptExchangeTermsOfService(
-        args.exchangesAcceptTosCmd.url,
-        args.exchangesAcceptTosCmd.etag,
-      );
+      await wallet.client.call(WalletApiOperation.SetExchangeTosAccepted, {
+        etag: args.exchangesAcceptTosCmd.etag,
+        exchangeBaseUrl: args.exchangesAcceptTosCmd.url,
+      });
     });
   });
 
@@ -419,7 +486,12 @@ exchangesCli
   })
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      const tosResult = await wallet.getExchangeTos(args.exchangesTosCmd.url);
+      const tosResult = await wallet.client.call(
+        WalletApiOperation.GetExchangeTos,
+        {
+          exchangeBaseUrl: args.exchangesTosCmd.url,
+        },
+      );
       console.log(JSON.stringify(tosResult, undefined, 2));
     });
   });
@@ -435,65 +507,44 @@ backupCli
   })
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      const backup = await wallet.setDeviceId(args.setDeviceId.deviceId);
-      console.log(JSON.stringify(backup, undefined, 2));
+      await wallet.client.call(WalletApiOperation.SetWalletDeviceId, {
+        walletDeviceId: args.setDeviceId.deviceId,
+      });
     });
   });
 
 backupCli.subcommand("exportPlain", "export-plain").action(async (args) => {
   await withWallet(args, async (wallet) => {
-    const backup = await wallet.exportBackupPlain();
+    const backup = await wallet.client.call(
+      WalletApiOperation.ExportBackupPlain,
+      {},
+    );
     console.log(JSON.stringify(backup, undefined, 2));
   });
 });
 
-backupCli
-  .subcommand("export", "export")
-  .requiredArgument("filename", clk.STRING, {
-    help: "backup filename",
-  })
-  .action(async (args) => {
-    await withWallet(args, async (wallet) => {
-      const backup = await wallet.exportBackupEncrypted();
-      fs.writeFileSync(args.export.filename, backup);
-    });
-  });
-
-backupCli
-  .subcommand("import", "import")
-  .requiredArgument("filename", clk.STRING, {
-    help: "backup filename",
-  })
-  .action(async (args) => {
-    await withWallet(args, async (wallet) => {
-      const backupEncBlob = fs.readFileSync(args.import.filename);
-      await wallet.importBackupEncrypted(backupEncBlob);
-    });
-  });
-
-backupCli.subcommand("importPlain", "import-plain").action(async (args) => {
-  await withWallet(args, async (wallet) => {
-    const data = JSON.parse(await read(process.stdin));
-    await wallet.importBackupPlain(data);
-  });
-});
-
 backupCli.subcommand("recoverySave", "save-recovery").action(async (args) => {
   await withWallet(args, async (wallet) => {
-    const recoveryJson = await wallet.getBackupRecovery();
+    const recoveryJson = await wallet.client.call(
+      WalletApiOperation.ExportBackupRecovery,
+      {},
+    );
     console.log(JSON.stringify(recoveryJson, undefined, 2));
   });
 });
 
 backupCli.subcommand("run", "run").action(async (args) => {
   await withWallet(args, async (wallet) => {
-    await wallet.runBackupCycle();
+    await wallet.client.call(WalletApiOperation.RunBackupCycle, {});
   });
 });
 
 backupCli.subcommand("status", "status").action(async (args) => {
   await withWallet(args, async (wallet) => {
-    const status = await wallet.getBackupStatus();
+    const status = await wallet.client.call(
+      WalletApiOperation.GetBackupInfo,
+      {},
+    );
     console.log(JSON.stringify(status, undefined, 2));
   });
 });
@@ -518,7 +569,7 @@ backupCli
           throw Error("invalid recovery strategy");
         }
       }
-      await wallet.loadBackupRecovery({
+      await wallet.client.call(WalletApiOperation.ImportBackupRecovery, {
         recovery: data,
         strategy,
       });
@@ -531,7 +582,7 @@ backupCli
   .flag("activate", ["--activate"])
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      await wallet.addBackupProvider({
+      await wallet.client.call(WalletApiOperation.AddBackupProvider, {
         backupProviderBaseUrl: args.addProvider.url,
         activate: args.addProvider.activate,
       });
@@ -548,12 +599,15 @@ depositCli
   .requiredArgument("targetPayto", clk.STRING)
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      const resp = await wallet.createDepositGroup({
-        amount: args.createDepositArgs.amount,
-        depositPaytoUri: args.createDepositArgs.targetPayto,
-      });
+      const resp = await wallet.client.call(
+        WalletApiOperation.CreateDepositGroup,
+        {
+          amount: args.createDepositArgs.amount,
+          depositPaytoUri: args.createDepositArgs.targetPayto,
+        },
+      );
       console.log(`Created deposit ${resp.depositGroupId}`);
-      await wallet.runPending();
+      await runPending(wallet.ws);
     });
   });
 
@@ -562,9 +616,12 @@ depositCli
   .requiredArgument("depositGroupId", clk.STRING)
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      const resp = await wallet.trackDepositGroup({
-        depositGroupId: args.trackDepositArgs.depositGroupId,
-      });
+      const resp = await wallet.client.call(
+        WalletApiOperation.TrackDepositGroup,
+        {
+          depositGroupId: args.trackDepositArgs.depositGroupId,
+        },
+      );
       console.log(JSON.stringify(resp, undefined, 2));
     });
   });
@@ -582,9 +639,12 @@ advancedCli
   .requiredArgument("amount", clk.STRING)
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      const details = await wallet.getWithdrawalDetailsForAmount(
-        args.manualWithdrawalDetails.exchange,
-        Amounts.parseOrThrow(args.manualWithdrawalDetails.amount),
+      const details = await wallet.client.call(
+        WalletApiOperation.GetWithdrawalDetailsForAmount,
+        {
+          amount: args.manualWithdrawalDetails.amount,
+          exchangeBaseUrl: args.manualWithdrawalDetails.exchange,
+        },
       );
       console.log(JSON.stringify(details, undefined, 2));
     });
@@ -611,23 +671,33 @@ advancedCli
   })
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      const { exchange, exchangeDetails } = await wallet.updateExchangeFromUrl(
-        args.withdrawManually.exchange,
+      const exchangeBaseUrl = args.withdrawManually.exchange;
+      const amount = args.withdrawManually.amount;
+      const d = await wallet.client.call(
+        WalletApiOperation.GetWithdrawalDetailsForAmount,
+        {
+          amount: args.withdrawManually.amount,
+          exchangeBaseUrl: exchangeBaseUrl,
+        },
       );
-      const acct = exchangeDetails.wireInfo.accounts[0];
+      const acct = d.paytoUris[0];
       if (!acct) {
         console.log("exchange has no accounts");
         return;
       }
-      const reserve = await wallet.acceptManualWithdrawal(
-        exchange.baseUrl,
-        Amounts.parseOrThrow(args.withdrawManually.amount),
+      const resp = await wallet.client.call(
+        WalletApiOperation.AcceptManualWithdrawal,
+        {
+          amount,
+          exchangeBaseUrl,
+        },
       );
-      const completePaytoUri = addPaytoQueryParams(acct.payto_uri, {
+      const reservePub = resp.reservePub;
+      const completePaytoUri = addPaytoQueryParams(acct, {
         amount: args.withdrawManually.amount,
-        message: `Taler top-up ${reserve.reservePub}`,
+        message: `Taler top-up ${reservePub}`,
       });
-      console.log("Created reserve", reserve.reservePub);
+      console.log("Created reserve", reservePub);
       console.log("Payto URI", completePaytoUri);
     });
   });
@@ -640,37 +710,14 @@ currenciesCli
   .subcommand("show", "show", { help: "Show currencies." })
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      const currencies = await wallet.getCurrencies();
+      const currencies = await wallet.client.call(
+        WalletApiOperation.ListCurrencies,
+        {},
+      );
       console.log(JSON.stringify(currencies, undefined, 2));
     });
   });
 
-const reservesCli = advancedCli.subcommand("reserves", "reserves", {
-  help: "Manage reserves.",
-});
-
-reservesCli
-  .subcommand("list", "list", {
-    help: "List reserves.",
-  })
-  .action(async (args) => {
-    await withWallet(args, async (wallet) => {
-      const reserves = await wallet.getReservesForExchange();
-      console.log(JSON.stringify(reserves, undefined, 2));
-    });
-  });
-
-reservesCli
-  .subcommand("update", "update", {
-    help: "Update reserve status via exchange.",
-  })
-  .requiredArgument("reservePub", clk.STRING)
-  .action(async (args) => {
-    await withWallet(args, async (wallet) => {
-      await wallet.updateReserve(args.update.reservePub);
-    });
-  });
-
 advancedCli
   .subcommand("payPrepare", "pay-prepare", {
     help: "Claim an order but don't pay yet.",
@@ -678,7 +725,12 @@ advancedCli
   .requiredArgument("url", clk.STRING)
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      const res = await wallet.preparePayForUri(args.payPrepare.url);
+      const res = await wallet.client.call(
+        WalletApiOperation.PreparePayForUri,
+        {
+          talerPayUri: args.payPrepare.url,
+        },
+      );
       switch (res.status) {
         case PreparePayResultType.InsufficientBalance:
           console.log("insufficient balance");
@@ -707,10 +759,10 @@ advancedCli
   .maybeOption("sessionIdOverride", ["--session-id"], clk.STRING)
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      wallet.confirmPay(
-        args.payConfirm.proposalId,
-        args.payConfirm.sessionIdOverride,
-      );
+      await wallet.client.call(WalletApiOperation.ConfirmPay, {
+        proposalId: args.payConfirm.proposalId,
+        sessionId: args.payConfirm.sessionIdOverride,
+      });
     });
   });
 
@@ -721,7 +773,9 @@ advancedCli
   .requiredArgument("coinPub", clk.STRING)
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      await wallet.refresh(args.refresh.coinPub);
+      await wallet.client.call(WalletApiOperation.ForceRefresh, {
+        coinPubList: [args.refresh.coinPub],
+      });
     });
   });
 
@@ -731,7 +785,10 @@ advancedCli
   })
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      const coinDump = await wallet.dumpCoins();
+      const coinDump = await wallet.client.call(
+        WalletApiOperation.DumpCoins,
+        {},
+      );
       console.log(JSON.stringify(coinDump, undefined, 2));
     });
   });
@@ -755,7 +812,10 @@ advancedCli
         process.exit(1);
       }
       for (const c of coinPubList) {
-        await wallet.setCoinSuspended(c, true);
+        await wallet.client.call(WalletApiOperation.SetCoinSuspended, {
+          coinPub: c,
+          suspended: true,
+        });
       }
     });
   });
@@ -777,7 +837,10 @@ advancedCli
         process.exit(1);
       }
       for (const c of coinPubList) {
-        await wallet.setCoinSuspended(c, false);
+        await wallet.client.call(WalletApiOperation.SetCoinSuspended, {
+          coinPub: c,
+          suspended: false,
+        });
       }
     });
   });
@@ -788,43 +851,18 @@ advancedCli
   })
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      const coins = await wallet.getCoins();
-      for (const coin of coins) {
-        console.log(`coin ${coin.coinPub}`);
-        console.log(` status ${coin.status}`);
-        console.log(` exchange ${coin.exchangeBaseUrl}`);
-        console.log(` denomPubHash ${coin.denomPubHash}`);
+      const coins = await wallet.client.call(WalletApiOperation.DumpCoins, {});
+      for (const coin of coins.coins) {
+        console.log(`coin ${coin.coin_pub}`);
+        console.log(` exchange ${coin.exchange_base_url}`);
+        console.log(` denomPubHash ${coin.denom_pub_hash}`);
         console.log(
-          ` remaining amount ${Amounts.stringify(coin.currentAmount)}`,
+          ` remaining amount ${Amounts.stringify(coin.remaining_value)}`,
         );
       }
     });
   });
 
-advancedCli
-  .subcommand("updateReserve", "update-reserve", {
-    help: "Update reserve status.",
-  })
-  .requiredArgument("reservePub", clk.STRING)
-  .action(async (args) => {
-    await withWallet(args, async (wallet) => {
-      const r = await wallet.updateReserve(args.updateReserve.reservePub);
-      console.log("updated reserve:", JSON.stringify(r, undefined, 2));
-    });
-  });
-
-advancedCli
-  .subcommand("updateReserve", "show-reserve", {
-    help: "Show the current reserve status.",
-  })
-  .requiredArgument("reservePub", clk.STRING)
-  .action(async (args) => {
-    await withWallet(args, async (wallet) => {
-      const r = await wallet.getReserve(args.updateReserve.reservePub);
-      console.log("updated reserve:", JSON.stringify(r, undefined, 2));
-    });
-  });
-
 const testCli = walletCli.subcommand("testingArgs", "testing", {
   help: "Subcommands for testing GNU Taler deployments.",
 });
diff --git a/packages/taler-wallet-cli/src/integrationtests/harness.ts 
b/packages/taler-wallet-cli/src/integrationtests/harness.ts
index b6ea0269..b0a538a7 100644
--- a/packages/taler-wallet-cli/src/integrationtests/harness.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/harness.ts
@@ -1783,7 +1783,10 @@ export class WalletCli {
   }
 
   async forceUpdateExchange(req: ForceExchangeUpdateRequest): Promise<void> {
-    const resp = await this.apiRequest("forceUpdateExchange", req);
+    const resp = await this.apiRequest("addExchange", {
+      exchangeBaseUrl: req.exchangeBaseUrl,
+      forceUpdate: true,
+    });
     if (resp.type === "response") {
       return;
     }
diff --git 
a/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts 
b/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts
index 26b8566b..37ae0739 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts
@@ -213,4 +213,4 @@ export async function runPaymentFaultTest(t: 
GlobalTestState) {
 }
 
 runPaymentFaultTest.suites = ["wallet"];
-runPaymentFaultTest.timeoutMs = 120000;
\ No newline at end of file
+runPaymentFaultTest.timeoutMs = 120000;
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index 8f9d5757..2d2c0615 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -576,7 +576,7 @@ export interface ExchangeDetailsRecord {
 
   /**
    * Timestamp when the ToS was accepted.
-   * 
+   *
    * Used during backup merging.
    */
   termsOfServiceAcceptedTimestamp: Timestamp | undefined;
diff --git a/packages/taler-wallet-core/src/headless/helpers.ts 
b/packages/taler-wallet-core/src/headless/helpers.ts
index 7b918d5d..8125ef6b 100644
--- a/packages/taler-wallet-core/src/headless/helpers.ts
+++ b/packages/taler-wallet-core/src/headless/helpers.ts
@@ -22,7 +22,6 @@
 /**
  * Imports.
  */
-import { Wallet } from "../wallet";
 import {
   MemoryBackend,
   BridgeIDBFactory,
@@ -36,6 +35,7 @@ import { Logger } from "@gnu-taler/taler-util";
 import { SynchronousCryptoWorkerFactory } from 
"../crypto/workers/synchronousWorker";
 import type { IDBFactory } from "@gnu-taler/idb-bridge";
 import { WalletNotification } from "@gnu-taler/taler-util";
+import { InternalWalletState } from "../operations/state.js";
 
 const logger = new Logger("headless/helpers.ts");
 
@@ -93,7 +93,7 @@ function makeId(length: number): string {
  */
 export async function getDefaultNodeWallet(
   args: DefaultNodeWalletArgs = {},
-): Promise<Wallet> {
+): Promise<InternalWalletState> {
   BridgeIDBFactory.enableTracing = false;
   const myBackend = new MemoryBackend();
   myBackend.enableTracing = false;
@@ -172,7 +172,8 @@ export async function getDefaultNodeWallet(
     workerFactory = new SynchronousCryptoWorkerFactory();
   }
 
-  const w = new Wallet(myDb, myHttpLib, workerFactory);
+  const w = new InternalWalletState(myDb, myHttpLib, workerFactory);
+
   if (args.notifyHandler) {
     w.addNotificationListener(args.notifyHandler);
   }
diff --git a/packages/taler-wallet-core/src/index.ts 
b/packages/taler-wallet-core/src/index.ts
index 459c4c07..24109d9b 100644
--- a/packages/taler-wallet-core/src/index.ts
+++ b/packages/taler-wallet-core/src/index.ts
@@ -19,34 +19,34 @@
  */
 
 // Errors
-export * from "./operations/errors";
+export * from "./operations/errors.js";
 
 // Util functionality
-export { URL } from "./util/url";
-export * from "./util/promiseUtils";
-export * from "./util/query";
-export * from "./util/http";
+export { URL } from "./util/url.js";
+export * from "./util/promiseUtils.js";
+export * from "./util/query.js";
+export * from "./util/http.js";
 
 // Utils for using the wallet under node
-export { NodeHttpLib } from "./headless/NodeHttpLib";
+export { NodeHttpLib } from "./headless/NodeHttpLib.js";
 export {
   getDefaultNodeWallet,
   DefaultNodeWalletArgs,
-} from "./headless/helpers";
+} from "./headless/helpers.js";
 
-export * from "./operations/versions";
+export * from "./operations/versions.js";
 
-export * from "./db";
+export * from "./db.js";
 
 // Crypto and crypto workers
-export * from "./crypto/workers/nodeThreadWorker";
-export { CryptoImplementation } from "./crypto/workers/cryptoImplementation";
-export type { CryptoWorker } from "./crypto/workers/cryptoWorker";
-export { CryptoWorkerFactory, CryptoApi } from "./crypto/workers/cryptoApi";
-export * from "./crypto/talerCrypto";
+export * from "./crypto/workers/nodeThreadWorker.js";
+export { CryptoImplementation } from 
"./crypto/workers/cryptoImplementation.js";
+export type { CryptoWorker } from "./crypto/workers/cryptoWorker.js";
+export { CryptoWorkerFactory, CryptoApi } from "./crypto/workers/cryptoApi.js";
+export * from "./crypto/talerCrypto.js";
 
-export * from "./pending-types";
+export * from "./pending-types.js";
 
-export * from "./util/debugFlags";
+export * from "./util/debugFlags.js";
 
-export { Wallet } from "./wallet";
+export * from "./wallet.js";
diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts 
b/packages/taler-wallet-core/src/operations/backup/index.ts
index 00a76bd1..2cc05672 100644
--- a/packages/taler-wallet-core/src/operations/backup/index.ts
+++ b/packages/taler-wallet-core/src/operations/backup/index.ts
@@ -38,7 +38,10 @@ import {
   WalletBackupConfState,
   WALLET_BACKUP_STATE_KEY,
 } from "../../db.js";
-import { checkDbInvariant, checkLogicInvariant } from 
"../../util/invariants.js";
+import {
+  checkDbInvariant,
+  checkLogicInvariant,
+} from "../../util/invariants.js";
 import {
   bytesToString,
   decodeCrock,
@@ -83,8 +86,15 @@ import {
   TalerErrorDetails,
 } from "@gnu-taler/taler-util";
 import { CryptoApi } from "../../crypto/workers/cryptoApi.js";
-import { secretbox, secretbox_open } from 
"../../crypto/primitives/nacl-fast.js";
-import { checkPaymentByProposalId, confirmPay, preparePayForUri } from 
"../pay.js";
+import {
+  secretbox,
+  secretbox_open,
+} from "../../crypto/primitives/nacl-fast.js";
+import {
+  checkPaymentByProposalId,
+  confirmPay,
+  preparePayForUri,
+} from "../pay.js";
 import { exportBackup } from "./export.js";
 import { BackupCryptoPrecomputedData, importBackup } from "./import.js";
 import { provideBackupState, getWalletBackupState } from "./state.js";
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts 
b/packages/taler-wallet-core/src/operations/exchanges.ts
index 1948f70e..c8dfcbc1 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -60,7 +60,12 @@ import {
   WALLET_CACHE_BREAKER_CLIENT_VERSION,
   WALLET_EXCHANGE_PROTOCOL_VERSION,
 } from "./versions.js";
-import { getExpiryTimestamp, HttpRequestLibrary, 
readSuccessResponseJsonOrThrow, readSuccessResponseTextOrThrow } from 
"../util/http.js";
+import {
+  getExpiryTimestamp,
+  HttpRequestLibrary,
+  readSuccessResponseJsonOrThrow,
+  readSuccessResponseTextOrThrow,
+} from "../util/http.js";
 import { CryptoApi } from "../crypto/workers/cryptoApi.js";
 import { DbAccess, GetReadOnlyAccess } from "../util/query.js";
 import { decodeCrock, encodeCrock, hash } from "../crypto/talerCrypto.js";
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts 
b/packages/taler-wallet-core/src/operations/refresh.ts
index c442a7c9..3c81362c 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -55,7 +55,10 @@ import { URL } from "../util/url.js";
 import { guardOperationException } from "./errors.js";
 import { updateExchangeFromUrl } from "./exchanges.js";
 import { EXCHANGE_COINS_LOCK, InternalWalletState } from "./state.js";
-import { isWithdrawableDenom, selectWithdrawalDenominations } from 
"./withdraw.js";
+import {
+  isWithdrawableDenom,
+  selectWithdrawalDenominations,
+} from "./withdraw.js";
 import { RefreshNewDenomInfo } from "../crypto/cryptoTypes.js";
 import { GetReadWriteAccess } from "../util/query.js";
 
diff --git a/packages/taler-wallet-core/src/operations/state.ts 
b/packages/taler-wallet-core/src/operations/state.ts
index 66baa95a..ee7ceb8a 100644
--- a/packages/taler-wallet-core/src/operations/state.ts
+++ b/packages/taler-wallet-core/src/operations/state.ts
@@ -27,8 +27,13 @@ import { WalletStoresV1 } from "../db.js";
 import { PendingOperationsResponse } from "../pending-types.js";
 import { AsyncOpMemoMap, AsyncOpMemoSingle } from "../util/asyncMemo.js";
 import { HttpRequestLibrary } from "../util/http";
-import { OpenedPromise, openPromise } from "../util/promiseUtils.js";
+import {
+  AsyncCondition,
+  OpenedPromise,
+  openPromise,
+} from "../util/promiseUtils.js";
 import { DbAccess } from "../util/query.js";
+import { TimerGroup } from "../util/timer.js";
 
 type NotificationListener = (n: WalletNotification) => void;
 
@@ -37,6 +42,9 @@ const logger = new Logger("state.ts");
 export const EXCHANGE_COINS_LOCK = "exchange-coins-lock";
 export const EXCHANGE_RESERVES_LOCK = "exchange-reserves-lock";
 
+/**
+ * Internal state of the wallet.
+ */
 export class InternalWalletState {
   memoProcessReserve: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
   memoMakePlanchet: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
@@ -47,8 +55,15 @@ export class InternalWalletState {
   memoProcessDeposit: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
   cryptoApi: CryptoApi;
 
+  timerGroup: TimerGroup = new TimerGroup();
+  latch = new AsyncCondition();
+  stopped = false;
+  memoRunRetryLoop = new AsyncOpMemoSingle<void>();
+
   listeners: NotificationListener[] = [];
 
+  initCalled: boolean = false;
+
   /**
    * Promises that are waiting for a particular resource.
    */
@@ -85,6 +100,15 @@ export class InternalWalletState {
     this.listeners.push(f);
   }
 
+  /**
+   * Stop ongoing processing.
+   */
+  stop(): void {
+    this.stopped = true;
+    this.timerGroup.stopCurrentAndFutureTimers();
+    this.cryptoApi.stop();
+  }
+
   /**
    * Run an async function after acquiring a list of locks, identified
    * by string tokens.
diff --git a/packages/taler-wallet-core/src/operations/testing.ts 
b/packages/taler-wallet-core/src/operations/testing.ts
index b163569a..ce3a47f3 100644
--- a/packages/taler-wallet-core/src/operations/testing.ts
+++ b/packages/taler-wallet-core/src/operations/testing.ts
@@ -33,10 +33,13 @@ import {
   TestPayArgs,
   PreparePayResultType,
 } from "@gnu-taler/taler-util";
-import { Wallet } from "../wallet.js";
 import { createTalerWithdrawReserve } from "./reserves.js";
 import { InternalWalletState } from "./state.js";
 import { URL } from "../util/url.js";
+import { confirmPay, preparePayForUri } from "./pay.js";
+import { getBalances } from "./balance.js";
+import { runUntilDone } from "../wallet.js";
+import { applyRefund } from "./refund.js";
 
 const logger = new Logger("operations/testing.ts");
 
@@ -261,14 +264,13 @@ interface BankWithdrawalResponse {
 }
 
 async function makePayment(
-  http: HttpRequestLibrary,
-  wallet: Wallet,
+  ws: InternalWalletState,
   merchant: MerchantBackendInfo,
   amount: string,
   summary: string,
 ): Promise<{ orderId: string }> {
   const orderResp = await createOrder(
-    http,
+    ws.http,
     merchant,
     amount,
     summary,
@@ -277,7 +279,7 @@ async function makePayment(
 
   logger.trace("created order with orderId", orderResp.orderId);
 
-  let paymentStatus = await checkPayment(http, merchant, orderResp.orderId);
+  let paymentStatus = await checkPayment(ws.http, merchant, orderResp.orderId);
 
   logger.trace("payment status", paymentStatus);
 
@@ -286,7 +288,7 @@ async function makePayment(
     throw Error("no taler://pay/ URI in payment response");
   }
 
-  const preparePayResult = await wallet.preparePayForUri(talerPayUri);
+  const preparePayResult = await preparePayForUri(ws, talerPayUri);
 
   logger.trace("prepare pay result", preparePayResult);
 
@@ -294,14 +296,15 @@ async function makePayment(
     throw Error("payment not possible");
   }
 
-  const confirmPayResult = await wallet.confirmPay(
+  const confirmPayResult = await confirmPay(
+    ws,
     preparePayResult.proposalId,
     undefined,
   );
 
   logger.trace("confirmPayResult", confirmPayResult);
 
-  paymentStatus = await checkPayment(http, merchant, orderResp.orderId);
+  paymentStatus = await checkPayment(ws.http, merchant, orderResp.orderId);
 
   logger.trace("payment status after wallet payment:", paymentStatus);
 
@@ -315,8 +318,7 @@ async function makePayment(
 }
 
 export async function runIntegrationTest(
-  http: HttpRequestLibrary,
-  wallet: Wallet,
+  ws: InternalWalletState,
   args: IntegrationTestArgs,
 ): Promise<void> {
   logger.info("running test with arguments", args);
@@ -325,15 +327,16 @@ export async function runIntegrationTest(
   const currency = parsedSpendAmount.currency;
 
   logger.info("withdrawing test balance");
-  await wallet.withdrawTestBalance({
-    amount: args.amountToWithdraw,
-    bankBaseUrl: args.bankBaseUrl,
-    exchangeBaseUrl: args.exchangeBaseUrl,
-  });
-  await wallet.runUntilDone();
+  await withdrawTestBalance(
+    ws,
+    args.amountToWithdraw,
+    args.bankBaseUrl,
+    args.exchangeBaseUrl,
+  );
+  await runUntilDone(ws);
   logger.info("done withdrawing test balance");
 
-  const balance = await wallet.getBalances();
+  const balance = await getBalances(ws);
 
   logger.trace(JSON.stringify(balance, null, 2));
 
@@ -342,16 +345,10 @@ export async function runIntegrationTest(
     authToken: args.merchantAuthToken,
   };
 
-  await makePayment(
-    http,
-    wallet,
-    myMerchant,
-    args.amountToSpend,
-    "hello world",
-  );
+  await makePayment(ws, myMerchant, args.amountToSpend, "hello world");
 
   // Wait until the refresh is done
-  await wallet.runUntilDone();
+  await runUntilDone(ws);
 
   logger.trace("withdrawing test balance for refund");
   const withdrawAmountTwo = Amounts.parseOrThrow(`${currency}:18`);
@@ -359,25 +356,25 @@ export async function runIntegrationTest(
   const refundAmount = Amounts.parseOrThrow(`${currency}:6`);
   const spendAmountThree = Amounts.parseOrThrow(`${currency}:3`);
 
-  await wallet.withdrawTestBalance({
-    amount: Amounts.stringify(withdrawAmountTwo),
-    bankBaseUrl: args.bankBaseUrl,
-    exchangeBaseUrl: args.exchangeBaseUrl,
-  });
+  await withdrawTestBalance(
+    ws,
+    Amounts.stringify(withdrawAmountTwo),
+    args.bankBaseUrl,
+    args.exchangeBaseUrl,
+  );
 
   // Wait until the withdraw is done
-  await wallet.runUntilDone();
+  await runUntilDone(ws);
 
   const { orderId: refundOrderId } = await makePayment(
-    http,
-    wallet,
+    ws,
     myMerchant,
     Amounts.stringify(spendAmountTwo),
     "order that will be refunded",
   );
 
   const refundUri = await refund(
-    http,
+    ws.http,
     myMerchant,
     refundOrderId,
     "test refund",
@@ -386,18 +383,17 @@ export async function runIntegrationTest(
 
   logger.trace("refund URI", refundUri);
 
-  await wallet.applyRefund(refundUri);
+  await applyRefund(ws, refundUri);
 
   logger.trace("integration test: applied refund");
 
   // Wait until the refund is done
-  await wallet.runUntilDone();
+  await runUntilDone(ws);
 
   logger.trace("integration test: making payment after refund");
 
   await makePayment(
-    http,
-    wallet,
+    ws,
     myMerchant,
     Amounts.stringify(spendAmountThree),
     "payment after refund",
@@ -405,30 +401,26 @@ export async function runIntegrationTest(
 
   logger.trace("integration test: make payment done");
 
-  await wallet.runUntilDone();
+  await runUntilDone(ws);
 
   logger.trace("integration test: all done!");
 }
 
-export async function testPay(
-  http: HttpRequestLibrary,
-  wallet: Wallet,
-  args: TestPayArgs,
-) {
+export async function testPay(ws: InternalWalletState, args: TestPayArgs) {
   logger.trace("creating order");
   const merchant = {
     authToken: args.merchantAuthToken,
     baseUrl: args.merchantBaseUrl,
   };
   const orderResp = await createOrder(
-    http,
+    ws.http,
     merchant,
     args.amount,
     args.summary,
     "taler://fulfillment-success/thank+you",
   );
   logger.trace("created new order with order ID", orderResp.orderId);
-  const checkPayResp = await checkPayment(http, merchant, orderResp.orderId);
+  const checkPayResp = await checkPayment(ws.http, merchant, 
orderResp.orderId);
   const talerPayUri = checkPayResp.taler_pay_uri;
   if (!talerPayUri) {
     console.error("fatal: no taler pay URI received from backend");
@@ -436,9 +428,9 @@ export async function testPay(
     return;
   }
   logger.trace("taler pay URI:", talerPayUri);
-  const result = await wallet.preparePayForUri(talerPayUri);
+  const result = await preparePayForUri(ws, talerPayUri);
   if (result.status !== PreparePayResultType.PaymentPossible) {
     throw Error(`unexpected prepare pay status: ${result.status}`);
   }
-  await wallet.confirmPay(result.proposalId, undefined);
+  await confirmPay(ws, result.proposalId, undefined);
 }
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts 
b/packages/taler-wallet-core/src/operations/transactions.ts
index 1b2c8477..5836a6ee 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -424,7 +424,7 @@ export async function retryTransaction(
       break;
     }
     case TransactionType.Payment: {
-      const proposalId = rest[0]
+      const proposalId = rest[0];
       await processPurchasePay(ws, proposalId, true);
       break;
     }
diff --git a/packages/taler-wallet-core/src/util/coinSelection.test.ts 
b/packages/taler-wallet-core/src/util/coinSelection.test.ts
index 1e87bc1f..ed48b8dd 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.test.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.test.ts
@@ -17,7 +17,7 @@
 /**
  * Imports.
  */
- import test from "ava";
+import test from "ava";
 import { AmountJson, Amounts } from "@gnu-taler/taler-util";
 import { AvailableCoinInfo, selectPayCoins } from "./coinSelection.js";
 
diff --git a/packages/taler-wallet-core/src/util/http.ts 
b/packages/taler-wallet-core/src/util/http.ts
index 868619ad..92a9e439 100644
--- a/packages/taler-wallet-core/src/util/http.ts
+++ b/packages/taler-wallet-core/src/util/http.ts
@@ -24,7 +24,10 @@
 /**
  * Imports
  */
-import { OperationFailedError, makeErrorDetails } from 
"../operations/errors.js";
+import {
+  OperationFailedError,
+  makeErrorDetails,
+} from "../operations/errors.js";
 import {
   Logger,
   Duration,
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index 33e431f3..82bc8b44 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -23,33 +23,46 @@
  * Imports.
  */
 import {
+  AcceptBankIntegratedWithdrawalRequest,
+  AcceptExchangeTosRequest,
+  AcceptManualWithdrawalRequest,
+  AcceptTipRequest,
+  AddExchangeRequest,
+  ApplyRefundRequest,
   BackupRecovery,
   codecForAny,
   codecForDeleteTransactionRequest,
   codecForRetryTransactionRequest,
+  codecForSetWalletDeviceIdRequest,
+  ConfirmPayRequest,
   DeleteTransactionRequest,
   durationFromSpec,
-  durationMax,
   durationMin,
+  ForceRefreshRequest,
   getDurationRemaining,
+  GetExchangeTosRequest,
+  GetWithdrawalDetailsForAmountRequest,
+  GetWithdrawalDetailsForUriRequest,
   isTimestampExpired,
   j2s,
+  PreparePayRequest,
+  PrepareTipRequest,
+  RetryTransactionRequest,
+  SetCoinSuspendedRequest,
+  SetWalletDeviceIdRequest,
   TalerErrorCode,
   Timestamp,
   timestampMin,
+  WalletBackupContentV1,
   WalletCurrencyInfo,
 } from "@gnu-taler/taler-util";
-import { CryptoWorkerFactory } from "./crypto/workers/cryptoApi";
 import {
   addBackupProvider,
   AddBackupProviderRequest,
   BackupInfo,
   codecForAddBackupProviderRequest,
-  exportBackupEncrypted,
   getBackupInfo,
   getBackupRecovery,
-  importBackupEncrypted,
-  importBackupPlain,
   loadBackupRecovery,
   runBackupCycle,
 } from "./operations/backup";
@@ -68,7 +81,6 @@ import {
 import {
   acceptExchangeTermsOfService,
   getExchangeDetails,
-  getExchangePaytoUri,
   updateExchangeFromUrl,
 } from "./operations/exchanges";
 import {
@@ -76,7 +88,6 @@ import {
   preparePayForUri,
   processDownloadProposal,
   processPurchasePay,
-  refuseProposal,
 } from "./operations/pay";
 import { getPendingOperations } from "./operations/pending";
 import { processRecoupGroup } from "./operations/recoup";
@@ -93,7 +104,6 @@ import {
 import {
   createReserve,
   createTalerWithdrawReserve,
-  forceQueryReserve,
   getFundingPaytoUris,
   processReserve,
 } from "./operations/reserves";
@@ -104,7 +114,11 @@ import {
   withdrawTestBalance,
 } from "./operations/testing";
 import { acceptTip, prepareTip, processTip } from "./operations/tip";
-import { deleteTransaction, getTransactions, retryTransaction } from 
"./operations/transactions";
+import {
+  deleteTransaction,
+  getTransactions,
+  retryTransaction,
+} from "./operations/transactions";
 import {
   getExchangeWithdrawalInfo,
   getWithdrawalDetailsForUri,
@@ -112,15 +126,10 @@ import {
 } from "./operations/withdraw";
 import {
   AuditorTrustRecord,
-  CoinRecord,
   CoinSourceType,
-  ExchangeDetailsRecord,
-  ExchangeRecord,
-  ReserveRecord,
   ReserveRecordStatus,
-  WalletStoresV1,
 } from "./db.js";
-import { NotificationType, WalletNotification } from "@gnu-taler/taler-util";
+import { NotificationType } from "@gnu-taler/taler-util";
 import {
   PendingOperationInfo,
   PendingOperationsResponse,
@@ -137,7 +146,6 @@ import {
   AcceptWithdrawalResponse,
   ApplyRefundResponse,
   BalancesResponse,
-  BenchmarkResult,
   codecForAbortPayWithRefundRequest,
   codecForAcceptBankIntegratedWithdrawalRequest,
   codecForAcceptExchangeTosRequest,
@@ -147,7 +155,6 @@ import {
   codecForApplyRefundRequest,
   codecForConfirmPayRequest,
   codecForCreateDepositGroupRequest,
-  codecForForceExchangeUpdateRequest,
   codecForForceRefreshRequest,
   codecForGetExchangeTosRequest,
   codecForGetWithdrawalDetailsForAmountRequest,
@@ -166,28 +173,18 @@ import {
   ExchangeListItem,
   ExchangesListRespose,
   GetExchangeTosResult,
-  IntegrationTestArgs,
   ManualWithdrawalDetails,
   PreparePayResult,
   PrepareTipResult,
   RecoveryLoadRequest,
   RefreshReason,
-  ReturnCoinsRequest,
-  TestPayArgs,
   TrackDepositGroupRequest,
   TrackDepositGroupResponse,
-  WithdrawTestBalanceRequest,
   WithdrawUriInfoResponse,
 } from "@gnu-taler/taler-util";
 import { AmountJson, Amounts } from "@gnu-taler/taler-util";
 import { assertUnreachable } from "./util/assertUnreachable";
-import { AsyncOpMemoSingle } from "./util/asyncMemo";
-import { HttpRequestLibrary } from "./util/http";
 import { Logger } from "@gnu-taler/taler-util";
-import { AsyncCondition } from "./util/promiseUtils";
-import { TimerGroup } from "./util/timer";
-import { getExchangeTrust } from "./operations/currencies.js";
-import { DbAccess } from "./util/query.js";
 import { setWalletDeviceId } from "./operations/backup/state.js";
 
 const builtinAuditors: AuditorTrustRecord[] = [
@@ -201,440 +198,815 @@ const builtinAuditors: AuditorTrustRecord[] = [
 
 const logger = new Logger("wallet.ts");
 
-/**
- * The platform-independent wallet implementation.
- */
-export class Wallet {
-  private ws: InternalWalletState;
-  private timerGroup: TimerGroup = new TimerGroup();
-  private latch = new AsyncCondition();
-  private stopped = false;
-  private memoRunRetryLoop = new AsyncOpMemoSingle<void>();
-
-  get db(): DbAccess<typeof WalletStoresV1> {
-    return this.ws.db;
-  }
-
-  constructor(
-    db: DbAccess<typeof WalletStoresV1>,
-    http: HttpRequestLibrary,
-    cryptoWorkerFactory: CryptoWorkerFactory,
-  ) {
-    this.ws = new InternalWalletState(db, http, cryptoWorkerFactory);
+async function getWithdrawalDetailsForAmount(
+  ws: InternalWalletState,
+  exchangeBaseUrl: string,
+  amount: AmountJson,
+): Promise<ManualWithdrawalDetails> {
+  const wi = await getExchangeWithdrawalInfo(ws, exchangeBaseUrl, amount);
+  const paytoUris = wi.exchangeDetails.wireInfo.accounts.map(
+    (x) => x.payto_uri,
+  );
+  if (!paytoUris) {
+    throw Error("exchange is in invalid state");
   }
+  return {
+    amountRaw: Amounts.stringify(amount),
+    amountEffective: Amounts.stringify(wi.selectedDenoms.totalCoinValue),
+    paytoUris,
+    tosAccepted: wi.termsOfServiceAccepted,
+  };
+}
 
-  getExchangePaytoUri(
-    exchangeBaseUrl: string,
-    supportedTargetTypes: string[],
-  ): Promise<string> {
-    return getExchangePaytoUri(this.ws, exchangeBaseUrl, supportedTargetTypes);
+/**
+ * Execute one operation based on the pending operation info record.
+ */
+async function processOnePendingOperation(
+  ws: InternalWalletState,
+  pending: PendingOperationInfo,
+  forceNow = false,
+): Promise<void> {
+  logger.trace(`running pending ${JSON.stringify(pending, undefined, 2)}`);
+  switch (pending.type) {
+    case PendingOperationType.ExchangeUpdate:
+      await updateExchangeFromUrl(ws, pending.exchangeBaseUrl, forceNow);
+      break;
+    case PendingOperationType.Refresh:
+      await processRefreshGroup(ws, pending.refreshGroupId, forceNow);
+      break;
+    case PendingOperationType.Reserve:
+      await processReserve(ws, pending.reservePub, forceNow);
+      break;
+    case PendingOperationType.Withdraw:
+      await processWithdrawGroup(ws, pending.withdrawalGroupId, forceNow);
+      break;
+    case PendingOperationType.ProposalDownload:
+      await processDownloadProposal(ws, pending.proposalId, forceNow);
+      break;
+    case PendingOperationType.TipPickup:
+      await processTip(ws, pending.tipId, forceNow);
+      break;
+    case PendingOperationType.Pay:
+      await processPurchasePay(ws, pending.proposalId, forceNow);
+      break;
+    case PendingOperationType.RefundQuery:
+      await processPurchaseQueryRefund(ws, pending.proposalId, forceNow);
+      break;
+    case PendingOperationType.Recoup:
+      await processRecoupGroup(ws, pending.recoupGroupId, forceNow);
+      break;
+    case PendingOperationType.ExchangeCheckRefresh:
+      await autoRefresh(ws, pending.exchangeBaseUrl);
+      break;
+    case PendingOperationType.Deposit:
+      await processDepositGroup(ws, pending.depositGroupId);
+      break;
+    default:
+      assertUnreachable(pending);
   }
+}
 
-  async getWithdrawalDetailsForAmount(
-    exchangeBaseUrl: string,
-    amount: AmountJson,
-  ): Promise<ManualWithdrawalDetails> {
-    const wi = await getExchangeWithdrawalInfo(
-      this.ws,
-      exchangeBaseUrl,
-      amount,
-    );
-    const paytoUris = wi.exchangeDetails.wireInfo.accounts.map(
-      (x) => x.payto_uri,
-    );
-    if (!paytoUris) {
-      throw Error("exchange is in invalid state");
+/**
+ * Process pending operations.
+ */
+export async function runPending(
+  ws: InternalWalletState,
+  forceNow = false,
+): Promise<void> {
+  const pendingOpsResponse = await getPendingOperations(ws);
+  for (const p of pendingOpsResponse.pendingOperations) {
+    if (!forceNow && !isTimestampExpired(p.timestampDue)) {
+      continue;
     }
-    return {
-      amountRaw: Amounts.stringify(amount),
-      amountEffective: Amounts.stringify(wi.selectedDenoms.totalCoinValue),
-      paytoUris,
-      tosAccepted: wi.termsOfServiceAccepted,
-    };
-  }
-
-  addNotificationListener(f: (n: WalletNotification) => void): void {
-    this.ws.addNotificationListener(f);
-  }
-
-  /**
-   * Execute one operation based on the pending operation info record.
-   */
-  async processOnePendingOperation(
-    pending: PendingOperationInfo,
-    forceNow = false,
-  ): Promise<void> {
-    logger.trace(`running pending ${JSON.stringify(pending, undefined, 2)}`);
-    switch (pending.type) {
-      case PendingOperationType.ExchangeUpdate:
-        await updateExchangeFromUrl(this.ws, pending.exchangeBaseUrl, 
forceNow);
-        break;
-      case PendingOperationType.Refresh:
-        await processRefreshGroup(this.ws, pending.refreshGroupId, forceNow);
-        break;
-      case PendingOperationType.Reserve:
-        await processReserve(this.ws, pending.reservePub, forceNow);
-        break;
-      case PendingOperationType.Withdraw:
-        await processWithdrawGroup(
-          this.ws,
-          pending.withdrawalGroupId,
-          forceNow,
+    try {
+      await processOnePendingOperation(ws, p, forceNow);
+    } catch (e) {
+      if (e instanceof OperationFailedAndReportedError) {
+        console.error(
+          "Operation failed:",
+          JSON.stringify(e.operationError, undefined, 2),
         );
-        break;
-      case PendingOperationType.ProposalDownload:
-        await processDownloadProposal(this.ws, pending.proposalId, forceNow);
-        break;
-      case PendingOperationType.TipPickup:
-        await processTip(this.ws, pending.tipId, forceNow);
-        break;
-      case PendingOperationType.Pay:
-        await processPurchasePay(this.ws, pending.proposalId, forceNow);
-        break;
-      case PendingOperationType.RefundQuery:
-        await processPurchaseQueryRefund(this.ws, pending.proposalId, 
forceNow);
-        break;
-      case PendingOperationType.Recoup:
-        await processRecoupGroup(this.ws, pending.recoupGroupId, forceNow);
-        break;
-      case PendingOperationType.ExchangeCheckRefresh:
-        await autoRefresh(this.ws, pending.exchangeBaseUrl);
-        break;
-      case PendingOperationType.Deposit:
-        await processDepositGroup(this.ws, pending.depositGroupId);
-        break;
-      default:
-        assertUnreachable(pending);
+      } else {
+        console.error(e);
+      }
     }
   }
+}
 
-  /**
-   * Process pending operations.
-   */
-  public async runPending(forceNow = false): Promise<void> {
-    const pendingOpsResponse = await this.getPendingOperations();
-    for (const p of pendingOpsResponse.pendingOperations) {
-      if (!forceNow && !isTimestampExpired(p.timestampDue)) {
-        continue;
+/**
+ * Run the wallet until there are no more pending operations that give
+ * liveness left.  The wallet will be in a stopped state when this function
+ * returns without resolving to an exception.
+ */
+export async function runUntilDone(
+  ws: InternalWalletState,
+  req: {
+    maxRetries?: number;
+  } = {},
+): Promise<void> {
+  let done = false;
+  const p = new Promise<void>((resolve, reject) => {
+    // Monitor for conditions that means we're done or we
+    // should quit with an error (due to exceeded retries).
+    ws.addNotificationListener((n) => {
+      if (done) {
+        return;
       }
-      try {
-        await this.processOnePendingOperation(p, forceNow);
-      } catch (e) {
-        if (e instanceof OperationFailedAndReportedError) {
-          console.error(
-            "Operation failed:",
-            JSON.stringify(e.operationError, undefined, 2),
-          );
-        } else {
-          console.error(e);
-        }
+      if (
+        n.type === NotificationType.WaitingForRetry &&
+        n.numGivingLiveness == 0
+      ) {
+        done = true;
+        logger.trace("no liveness-giving operations left");
+        resolve();
       }
-    }
-  }
-
-  /**
-   * Run the wallet until there are no more pending operations that give
-   * liveness left.  The wallet will be in a stopped state when this function
-   * returns without resolving to an exception.
-   */
-  public async runUntilDone(
-    req: {
-      maxRetries?: number;
-    } = {},
-  ): Promise<void> {
-    let done = false;
-    const p = new Promise<void>((resolve, reject) => {
-      // Monitor for conditions that means we're done or we
-      // should quit with an error (due to exceeded retries).
-      this.addNotificationListener((n) => {
-        if (done) {
-          return;
-        }
-        if (
-          n.type === NotificationType.WaitingForRetry &&
-          n.numGivingLiveness == 0
-        ) {
-          done = true;
-          logger.trace("no liveness-giving operations left");
-          resolve();
-        }
-        const maxRetries = req.maxRetries;
-        if (!maxRetries) {
-          return;
-        }
-        this.getPendingOperations()
-          .then((pending) => {
-            for (const p of pending.pendingOperations) {
-              if (p.retryInfo && p.retryInfo.retryCounter > maxRetries) {
-                console.warn(
-                  `stopping, as ${maxRetries} retries are exceeded in an 
operation of type ${p.type}`,
-                );
-                this.stop();
-                done = true;
-                resolve();
-              }
+      const maxRetries = req.maxRetries;
+      if (!maxRetries) {
+        return;
+      }
+      getPendingOperations(ws)
+        .then((pending) => {
+          for (const p of pending.pendingOperations) {
+            if (p.retryInfo && p.retryInfo.retryCounter > maxRetries) {
+              console.warn(
+                `stopping, as ${maxRetries} retries are exceeded in an 
operation of type ${p.type}`,
+              );
+              ws.stop();
+              done = true;
+              resolve();
             }
-          })
-          .catch((e) => {
-            logger.error(e);
-            reject(e);
-          });
-      });
-      // Run this asynchronously
-      this.runRetryLoop().catch((e) => {
-        logger.error("exception in wallet retry loop");
-        reject(e);
-      });
+          }
+        })
+        .catch((e) => {
+          logger.error(e);
+          reject(e);
+        });
     });
-    await p;
-  }
-
-  /**
-   * Process pending operations and wait for scheduled operations in
-   * a loop until the wallet is stopped explicitly.
-   */
-  public async runRetryLoop(): Promise<void> {
-    // Make sure we only run one main loop at a time.
-    return this.memoRunRetryLoop.memo(async () => {
-      try {
-        await this.runRetryLoopImpl();
-      } catch (e) {
-        console.error("error during retry loop execution", e);
-        throw e;
-      }
+    // Run this asynchronously
+    runRetryLoop(ws).catch((e) => {
+      logger.error("exception in wallet retry loop");
+      reject(e);
     });
-  }
+  });
+  await p;
+}
 
-  private async runRetryLoopImpl(): Promise<void> {
-    for (let iteration = 0; !this.stopped; iteration++) {
-      const pending = await this.getPendingOperations();
-      logger.trace(`pending operations: ${j2s(pending)}`);
-      let numGivingLiveness = 0;
-      let numDue = 0;
-      let minDue: Timestamp = { t_ms: "never" };
+/**
+ * Process pending operations and wait for scheduled operations in
+ * a loop until the wallet is stopped explicitly.
+ */
+export async function runRetryLoop(ws: InternalWalletState): Promise<void> {
+  // Make sure we only run one main loop at a time.
+  return ws.memoRunRetryLoop.memo(async () => {
+    try {
+      await runRetryLoopImpl(ws);
+    } catch (e) {
+      console.error("error during retry loop execution", e);
+      throw e;
+    }
+  });
+}
+
+async function runRetryLoopImpl(ws: InternalWalletState): Promise<void> {
+  for (let iteration = 0; !ws.stopped; iteration++) {
+    const pending = await getPendingOperations(ws);
+    logger.trace(`pending operations: ${j2s(pending)}`);
+    let numGivingLiveness = 0;
+    let numDue = 0;
+    let minDue: Timestamp = { t_ms: "never" };
+    for (const p of pending.pendingOperations) {
+      minDue = timestampMin(minDue, p.timestampDue);
+      if (isTimestampExpired(p.timestampDue)) {
+        numDue++;
+      }
+      if (p.givesLifeness) {
+        numGivingLiveness++;
+      }
+    }
+    // Make sure that we run tasks that don't give lifeness at least
+    // one time.
+    if (iteration !== 0 && numDue === 0) {
+      // We've executed pending, due operations at least one.
+      // Now we don't have any more operations available,
+      // and need to wait.
+
+      // Wait for at most 5 seconds to the next check.
+      const dt = durationMin(
+        durationFromSpec({
+          seconds: 5,
+        }),
+        getDurationRemaining(minDue),
+      );
+      logger.trace(`waiting for at most ${dt.d_ms} ms`);
+      const timeout = ws.timerGroup.resolveAfter(dt);
+      ws.notify({
+        type: NotificationType.WaitingForRetry,
+        numGivingLiveness,
+        numPending: pending.pendingOperations.length,
+      });
+      // Wait until either the timeout, or we are notified (via the latch)
+      // that more work might be available.
+      await Promise.race([timeout, ws.latch.wait()]);
+    } else {
+      logger.trace(
+        `running ${pending.pendingOperations.length} pending operations`,
+      );
       for (const p of pending.pendingOperations) {
-        minDue = timestampMin(minDue, p.timestampDue);
-        if (isTimestampExpired(p.timestampDue)) {
-          numDue++;
-        }
-        if (p.givesLifeness) {
-            numGivingLiveness++;
+        if (!isTimestampExpired(p.timestampDue)) {
+          continue;
         }
-      }
-      // Make sure that we run tasks that don't give lifeness at least
-      // one time.
-      if (iteration !== 0 && numDue === 0) {
-        // We've executed pending, due operations at least one.
-        // Now we don't have any more operations available,
-        // and need to wait.
-
-        // Wait for at most 5 seconds to the next check.
-        const dt = durationMin(
-          durationFromSpec({
-            seconds: 5,
-          }),
-          getDurationRemaining(minDue),
-        );
-        logger.trace(`waiting for at most ${dt.d_ms} ms`)
-        const timeout = this.timerGroup.resolveAfter(dt);
-        this.ws.notify({
-          type: NotificationType.WaitingForRetry,
-          numGivingLiveness,
-          numPending: pending.pendingOperations.length,
-        });
-        // Wait until either the timeout, or we are notified (via the latch)
-        // that more work might be available.
-        await Promise.race([timeout, this.latch.wait()]);
-      } else {
-        logger.trace(
-          `running ${pending.pendingOperations.length} pending operations`,
-        );
-        for (const p of pending.pendingOperations) {
-          if (!isTimestampExpired(p.timestampDue)) {
-            continue;
-          }
-          try {
-            await this.processOnePendingOperation(p);
-          } catch (e) {
-            if (e instanceof OperationFailedAndReportedError) {
-              logger.warn("operation processed resulted in reported error");
-            } else {
-              logger.error("Uncaught exception", e);
-              this.ws.notify({
-                type: NotificationType.InternalError,
-                message: "uncaught exception",
-                exception: e,
-              });
-            }
+        try {
+          await processOnePendingOperation(ws, p);
+        } catch (e) {
+          if (e instanceof OperationFailedAndReportedError) {
+            logger.warn("operation processed resulted in reported error");
+          } else {
+            logger.error("Uncaught exception", e);
+            ws.notify({
+              type: NotificationType.InternalError,
+              message: "uncaught exception",
+              exception: e,
+            });
           }
-          this.ws.notify({
-            type: NotificationType.PendingOperationProcessed,
-          });
         }
+        ws.notify({
+          type: NotificationType.PendingOperationProcessed,
+        });
       }
     }
-    logger.trace("exiting wallet retry loop");
   }
+  logger.trace("exiting wallet retry loop");
+}
 
-  /**
-   * Insert the hard-coded defaults for exchanges, coins and
-   * auditors into the database, unless these defaults have
-   * already been applied.
-   */
-  async fillDefaults(): Promise<void> {
-    await this.db
-      .mktx((x) => ({ config: x.config, auditorTrustStore: x.auditorTrust }))
-      .runReadWrite(async (tx) => {
-        let applied = false;
-        await tx.config.iter().forEach((x) => {
-          if (x.key == "currencyDefaultsApplied" && x.value == true) {
-            applied = true;
-          }
-        });
-        if (!applied) {
-          for (const c of builtinAuditors) {
-            await tx.auditorTrustStore.put(c);
-          }
+/**
+ * Insert the hard-coded defaults for exchanges, coins and
+ * auditors into the database, unless these defaults have
+ * already been applied.
+ */
+async function fillDefaults(ws: InternalWalletState): Promise<void> {
+  await ws.db
+    .mktx((x) => ({ config: x.config, auditorTrustStore: x.auditorTrust }))
+    .runReadWrite(async (tx) => {
+      let applied = false;
+      await tx.config.iter().forEach((x) => {
+        if (x.key == "currencyDefaultsApplied" && x.value == true) {
+          applied = true;
         }
       });
-  }
+      if (!applied) {
+        for (const c of builtinAuditors) {
+          await tx.auditorTrustStore.put(c);
+        }
+      }
+    });
+}
 
-  /**
-   * Check if a payment for the given taler://pay/ URI is possible.
-   *
-   * If the payment is possible, the signature are already generated but not
-   * yet send to the merchant.
-   */
-  async preparePayForUri(talerPayUri: string): Promise<PreparePayResult> {
-    return preparePayForUri(this.ws, talerPayUri);
+/**
+ * Create a reserve, but do not flag it as confirmed yet.
+ *
+ * Adds the corresponding exchange as a trusted exchange if it is neither
+ * audited nor trusted already.
+ */
+async function acceptManualWithdrawal(
+  ws: InternalWalletState,
+  exchangeBaseUrl: string,
+  amount: AmountJson,
+): Promise<AcceptManualWithdrawalResult> {
+  try {
+    const resp = await createReserve(ws, {
+      amount,
+      exchange: exchangeBaseUrl,
+    });
+    const exchangePaytoUris = await ws.db
+      .mktx((x) => ({
+        exchanges: x.exchanges,
+        exchangeDetails: x.exchangeDetails,
+        reserves: x.reserves,
+      }))
+      .runReadWrite((tx) => getFundingPaytoUris(tx, resp.reservePub));
+    return {
+      reservePub: resp.reservePub,
+      exchangePaytoUris,
+    };
+  } finally {
+    ws.latch.trigger();
   }
+}
 
-  /**
-   * Add a contract to the wallet and sign coins, and send them.
-   */
-  async confirmPay(
-    proposalId: string,
-    sessionIdOverride: string | undefined,
-  ): Promise<ConfirmPayResult> {
-    try {
-      return await confirmPay(this.ws, proposalId, sessionIdOverride);
-    } finally {
-      this.latch.trigger();
-    }
+async function getExchangeTos(
+  ws: InternalWalletState,
+  exchangeBaseUrl: string,
+): Promise<GetExchangeTosResult> {
+  const { exchange, exchangeDetails } = await updateExchangeFromUrl(
+    ws,
+    exchangeBaseUrl,
+  );
+  const tos = exchangeDetails.termsOfServiceText;
+  const currentEtag = exchangeDetails.termsOfServiceLastEtag;
+  if (!tos || !currentEtag) {
+    throw Error("exchange is in invalid state");
   }
+  return {
+    acceptedEtag: exchangeDetails.termsOfServiceAcceptedEtag,
+    currentEtag,
+    tos,
+  };
+}
 
-  /**
-   * First fetch information required to withdraw from the reserve,
-   * then deplete the reserve, withdrawing coins until it is empty.
-   *
-   * The returned promise resolves once the reserve is set to the
-   * state DORMANT.
-   */
-  async processReserve(reservePub: string): Promise<void> {
-    try {
-      return await processReserve(this.ws, reservePub);
-    } finally {
-      this.latch.trigger();
-    }
+async function getExchanges(
+  ws: InternalWalletState,
+): Promise<ExchangesListRespose> {
+  const exchanges: ExchangeListItem[] = [];
+  await ws.db
+    .mktx((x) => ({
+      exchanges: x.exchanges,
+      exchangeDetails: x.exchangeDetails,
+    }))
+    .runReadOnly(async (tx) => {
+      const exchangeRecords = await tx.exchanges.iter().toArray();
+      for (const r of exchangeRecords) {
+        const dp = r.detailsPointer;
+        if (!dp) {
+          continue;
+        }
+        const { currency, masterPublicKey } = dp;
+        const exchangeDetails = await getExchangeDetails(tx, r.baseUrl);
+        if (!exchangeDetails) {
+          continue;
+        }
+        exchanges.push({
+          exchangeBaseUrl: r.baseUrl,
+          currency,
+          paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
+        });
+      }
+    });
+  return { exchanges };
+}
+
+async function acceptWithdrawal(
+  ws: InternalWalletState,
+  talerWithdrawUri: string,
+  selectedExchange: string,
+): Promise<AcceptWithdrawalResponse> {
+  try {
+    return createTalerWithdrawReserve(ws, talerWithdrawUri, selectedExchange);
+  } finally {
+    ws.latch.trigger();
   }
+}
 
-  /**
-   * Create a reserve, but do not flag it as confirmed yet.
-   *
-   * Adds the corresponding exchange as a trusted exchange if it is neither
-   * audited nor trusted already.
-   */
-  async acceptManualWithdrawal(
-    exchangeBaseUrl: string,
-    amount: AmountJson,
-  ): Promise<AcceptManualWithdrawalResult> {
-    try {
-      const resp = await createReserve(this.ws, {
-        amount,
-        exchange: exchangeBaseUrl,
-      });
-      const exchangePaytoUris = await this.db
-        .mktx((x) => ({
-          exchanges: x.exchanges,
-          exchangeDetails: x.exchangeDetails,
-          reserves: x.reserves,
-        }))
-        .runReadWrite((tx) => getFundingPaytoUris(tx, resp.reservePub));
-      return {
-        reservePub: resp.reservePub,
-        exchangePaytoUris,
-      };
-    } finally {
-      this.latch.trigger();
+/**
+ * Inform the wallet that the status of a reserve has changed (e.g. due to a
+ * confirmation from the bank.).
+ */
+export async function handleNotifyReserve(
+  ws: InternalWalletState,
+): Promise<void> {
+  const reserves = await ws.db
+    .mktx((x) => ({
+      reserves: x.reserves,
+    }))
+    .runReadOnly(async (tx) => {
+      return tx.reserves.iter().toArray();
+    });
+  for (const r of reserves) {
+    if (r.reserveStatus === ReserveRecordStatus.WAIT_CONFIRM_BANK) {
+      try {
+        processReserve(ws, r.reservePub);
+      } catch (e) {
+        console.error(e);
+      }
     }
   }
+}
 
-  /**
-   * Check if and how an exchange is trusted and/or audited.
-   */
-  async getExchangeTrust(
-    exchangeInfo: ExchangeRecord,
-  ): Promise<{ isTrusted: boolean; isAudited: boolean }> {
-    return getExchangeTrust(this.ws, exchangeInfo);
-  }
+async function setCoinSuspended(
+  ws: InternalWalletState,
+  coinPub: string,
+  suspended: boolean,
+): Promise<void> {
+  await ws.db
+    .mktx((x) => ({
+      coins: x.coins,
+    }))
+    .runReadWrite(async (tx) => {
+      const c = await tx.coins.get(coinPub);
+      if (!c) {
+        logger.warn(`coin ${coinPub} not found, won't suspend`);
+        return;
+      }
+      c.suspended = suspended;
+      await tx.coins.put(c);
+    });
+}
 
-  async getWithdrawalDetailsForUri(
-    talerWithdrawUri: string,
-  ): Promise<WithdrawUriInfoResponse> {
-    return getWithdrawalDetailsForUri(this.ws, talerWithdrawUri);
-  }
+/**
+ * Dump the public information of coins we have in an easy-to-process format.
+ */
+async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> {
+  const coinsJson: CoinDumpJson = { coins: [] };
+  await ws.db
+    .mktx((x) => ({
+      coins: x.coins,
+      denominations: x.denominations,
+      withdrawalGroups: x.withdrawalGroups,
+    }))
+    .runReadOnly(async (tx) => {
+      const coins = await tx.coins.iter().toArray();
+      for (const c of coins) {
+        const denom = await tx.denominations.get([
+          c.exchangeBaseUrl,
+          c.denomPubHash,
+        ]);
+        if (!denom) {
+          console.error("no denom session found for coin");
+          continue;
+        }
+        const cs = c.coinSource;
+        let refreshParentCoinPub: string | undefined;
+        if (cs.type == CoinSourceType.Refresh) {
+          refreshParentCoinPub = cs.oldCoinPub;
+        }
+        let withdrawalReservePub: string | undefined;
+        if (cs.type == CoinSourceType.Withdraw) {
+          const ws = await tx.withdrawalGroups.get(cs.withdrawalGroupId);
+          if (!ws) {
+            console.error("no withdrawal session found for coin");
+            continue;
+          }
+          withdrawalReservePub = ws.reservePub;
+        }
+        coinsJson.coins.push({
+          coin_pub: c.coinPub,
+          denom_pub: c.denomPub,
+          denom_pub_hash: c.denomPubHash,
+          denom_value: Amounts.stringify(denom.value),
+          exchange_base_url: c.exchangeBaseUrl,
+          refresh_parent_coin_pub: refreshParentCoinPub,
+          remaining_value: Amounts.stringify(c.currentAmount),
+          withdrawal_reserve_pub: withdrawalReservePub,
+          coin_suspended: c.suspended,
+        });
+      }
+    });
+  return coinsJson;
+}
 
-  async deleteTransaction(req: DeleteTransactionRequest): Promise<void> {
-    return deleteTransaction(this.ws, req.transactionId);
-  }
+export enum WalletApiOperation {
+  InitWallet = "initWallet",
+  WithdrawTestkudos = "withdrawTestkudos",
+  WithdrawTestBalance = "withdrawTestBalance",
+  PreparePayForUri = "preparePayForUri",
+  RunIntegrationTest = "runIntegrationTest",
+  TestPay = "testPay",
+  AddExchange = "addExchange",
+  GetTransactions = "getTransactions",
+  ListExchanges = "listExchanges",
+  GetWithdrawalDetailsForUri = "getWithdrawalDetailsForUri",
+  GetWithdrawalDetailsForAmount = "getWithdrawalDetailsForAmount",
+  AcceptManualWithdrawal = "acceptManualWithdrawal",
+  GetBalances = "getBalances",
+  GetPendingOperations = "getPendingOperations",
+  SetExchangeTosAccepted = "setExchangeTosAccepted",
+  ApplyRefund = "applyRefund",
+  AcceptBankIntegratedWithdrawal = "acceptBankIntegratedWithdrawal",
+  GetExchangeTos = "getExchangeTos",
+  RetryPendingNow = "retryPendingNow",
+  PreparePay = "preparePay",
+  ConfirmPay = "confirmPay",
+  DumpCoins = "dumpCoins",
+  SetCoinSuspended = "setCoinSuspended",
+  ForceRefresh = "forceRefresh",
+  PrepareTip = "prepareTip",
+  AcceptTip = "acceptTip",
+  ExportBackup = "exportBackup",
+  AddBackupProvider = "addBackupProvider",
+  RunBackupCycle = "runBackupCycle",
+  ExportBackupRecovery = "exportBackupRecovery",
+  ImportBackupRecovery = "importBackupRecovery",
+  GetBackupInfo = "getBackupInfo",
+  TrackDepositGroup = "trackDepositGroup",
+  DeleteTransaction = "deleteTransaction",
+  RetryTransaction = "retryTransaction",
+  GetCoins = "getCoins",
+  ListCurrencies = "listCurrencies",
+  CreateDepositGroup = "createDepositGroup",
+  SetWalletDeviceId = "setWalletDeviceId",
+  ExportBackupPlain = "exportBackupPlain",
+}
 
-  async setDeviceId(newDeviceId: string): Promise<void> {
-    return setWalletDeviceId(this.ws, newDeviceId);
-  }
+export type WalletOperations = {
+  [WalletApiOperation.PreparePayForUri]: {
+    request: PreparePayRequest;
+    response: PreparePayResult;
+  };
+  [WalletApiOperation.WithdrawTestkudos]: {
+    request: {};
+    response: {};
+  };
+  [WalletApiOperation.PreparePay]: {
+    request: PreparePayRequest;
+    response: PreparePayResult;
+  };
+  [WalletApiOperation.ConfirmPay]: {
+    request: ConfirmPayRequest;
+    response: ConfirmPayResult;
+  };
+  [WalletApiOperation.GetBalances]: {
+    request: {};
+    response: BalancesResponse;
+  };
+  [WalletApiOperation.GetTransactions]: {
+    request: TransactionsRequest;
+    response: TransactionsResponse;
+  };
+  [WalletApiOperation.GetPendingOperations]: {
+    request: {};
+    response: PendingOperationsResponse;
+  };
+  [WalletApiOperation.DumpCoins]: {
+    request: {};
+    response: CoinDumpJson;
+  };
+  [WalletApiOperation.SetCoinSuspended]: {
+    request: SetCoinSuspendedRequest;
+    response: {};
+  };
+  [WalletApiOperation.ForceRefresh]: {
+    request: ForceRefreshRequest;
+    response: {};
+  };
+  [WalletApiOperation.DeleteTransaction]: {
+    request: DeleteTransactionRequest;
+    response: {};
+  };
+  [WalletApiOperation.RetryTransaction]: {
+    request: RetryTransactionRequest;
+    response: {};
+  };
+  [WalletApiOperation.PrepareTip]: {
+    request: PrepareTipRequest;
+    response: PrepareTipResult;
+  };
+  [WalletApiOperation.AcceptTip]: {
+    request: AcceptTipRequest;
+    response: {};
+  };
+  [WalletApiOperation.ApplyRefund]: {
+    request: ApplyRefundRequest;
+    response: ApplyRefundResponse;
+  };
+  [WalletApiOperation.ListCurrencies]: {
+    request: {};
+    response: WalletCurrencyInfo;
+  };
+  [WalletApiOperation.GetWithdrawalDetailsForAmount]: {
+    request: GetWithdrawalDetailsForAmountRequest;
+    response: ManualWithdrawalDetails;
+  };
+  [WalletApiOperation.GetWithdrawalDetailsForUri]: {
+    request: GetWithdrawalDetailsForUriRequest;
+    response: WithdrawUriInfoResponse;
+  };
+  [WalletApiOperation.AcceptBankIntegratedWithdrawal]: {
+    request: AcceptBankIntegratedWithdrawalRequest;
+    response: AcceptWithdrawalResponse;
+  };
+  [WalletApiOperation.AcceptManualWithdrawal]: {
+    request: AcceptManualWithdrawalRequest;
+    response: AcceptManualWithdrawalResult;
+  };
+  [WalletApiOperation.ListExchanges]: {
+    request: {};
+    response: ExchangesListRespose;
+  };
+  [WalletApiOperation.AddExchange]: {
+    request: AddExchangeRequest;
+    response: {};
+  };
+  [WalletApiOperation.SetExchangeTosAccepted]: {
+    request: AcceptExchangeTosRequest;
+    response: {};
+  };
+  [WalletApiOperation.GetExchangeTos]: {
+    request: GetExchangeTosRequest;
+    response: GetExchangeTosResult;
+  };
+  [WalletApiOperation.TrackDepositGroup]: {
+    request: TrackDepositGroupRequest;
+    response: TrackDepositGroupResponse;
+  };
+  [WalletApiOperation.CreateDepositGroup]: {
+    request: CreateDepositGroupRequest;
+    response: CreateDepositGroupResponse;
+  };
+  [WalletApiOperation.SetWalletDeviceId]: {
+    request: SetWalletDeviceIdRequest;
+    response: {};
+  };
+  [WalletApiOperation.ExportBackupPlain]: {
+    request: {};
+    response: WalletBackupContentV1;
+  };
+  [WalletApiOperation.ExportBackupRecovery]: {
+    request: {};
+    response: BackupRecovery;
+  };
+  [WalletApiOperation.ImportBackupRecovery]: {
+    request: RecoveryLoadRequest;
+    response: {};
+  };
+  [WalletApiOperation.RunBackupCycle]: {
+    request: {};
+    response: {};
+  };
+  [WalletApiOperation.AddBackupProvider]: {
+    request: AddBackupProviderRequest;
+    response: {};
+  };
+  [WalletApiOperation.GetBackupInfo]: {
+    request: {};
+    response: BackupInfo;
+  };
+};
+
+export type RequestType<
+  Op extends WalletApiOperation & keyof WalletOperations
+> = WalletOperations[Op] extends { request: infer T } ? T : never;
+
+export type ResponseType<
+  Op extends WalletApiOperation & keyof WalletOperations
+> = WalletOperations[Op] extends { response: infer T } ? T : never;
+
+export interface WalletCoreApiClient {
+  call<Op extends WalletApiOperation & keyof WalletOperations>(
+    operation: Op,
+    payload: RequestType<Op>,
+  ): Promise<ResponseType<Op>>;
+}
 
-  /**
-   * Update or add exchange DB entry by fetching the /keys and /wire 
information.
-   */
-  async updateExchangeFromUrl(
-    baseUrl: string,
-    force = false,
-  ): Promise<{
-    exchange: ExchangeRecord;
-    exchangeDetails: ExchangeDetailsRecord;
-  }> {
-    try {
-      return updateExchangeFromUrl(this.ws, baseUrl, force);
-    } finally {
-      this.latch.trigger();
-    }
-  }
+/**
+ * Get an API client from an internal wallet state object.
+ */
+export async function getClientFromWalletState(
+  ws: InternalWalletState,
+): Promise<WalletCoreApiClient> {
+  let id = 0;
+  const client: WalletCoreApiClient = {
+    async call(op, payload): Promise<any> {
+      const res = await handleCoreApiRequest(ws, op, `${id++}`, payload);
+      switch (res.type) {
+        case "error":
+          throw new OperationFailedError(res.error);
+        case "response":
+          return res.result;
+      }
+    },
+  };
+  return client;
+}
 
-  async getExchangeTos(exchangeBaseUrl: string): Promise<GetExchangeTosResult> 
{
-    const { exchange, exchangeDetails } = await this.updateExchangeFromUrl(
-      exchangeBaseUrl,
+/**
+ * Implementation of the "wallet-core" API.
+ */
+async function dispatchRequestInternal(
+  ws: InternalWalletState,
+  operation: string,
+  payload: unknown,
+): Promise<Record<string, any>> {
+  if (ws.initCalled && operation !== "initWallet") {
+    throw Error(
+      `wallet must be initialized before running operation ${operation}`,
     );
-    const tos = exchangeDetails.termsOfServiceText;
-    const currentEtag = exchangeDetails.termsOfServiceLastEtag;
-    if (!tos || !currentEtag) {
-      throw Error("exchange is in invalid state");
-    }
-    return {
-      acceptedEtag: exchangeDetails.termsOfServiceAcceptedEtag,
-      currentEtag,
-      tos,
-    };
   }
-
-  /**
-   * Get detailed balance information, sliced by exchange and by currency.
-   */
-  async getBalances(): Promise<BalancesResponse> {
-    return this.ws.memoGetBalance.memo(() => getBalances(this.ws));
-  }
-
-  async refresh(oldCoinPub: string): Promise<void> {
-    try {
-      const refreshGroupId = await this.db
+  switch (operation) {
+    case "initWallet": {
+      ws.initCalled = true;
+      return {};
+    }
+    case "withdrawTestkudos": {
+      await withdrawTestBalance(
+        ws,
+        "TESTKUDOS:10",
+        "https://bank.test.taler.net/";,
+        "https://exchange.test.taler.net/";,
+      );
+      return {};
+    }
+    case "withdrawTestBalance": {
+      const req = codecForWithdrawTestBalance().decode(payload);
+      await withdrawTestBalance(
+        ws,
+        req.amount,
+        req.bankBaseUrl,
+        req.exchangeBaseUrl,
+      );
+      return {};
+    }
+    case "runIntegrationTest": {
+      const req = codecForIntegrationTestArgs().decode(payload);
+      await runIntegrationTest(ws, req);
+      return {};
+    }
+    case "testPay": {
+      const req = codecForTestPayArgs().decode(payload);
+      await testPay(ws, req);
+      return {};
+    }
+    case "getTransactions": {
+      const req = codecForTransactionsRequest().decode(payload);
+      return await getTransactions(ws, req);
+    }
+    case "addExchange": {
+      const req = codecForAddExchangeRequest().decode(payload);
+      await updateExchangeFromUrl(ws, req.exchangeBaseUrl, req.forceUpdate);
+      return {};
+    }
+    case "listExchanges": {
+      return await getExchanges(ws);
+    }
+    case "getWithdrawalDetailsForUri": {
+      const req = codecForGetWithdrawalDetailsForUri().decode(payload);
+      return await getWithdrawalDetailsForUri(ws, req.talerWithdrawUri);
+    }
+    case "acceptManualWithdrawal": {
+      const req = codecForAcceptManualWithdrawalRequet().decode(payload);
+      const res = await acceptManualWithdrawal(
+        ws,
+        req.exchangeBaseUrl,
+        Amounts.parseOrThrow(req.amount),
+      );
+      return res;
+    }
+    case "getWithdrawalDetailsForAmount": {
+      const req = codecForGetWithdrawalDetailsForAmountRequest().decode(
+        payload,
+      );
+      return await getWithdrawalDetailsForAmount(
+        ws,
+        req.exchangeBaseUrl,
+        Amounts.parseOrThrow(req.amount),
+      );
+    }
+    case "getBalances": {
+      return await getBalances(ws);
+    }
+    case "getPendingOperations": {
+      return await getPendingOperations(ws);
+    }
+    case "setExchangeTosAccepted": {
+      const req = codecForAcceptExchangeTosRequest().decode(payload);
+      await acceptExchangeTermsOfService(ws, req.exchangeBaseUrl, req.etag);
+      return {};
+    }
+    case "applyRefund": {
+      const req = codecForApplyRefundRequest().decode(payload);
+      return await applyRefund(ws, req.talerRefundUri);
+    }
+    case "acceptBankIntegratedWithdrawal": {
+      const req = codecForAcceptBankIntegratedWithdrawalRequest().decode(
+        payload,
+      );
+      return await acceptWithdrawal(
+        ws,
+        req.talerWithdrawUri,
+        req.exchangeBaseUrl,
+      );
+    }
+    case "getExchangeTos": {
+      const req = codecForGetExchangeTosRequest().decode(payload);
+      return getExchangeTos(ws, req.exchangeBaseUrl);
+    }
+    case "retryPendingNow": {
+      await runPending(ws, true);
+      return {};
+    }
+    case "preparePay": {
+      const req = codecForPreparePayRequest().decode(payload);
+      return await preparePayForUri(ws, req.talerPayUri);
+    }
+    case "confirmPay": {
+      const req = codecForConfirmPayRequest().decode(payload);
+      return await confirmPay(ws, req.proposalId, req.sessionId);
+    }
+    case "abortFailedPayWithRefund": {
+      const req = codecForAbortPayWithRefundRequest().decode(payload);
+      await abortFailedPayWithRefund(ws, req.proposalId);
+      return {};
+    }
+    case "dumpCoins": {
+      return await dumpCoins(ws);
+    }
+    case "setCoinSuspended": {
+      const req = codecForSetCoinSuspendedRequest().decode(payload);
+      await setCoinSuspended(ws, req.coinPub, req.suspended);
+      return {};
+    }
+    case "forceRefresh": {
+      const req = codecForForceRefreshRequest().decode(payload);
+      const coinPubs = req.coinPubList.map((x) => ({ coinPub: x }));
+      const refreshGroupId = await ws.db
         .mktx((x) => ({
           refreshGroups: x.refreshGroups,
           denominations: x.denominations,
@@ -642,613 +1014,150 @@ export class Wallet {
         }))
         .runReadWrite(async (tx) => {
           return await createRefreshGroup(
-            this.ws,
+            ws,
             tx,
-            [{ coinPub: oldCoinPub }],
+            coinPubs,
             RefreshReason.Manual,
           );
         });
-      await processRefreshGroup(this.ws, refreshGroupId.refreshGroupId);
-    } catch (e) {
-      this.latch.trigger();
+      processRefreshGroup(ws, refreshGroupId.refreshGroupId, true).catch(
+        (x) => {
+          logger.error(x);
+        },
+      );
+      return {
+        refreshGroupId,
+      };
     }
-  }
-
-  async getPendingOperations(): Promise<PendingOperationsResponse> {
-    return this.ws.memoGetPending.memo(() => getPendingOperations(this.ws));
-  }
-
-  async acceptExchangeTermsOfService(
-    exchangeBaseUrl: string,
-    etag: string | undefined,
-  ): Promise<void> {
-    return acceptExchangeTermsOfService(this.ws, exchangeBaseUrl, etag);
-  }
-
-  async getExchanges(): Promise<ExchangesListRespose> {
-    const exchanges: ExchangeListItem[] = [];
-    await this.db
-      .mktx((x) => ({
-        exchanges: x.exchanges,
-        exchangeDetails: x.exchangeDetails,
-      }))
-      .runReadOnly(async (tx) => {
-        const exchangeRecords = await tx.exchanges.iter().toArray();
-        for (const r of exchangeRecords) {
-          const dp = r.detailsPointer;
-          if (!dp) {
-            continue;
-          }
-          const { currency, masterPublicKey } = dp;
-          const exchangeDetails = await getExchangeDetails(tx, r.baseUrl);
-          if (!exchangeDetails) {
-            continue;
-          }
-          exchanges.push({
-            exchangeBaseUrl: r.baseUrl,
-            currency,
-            paytoUris: exchangeDetails.wireInfo.accounts.map(
-              (x) => x.payto_uri,
-            ),
-          });
-        }
-      });
-    return { exchanges };
-  }
-
-  async getCurrencies(): Promise<WalletCurrencyInfo> {
-    return await this.ws.db
-      .mktx((x) => ({
-        auditorTrust: x.auditorTrust,
-        exchangeTrust: x.exchangeTrust,
-      }))
-      .runReadOnly(async (tx) => {
-        const trustedAuditors = await tx.auditorTrust.iter().toArray();
-        const trustedExchanges = await tx.exchangeTrust.iter().toArray();
-        return {
-          trustedAuditors: trustedAuditors.map((x) => ({
-            currency: x.currency,
-            auditorBaseUrl: x.auditorBaseUrl,
-            auditorPub: x.auditorPub,
-          })),
-          trustedExchanges: trustedExchanges.map((x) => ({
-            currency: x.currency,
-            exchangeBaseUrl: x.exchangeBaseUrl,
-            exchangeMasterPub: x.exchangeMasterPub,
-          })),
-        };
-      });
-  }
-
-  /**
-   * Stop ongoing processing.
-   */
-  stop(): void {
-    this.stopped = true;
-    this.timerGroup.stopCurrentAndFutureTimers();
-    this.ws.cryptoApi.stop();
-  }
-
-  /**
-   * Trigger paying coins back into the user's account.
-   */
-  async returnCoins(req: ReturnCoinsRequest): Promise<void> {
-    throw Error("not implemented");
-  }
-
-  /**
-   * Accept a refund, return the contract hash for the contract
-   * that was involved in the refund.
-   */
-  async applyRefund(talerRefundUri: string): Promise<ApplyRefundResponse> {
-    return applyRefund(this.ws, talerRefundUri);
-  }
-
-  async acceptTip(talerTipUri: string): Promise<void> {
-    try {
-      return acceptTip(this.ws, talerTipUri);
-    } catch (e) {
-      this.latch.trigger();
+    case "prepareTip": {
+      const req = codecForPrepareTipRequest().decode(payload);
+      return await prepareTip(ws, req.talerTipUri);
     }
-  }
-
-  async prepareTip(talerTipUri: string): Promise<PrepareTipResult> {
-    return prepareTip(this.ws, talerTipUri);
-  }
-
-  async abortFailedPayWithRefund(proposalId: string): Promise<void> {
-    return abortFailedPayWithRefund(this.ws, proposalId);
-  }
-
-  /**
-   * Inform the wallet that the status of a reserve has changed (e.g. due to a
-   * confirmation from the bank.).
-   */
-  public async handleNotifyReserve(): Promise<void> {
-    const reserves = await this.ws.db
-      .mktx((x) => ({
-        reserves: x.reserves,
-      }))
-      .runReadOnly(async (tx) => {
-        return tx.reserves.iter().toArray();
-      });
-    for (const r of reserves) {
-      if (r.reserveStatus === ReserveRecordStatus.WAIT_CONFIRM_BANK) {
-        try {
-          this.processReserve(r.reservePub);
-        } catch (e) {
-          console.error(e);
-        }
-      }
+    case "acceptTip": {
+      const req = codecForAcceptTipRequest().decode(payload);
+      await acceptTip(ws, req.walletTipId);
+      return {};
     }
-  }
-
-  /**
-   * Remove unreferenced / expired data from the wallet's database
-   * based on the current system time.
-   */
-  async collectGarbage(): Promise<void> {
-    // FIXME(#5845)
-    // We currently do not garbage-collect the wallet database.  This might 
change
-    // after the feature has been properly re-designed, and we have come up 
with a
-    // strategy to test it.
-  }
-
-  async acceptWithdrawal(
-    talerWithdrawUri: string,
-    selectedExchange: string,
-  ): Promise<AcceptWithdrawalResponse> {
-    try {
-      return createTalerWithdrawReserve(
-        this.ws,
-        talerWithdrawUri,
-        selectedExchange,
-      );
-    } finally {
-      this.latch.trigger();
+    case "exportBackupPlain": {
+      return exportBackup(ws);
     }
-  }
-
-  async refuseProposal(proposalId: string): Promise<void> {
-    return refuseProposal(this.ws, proposalId);
-  }
-
-  benchmarkCrypto(repetitions: number): Promise<BenchmarkResult> {
-    return this.ws.cryptoApi.benchmark(repetitions);
-  }
-
-  async setCoinSuspended(coinPub: string, suspended: boolean): Promise<void> {
-    await this.db
-      .mktx((x) => ({
-        coins: x.coins,
-      }))
-      .runReadWrite(async (tx) => {
-        const c = await tx.coins.get(coinPub);
-        if (!c) {
-          logger.warn(`coin ${coinPub} not found, won't suspend`);
-          return;
-        }
-        c.suspended = suspended;
-        await tx.coins.put(c);
-      });
-  }
-
-  /**
-   * Dump the public information of coins we have in an easy-to-process format.
-   */
-  async dumpCoins(): Promise<CoinDumpJson> {
-    const coinsJson: CoinDumpJson = { coins: [] };
-    await this.ws.db
-      .mktx((x) => ({
-        coins: x.coins,
-        denominations: x.denominations,
-        withdrawalGroups: x.withdrawalGroups,
-      }))
-      .runReadOnly(async (tx) => {
-        const coins = await tx.coins.iter().toArray();
-        for (const c of coins) {
-          const denom = await tx.denominations.get([
-            c.exchangeBaseUrl,
-            c.denomPubHash,
-          ]);
-          if (!denom) {
-            console.error("no denom session found for coin");
-            continue;
-          }
-          const cs = c.coinSource;
-          let refreshParentCoinPub: string | undefined;
-          if (cs.type == CoinSourceType.Refresh) {
-            refreshParentCoinPub = cs.oldCoinPub;
-          }
-          let withdrawalReservePub: string | undefined;
-          if (cs.type == CoinSourceType.Withdraw) {
-            const ws = await tx.withdrawalGroups.get(cs.withdrawalGroupId);
-            if (!ws) {
-              console.error("no withdrawal session found for coin");
-              continue;
-            }
-            withdrawalReservePub = ws.reservePub;
-          }
-          coinsJson.coins.push({
-            coin_pub: c.coinPub,
-            denom_pub: c.denomPub,
-            denom_pub_hash: c.denomPubHash,
-            denom_value: Amounts.stringify(denom.value),
-            exchange_base_url: c.exchangeBaseUrl,
-            refresh_parent_coin_pub: refreshParentCoinPub,
-            remaining_value: Amounts.stringify(c.currentAmount),
-            withdrawal_reserve_pub: withdrawalReservePub,
-            coin_suspended: c.suspended,
-          });
-        }
-      });
-    return coinsJson;
-  }
-
-  async getTransactions(
-    request: TransactionsRequest,
-  ): Promise<TransactionsResponse> {
-    return getTransactions(this.ws, request);
-  }
-
-  async withdrawTestBalance(req: WithdrawTestBalanceRequest): Promise<void> {
-    await withdrawTestBalance(
-      this.ws,
-      req.amount,
-      req.bankBaseUrl,
-      req.exchangeBaseUrl,
-    );
-  }
-
-  async updateReserve(reservePub: string): Promise<ReserveRecord | undefined> {
-    await forceQueryReserve(this.ws, reservePub);
-    return await this.ws.db
-      .mktx((x) => ({
-        reserves: x.reserves,
-      }))
-      .runReadOnly(async (tx) => {
-        return tx.reserves.get(reservePub);
-      });
-  }
-
-  async getCoins(): Promise<CoinRecord[]> {
-    return await this.db
-      .mktx((x) => ({
-        coins: x.coins,
-      }))
-      .runReadOnly(async (tx) => {
-        return tx.coins.iter().toArray();
-      });
-  }
-
-  async getReservesForExchange(
-    exchangeBaseUrl?: string,
-  ): Promise<ReserveRecord[]> {
-    return await this.db
-      .mktx((x) => ({
-        reserves: x.reserves,
-      }))
-      .runReadOnly(async (tx) => {
-        if (exchangeBaseUrl) {
-          return await tx.reserves
-            .iter()
-            .filter((r) => r.exchangeBaseUrl === exchangeBaseUrl);
-        } else {
-          return await tx.reserves.iter().toArray();
-        }
-      });
-  }
-
-  async getReserve(reservePub: string): Promise<ReserveRecord | undefined> {
-    return await this.db
-      .mktx((x) => ({
-        reserves: x.reserves,
-      }))
-      .runReadOnly(async (tx) => {
-        return tx.reserves.get(reservePub);
-      });
-  }
-
-  async runIntegrationtest(args: IntegrationTestArgs): Promise<void> {
-    return runIntegrationTest(this.ws.http, this, args);
-  }
-
-  async testPay(args: TestPayArgs) {
-    return testPay(this.ws.http, this, args);
-  }
-
-  async exportBackupPlain() {
-    return exportBackup(this.ws);
-  }
-
-  async importBackupPlain(backup: any) {
-    return importBackupPlain(this.ws, backup);
-  }
-
-  async exportBackupEncrypted() {
-    return exportBackupEncrypted(this.ws);
-  }
-
-  async importBackupEncrypted(backup: Uint8Array) {
-    return importBackupEncrypted(this.ws, backup);
-  }
-
-  async getBackupRecovery(): Promise<BackupRecovery> {
-    return getBackupRecovery(this.ws);
-  }
-
-  async loadBackupRecovery(req: RecoveryLoadRequest): Promise<void> {
-    return loadBackupRecovery(this.ws, req);
-  }
-
-  async addBackupProvider(req: AddBackupProviderRequest): Promise<void> {
-    return addBackupProvider(this.ws, req);
-  }
-
-  async createDepositGroup(
-    req: CreateDepositGroupRequest,
-  ): Promise<CreateDepositGroupResponse> {
-    return createDepositGroup(this.ws, req);
-  }
-
-  async runBackupCycle(): Promise<void> {
-    return runBackupCycle(this.ws);
-  }
-
-  async getBackupStatus(): Promise<BackupInfo> {
-    return getBackupInfo(this.ws);
-  }
-
-  async trackDepositGroup(
-    req: TrackDepositGroupRequest,
-  ): Promise<TrackDepositGroupResponse> {
-    return trackDepositGroup(this.ws, req);
-  }
-
-  /**
-   * Implementation of the "wallet-core" API.
-   */
-  private async dispatchRequestInternal(
-    operation: string,
-    payload: unknown,
-  ): Promise<Record<string, any>> {
-    switch (operation) {
-      case "withdrawTestkudos": {
-        await this.withdrawTestBalance({
-          amount: "TESTKUDOS:10",
-          bankBaseUrl: "https://bank.test.taler.net/";,
-          exchangeBaseUrl: "https://exchange.test.taler.net/";,
+    case "addBackupProvider": {
+      const req = codecForAddBackupProviderRequest().decode(payload);
+      await addBackupProvider(ws, req);
+      return {};
+    }
+    case "runBackupCycle": {
+      await runBackupCycle(ws);
+      return {};
+    }
+    case "exportBackupRecovery": {
+      const resp = await getBackupRecovery(ws);
+      return resp;
+    }
+    case "importBackupRecovery": {
+      const req = codecForAny().decode(payload);
+      await loadBackupRecovery(ws, req);
+      return {};
+    }
+    case "getBackupInfo": {
+      const resp = await getBackupInfo(ws);
+      return resp;
+    }
+    case "createDepositGroup": {
+      const req = codecForCreateDepositGroupRequest().decode(payload);
+      return await createDepositGroup(ws, req);
+    }
+    case "trackDepositGroup": {
+      const req = codecForTrackDepositGroupRequest().decode(payload);
+      return trackDepositGroup(ws, req);
+    }
+    case "deleteTransaction": {
+      const req = codecForDeleteTransactionRequest().decode(payload);
+      await deleteTransaction(ws, req.transactionId);
+      return {};
+    }
+    case "retryTransaction": {
+      const req = codecForRetryTransactionRequest().decode(payload);
+      await retryTransaction(ws, req.transactionId);
+      return {};
+    }
+    case "setWalletDeviceId": {
+      const req = codecForSetWalletDeviceIdRequest().decode(payload);
+      await setWalletDeviceId(ws, req.walletDeviceId);
+      return {};
+    }
+    case "listCurrencies": {
+      return await ws.db
+        .mktx((x) => ({
+          auditorTrust: x.auditorTrust,
+          exchangeTrust: x.exchangeTrust,
+        }))
+        .runReadOnly(async (tx) => {
+          const trustedAuditors = await tx.auditorTrust.iter().toArray();
+          const trustedExchanges = await tx.exchangeTrust.iter().toArray();
+          return {
+            trustedAuditors: trustedAuditors.map((x) => ({
+              currency: x.currency,
+              auditorBaseUrl: x.auditorBaseUrl,
+              auditorPub: x.auditorPub,
+            })),
+            trustedExchanges: trustedExchanges.map((x) => ({
+              currency: x.currency,
+              exchangeBaseUrl: x.exchangeBaseUrl,
+              exchangeMasterPub: x.exchangeMasterPub,
+            })),
+          };
         });
-        return {};
-      }
-      case "withdrawTestBalance": {
-        const req = codecForWithdrawTestBalance().decode(payload);
-        await this.withdrawTestBalance(req);
-        return {};
-      }
-      case "runIntegrationTest": {
-        const req = codecForIntegrationTestArgs().decode(payload);
-        await this.runIntegrationtest(req);
-        return {};
-      }
-      case "testPay": {
-        const req = codecForTestPayArgs().decode(payload);
-        await this.testPay(req);
-        return {};
-      }
-      case "getTransactions": {
-        const req = codecForTransactionsRequest().decode(payload);
-        return await this.getTransactions(req);
-      }
-      case "addExchange": {
-        const req = codecForAddExchangeRequest().decode(payload);
-        await this.updateExchangeFromUrl(req.exchangeBaseUrl);
-        return {};
-      }
-      case "forceUpdateExchange": {
-        const req = codecForForceExchangeUpdateRequest().decode(payload);
-        await this.updateExchangeFromUrl(req.exchangeBaseUrl, true);
-        return {};
-      }
-      case "listExchanges": {
-        return await this.getExchanges();
-      }
-      case "getWithdrawalDetailsForUri": {
-        const req = codecForGetWithdrawalDetailsForUri().decode(payload);
-        return await this.getWithdrawalDetailsForUri(req.talerWithdrawUri);
-      }
-      case "acceptManualWithdrawal": {
-        const req = codecForAcceptManualWithdrawalRequet().decode(payload);
-        const res = await this.acceptManualWithdrawal(
-          req.exchangeBaseUrl,
-          Amounts.parseOrThrow(req.amount),
-        );
-        return res;
-      }
-      case "getWithdrawalDetailsForAmount": {
-        const req = codecForGetWithdrawalDetailsForAmountRequest().decode(
-          payload,
-        );
-        return await this.getWithdrawalDetailsForAmount(
-          req.exchangeBaseUrl,
-          Amounts.parseOrThrow(req.amount),
-        );
-      }
-      case "getBalances": {
-        return await this.getBalances();
-      }
-      case "getPendingOperations": {
-        return await this.getPendingOperations();
-      }
-      case "setExchangeTosAccepted": {
-        const req = codecForAcceptExchangeTosRequest().decode(payload);
-        await this.acceptExchangeTermsOfService(req.exchangeBaseUrl, req.etag);
-        return {};
-      }
-      case "applyRefund": {
-        const req = codecForApplyRefundRequest().decode(payload);
-        return await this.applyRefund(req.talerRefundUri);
-      }
-      case "acceptBankIntegratedWithdrawal": {
-        const req = codecForAcceptBankIntegratedWithdrawalRequest().decode(
-          payload,
-        );
-        return await this.acceptWithdrawal(
-          req.talerWithdrawUri,
-          req.exchangeBaseUrl,
-        );
-      }
-      case "getExchangeTos": {
-        const req = codecForGetExchangeTosRequest().decode(payload);
-        return this.getExchangeTos(req.exchangeBaseUrl);
-      }
-      case "retryPendingNow": {
-        await this.runPending(true);
-        return {};
-      }
-      case "preparePay": {
-        const req = codecForPreparePayRequest().decode(payload);
-        return await this.preparePayForUri(req.talerPayUri);
-      }
-      case "confirmPay": {
-        const req = codecForConfirmPayRequest().decode(payload);
-        return await this.confirmPay(req.proposalId, req.sessionId);
-      }
-      case "abortFailedPayWithRefund": {
-        const req = codecForAbortPayWithRefundRequest().decode(payload);
-        await this.abortFailedPayWithRefund(req.proposalId);
-        return {};
-      }
-      case "dumpCoins": {
-        return await this.dumpCoins();
-      }
-      case "setCoinSuspended": {
-        const req = codecForSetCoinSuspendedRequest().decode(payload);
-        await this.setCoinSuspended(req.coinPub, req.suspended);
-        return {};
-      }
-      case "forceRefresh": {
-        const req = codecForForceRefreshRequest().decode(payload);
-        const coinPubs = req.coinPubList.map((x) => ({ coinPub: x }));
-        const refreshGroupId = await this.db
-          .mktx((x) => ({
-            refreshGroups: x.refreshGroups,
-            denominations: x.denominations,
-            coins: x.coins,
-          }))
-          .runReadWrite(async (tx) => {
-            return await createRefreshGroup(
-              this.ws,
-              tx,
-              coinPubs,
-              RefreshReason.Manual,
-            );
-          });
-        return {
-          refreshGroupId,
-        };
-      }
-      case "prepareTip": {
-        const req = codecForPrepareTipRequest().decode(payload);
-        return await this.prepareTip(req.talerTipUri);
-      }
-      case "acceptTip": {
-        const req = codecForAcceptTipRequest().decode(payload);
-        await this.acceptTip(req.walletTipId);
-        return {};
-      }
-      case "exportBackup": {
-        return exportBackup(this.ws);
-      }
-      case "addBackupProvider": {
-        const req = codecForAddBackupProviderRequest().decode(payload);
-        await addBackupProvider(this.ws, req);
-        return {};
-      }
-      case "runBackupCycle": {
-        await runBackupCycle(this.ws);
-        return {};
-      }
-      case "exportBackupRecovery": {
-        const resp = await getBackupRecovery(this.ws);
-        return resp;
-      }
-      case "importBackupRecovery": {
-        const req = codecForAny().decode(payload);
-        await loadBackupRecovery(this.ws, req);
-        return {};
-      }
-      case "getBackupInfo": {
-        const resp = await getBackupInfo(this.ws);
-        return resp;
-      }
-      case "createDepositGroup": {
-        const req = codecForCreateDepositGroupRequest().decode(payload);
-        return await createDepositGroup(this.ws, req);
-      }
-      case "trackDepositGroup": {
-        const req = codecForTrackDepositGroupRequest().decode(payload);
-        return trackDepositGroup(this.ws, req);
-      }
-      case "deleteTransaction": {
-        const req = codecForDeleteTransactionRequest().decode(payload);
-        await deleteTransaction(this.ws, req.transactionId);
-        return {};
-      }
-      case "retryTransaction": {
-        const req = codecForRetryTransactionRequest().decode(payload);
-        await retryTransaction(this.ws, req.transactionId);
-        return {};
-      }
     }
-    throw OperationFailedError.fromCode(
-      TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
-      "unknown operation",
-      {
-        operation,
-      },
-    );
   }
+  throw OperationFailedError.fromCode(
+    TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
+    "unknown operation",
+    {
+      operation,
+    },
+  );
+}
 
-  /**
-   * Handle a request to the wallet-core API.
-   */
-  async handleCoreApiRequest(
-    operation: string,
-    id: string,
-    payload: unknown,
-  ): Promise<CoreApiResponse> {
-    try {
-      const result = await this.dispatchRequestInternal(operation, payload);
+/**
+ * Handle a request to the wallet-core API.
+ */
+export async function handleCoreApiRequest(
+  ws: InternalWalletState,
+  operation: string,
+  id: string,
+  payload: unknown,
+): Promise<CoreApiResponse> {
+  try {
+    const result = await dispatchRequestInternal(ws, operation, payload);
+    return {
+      type: "response",
+      operation,
+      id,
+      result,
+    };
+  } catch (e) {
+    if (
+      e instanceof OperationFailedError ||
+      e instanceof OperationFailedAndReportedError
+    ) {
       return {
-        type: "response",
+        type: "error",
         operation,
         id,
-        result,
+        error: e.operationError,
+      };
+    } else {
+      return {
+        type: "error",
+        operation,
+        id,
+        error: makeErrorDetails(
+          TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+          `unexpected exception: ${e}`,
+          {},
+        ),
       };
-    } catch (e) {
-      if (
-        e instanceof OperationFailedError ||
-        e instanceof OperationFailedAndReportedError
-      ) {
-        return {
-          type: "error",
-          operation,
-          id,
-          error: e.operationError,
-        };
-      } else {
-        return {
-          type: "error",
-          operation,
-          id,
-          error: makeErrorDetails(
-            TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
-            `unexpected exception: ${e}`,
-            {},
-          ),
-        };
-      }
     }
   }
 }
diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts 
b/packages/taler-wallet-webextension/src/wxBackend.ts
index 51a44ee6..d3f99d9c 100644
--- a/packages/taler-wallet-webextension/src/wxBackend.ts
+++ b/packages/taler-wallet-webextension/src/wxBackend.ts
@@ -26,7 +26,6 @@
 import { isFirefox, getPermissionsApi } from "./compat";
 import { extendedPermissions } from "./permissions";
 import {
-  Wallet,
   OpenedPromise,
   openPromise,
   openTalerDatabase,
@@ -34,6 +33,9 @@ import {
   deleteTalerDatabase,
   DbAccess,
   WalletStoresV1,
+  handleCoreApiRequest,
+  runRetryLoop,
+  handleNotifyReserve,
 } from "@gnu-taler/taler-wallet-core";
 import {
   classifyTalerUri,
@@ -45,12 +47,13 @@ import {
 } from "@gnu-taler/taler-util";
 import { BrowserHttpLib } from "./browserHttpLib";
 import { BrowserCryptoWorkerFactory } from "./browserCryptoWorkerFactory";
+import { InternalWalletState } from 
"@gnu-taler/taler-wallet-core/src/operations/state";
 
 /**
  * Currently active wallet instance.  Might be unloaded and
  * re-instantiated when the database is reset.
  */
-let currentWallet: Wallet | undefined;
+let currentWallet: InternalWalletState | undefined;
 
 let currentDatabase: DbAccess<typeof WalletStoresV1> | undefined;
 
@@ -167,7 +170,7 @@ async function dispatch(
         };
         break;
       }
-      r = await w.handleCoreApiRequest(req.operation, req.id, req.payload);
+      r = await handleCoreApiRequest(w, req.operation, req.id, req.payload);
       break;
     }
   }
@@ -253,7 +256,7 @@ async function reinitWallet(): Promise<void> {
   }
   const http = new BrowserHttpLib();
   console.log("setting wallet");
-  const wallet = new Wallet(
+  const wallet = new InternalWalletState(
     currentDatabase,
     http,
     new BrowserCryptoWorkerFactory(),
@@ -267,7 +270,7 @@ async function reinitWallet(): Promise<void> {
       }
     }
   });
-  wallet.runRetryLoop().catch((e) => {
+  runRetryLoop(wallet).catch((e) => {
     console.log("error during wallet retry loop", e);
   });
   // Useful for debugging in the background page.
@@ -357,7 +360,7 @@ function headerListener(
               if (!w) {
                 return;
               }
-              w.handleNotifyReserve();
+              handleNotifyReserve(w);
             });
             break;
           default:
@@ -448,3 +451,4 @@ export async function wxMain(): Promise<void> {
     setupHeaderListener();
   });
 }
+

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