gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: the giant refactoring: split


From: gnunet
Subject: [taler-wallet-core] branch master updated: the giant refactoring: split wallet into multiple parts
Date: Mon, 02 Dec 2019 00:42:49 +0100

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 e1369ff7 the giant refactoring: split wallet into multiple parts
e1369ff7 is described below

commit e1369ff7e8fc02116b9c4261036f0e42e3423cf4
Author: Florian Dold <address@hidden>
AuthorDate: Mon Dec 2 00:42:40 2019 +0100

    the giant refactoring: split wallet into multiple parts
---
 package.json                          |    6 +-
 src/android/index.ts                  |    6 +-
 src/crypto/cryptoApi.ts               |   12 +-
 src/crypto/cryptoImplementation.ts    |   25 +-
 src/db.ts                             |   10 +-
 src/dbTypes.ts                        |  116 +-
 src/headless/bank.ts                  |   11 +-
 src/headless/helpers.ts               |   10 +-
 src/headless/merchant.ts              |   13 +-
 src/headless/taler-wallet-cli.ts      |    7 +-
 src/talerTypes.ts                     |    6 +-
 src/types-test.ts                     |   78 +-
 src/{ => util}/amounts.ts             |    0
 src/util/assertUnreachable.ts         |   19 +
 src/util/asyncMemo.ts                 |   52 +
 src/{ => util}/checkable.ts           |    0
 src/{ => util}/helpers-test.ts        |    0
 src/{ => util}/helpers.ts             |   15 +-
 src/{ => util}/http.ts                |    0
 src/{ => util}/libtoolVersion-test.ts |    0
 src/{ => util}/libtoolVersion.ts      |    0
 src/{ => util}/logging.ts             |    2 +-
 src/util/payto-test.ts                |   31 +
 src/util/payto.ts                     |   54 +
 src/{ => util}/promiseUtils.ts        |    0
 src/{ => util}/query.ts               |    0
 src/{ => util}/taleruri-test.ts       |  100 +-
 src/{ => util}/taleruri.ts            |  118 +-
 src/{ => util}/timer.ts               |    0
 src/{ => util}/wire.ts                |    2 +-
 src/wallet-impl/balance.ts            |  144 ++
 src/wallet-impl/exchanges.ts          |  401 ++++
 src/wallet-impl/history.ts            |  172 ++
 src/wallet-impl/pay.ts                |  822 +++++++
 src/wallet-impl/payback.ts            |   88 +
 src/wallet-impl/pending.ts            |  208 ++
 src/wallet-impl/refresh.ts            |  416 ++++
 src/wallet-impl/refund.ts             |  245 +++
 src/wallet-impl/reserves.ts           |  567 +++++
 src/wallet-impl/return.ts             |  274 +++
 src/wallet-impl/state.ts              |   32 +
 src/wallet-impl/tip.ts                |  246 +++
 src/wallet-impl/withdraw.ts           |  577 +++++
 src/wallet-test.ts                    |   44 +-
 src/wallet.ts                         | 3815 ++-------------------------------
 src/walletTypes.ts                    |   36 +-
 src/webex/messages.ts                 |   14 +-
 src/webex/notify.ts                   |   28 +-
 src/webex/pages/add-auditor.tsx       |   27 +-
 src/webex/pages/pay.tsx               |   13 +-
 src/webex/pages/popup.tsx             |   20 +-
 src/webex/pages/refund.tsx            |    6 +-
 src/webex/pages/return-coins.tsx      |    6 +-
 src/webex/pages/tip.tsx               |   10 +-
 src/webex/pages/withdraw.tsx          |    6 +-
 src/webex/renderHtml.tsx              |    4 +-
 src/webex/wxApi.ts                    |   25 +-
 src/webex/wxBackend.ts                |   60 +-
 tsconfig.json                         |   47 +-
 yarn.lock                             |   10 -
 60 files changed, 4893 insertions(+), 4163 deletions(-)

diff --git a/package.json b/package.json
index fcff69c9..c87efd4f 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
   "devDependencies": {
     "@types/react": "^16.4.0",
     "@types/react-dom": "^16.0.0",
+    "@types/chrome": "^0.0.91",
     "ava": "^2.4.0",
     "awesome-typescript-loader": "^5.2.1",
     "glob": "^7.1.1",
@@ -60,13 +61,10 @@
     "webpack-merge": "^4.2.2"
   },
   "dependencies": {
-    "@types/chrome": "^0.0.91",
-    "@types/urijs": "^1.19.3",
     "axios": "^0.19.0",
     "big-integer": "^1.6.48",
     "idb-bridge": "^0.0.15",
     "qrcode-generator": "^1.4.3",
-    "source-map-support": "^0.5.12",
-    "urijs": "^1.18.10"
+    "source-map-support": "^0.5.12"
   }
 }
diff --git a/src/android/index.ts b/src/android/index.ts
index ab0d3f7b..6a29f794 100644
--- a/src/android/index.ts
+++ b/src/android/index.ts
@@ -24,10 +24,10 @@ import {
   DefaultNodeWalletArgs,
   NodeHttpLib,
 } from "../headless/helpers";
-import { openPromise, OpenedPromise } from "../promiseUtils";
+import { openPromise, OpenedPromise } from "../util/promiseUtils";
 import fs = require("fs");
 import axios from "axios";
-import { HttpRequestLibrary, HttpResponse } from "../http";
+import { HttpRequestLibrary, HttpResponse } from "../util/http";
 import querystring = require("querystring");
 
 // @ts-ignore: special built-in module
@@ -66,7 +66,7 @@ export class AndroidHttpLib implements HttpRequestLibrary {
     }
   }
 
-  postJson(url: string, body: any): Promise<import("../http").HttpResponse> {
+  postJson(url: string, body: any): 
Promise<import("../util/http").HttpResponse> {
     if (this.useNfcTunnel) {
       const myId = this.requestId++;
       const p = openPromise<HttpResponse>();
diff --git a/src/crypto/cryptoApi.ts b/src/crypto/cryptoApi.ts
index b5eae9be..5ef78771 100644
--- a/src/crypto/cryptoApi.ts
+++ b/src/crypto/cryptoApi.ts
@@ -22,12 +22,11 @@
 /**
  * Imports.
  */
-import { AmountJson } from "../amounts";
+import { AmountJson } from "../util/amounts";
 
 import {
   CoinRecord,
   DenominationRecord,
-  PlanchetRecord,
   RefreshSessionRecord,
   ReserveRecord,
   TipPlanchet,
@@ -38,9 +37,9 @@ import { CryptoWorker } from "./cryptoWorker";
 
 import { ContractTerms, PaybackRequest } from "../talerTypes";
 
-import { BenchmarkResult, CoinWithDenom, PayCoinInfo, PlanchetCreationResult } 
from "../walletTypes";
+import { BenchmarkResult, CoinWithDenom, PayCoinInfo, PlanchetCreationResult, 
PlanchetCreationRequest } from "../walletTypes";
 
-import * as timer from "../timer";
+import * as timer from "../util/timer";
 
 /**
  * State of a crypto worker.
@@ -336,10 +335,9 @@ export class CryptoApi {
   }
 
   createPlanchet(
-    denom: DenominationRecord,
-    reserve: ReserveRecord,
+    req: PlanchetCreationRequest
   ): Promise<PlanchetCreationResult> {
-    return this.doRpc<PlanchetCreationResult>("createPlanchet", 1, denom, 
reserve);
+    return this.doRpc<PlanchetCreationResult>("createPlanchet", 1, req);
   }
 
   createTipPlanchet(denom: DenominationRecord): Promise<TipPlanchet> {
diff --git a/src/crypto/cryptoImplementation.ts 
b/src/crypto/cryptoImplementation.ts
index 7cddf903..faebbaa4 100644
--- a/src/crypto/cryptoImplementation.ts
+++ b/src/crypto/cryptoImplementation.ts
@@ -42,11 +42,12 @@ import {
   PayCoinInfo,
   Timestamp,
   PlanchetCreationResult,
+  PlanchetCreationRequest,
 } from "../walletTypes";
-import { canonicalJson, getTalerStampSec } from "../helpers";
-import { AmountJson } from "../amounts";
-import * as Amounts from "../amounts";
-import * as timer from "../timer";
+import { canonicalJson, getTalerStampSec } from "../util/helpers";
+import { AmountJson } from "../util/amounts";
+import * as Amounts from "../util/amounts";
+import * as timer from "../util/timer";
 import {
   getRandomBytes,
   encodeCrock,
@@ -155,24 +156,23 @@ export class CryptoImplementation {
    * reserve.
    */
   createPlanchet(
-    denom: DenominationRecord,
-    reserve: ReserveRecord,
+    req: PlanchetCreationRequest,
   ): PlanchetCreationResult {
-    const reservePub = decodeCrock(reserve.reservePub);
-    const reservePriv = decodeCrock(reserve.reservePriv);
-    const denomPub = decodeCrock(denom.denomPub);
+    const reservePub = decodeCrock(req.reservePub);
+    const reservePriv = decodeCrock(req.reservePriv);
+    const denomPub = decodeCrock(req.denomPub);
     const coinKeyPair = createEddsaKeyPair();
     const blindingFactor = createBlindingKeySecret();
     const coinPubHash = hash(coinKeyPair.eddsaPub);
     const ev = rsaBlind(coinPubHash, blindingFactor, denomPub);
-    const amountWithFee = Amounts.add(denom.value, denom.feeWithdraw).amount;
+    const amountWithFee = Amounts.add(req.value, req.feeWithdraw).amount;
     const denomPubHash = hash(denomPub);
     const evHash = hash(ev);
 
     const withdrawRequest = buildSigPS(SignaturePurpose.RESERVE_WITHDRAW)
       .put(reservePub)
       .put(amountToBuffer(amountWithFee))
-      .put(amountToBuffer(denom.feeWithdraw))
+      .put(amountToBuffer(req.feeWithdraw))
       .put(denomPubHash)
       .put(evHash)
       .build();
@@ -184,10 +184,9 @@ export class CryptoImplementation {
       coinEv: encodeCrock(ev),
       coinPriv: encodeCrock(coinKeyPair.eddsaPriv),
       coinPub: encodeCrock(coinKeyPair.eddsaPub),
-      coinValue: denom.value,
+      coinValue: req.value,
       denomPub: encodeCrock(denomPub),
       denomPubHash: encodeCrock(denomPubHash),
-      exchangeBaseUrl: reserve.exchangeBaseUrl,
       reservePub: encodeCrock(reservePub),
       withdrawSig: encodeCrock(sig),
     };
diff --git a/src/db.ts b/src/db.ts
index e317b0aa..ddf3771b 100644
--- a/src/db.ts
+++ b/src/db.ts
@@ -1,5 +1,5 @@
 import { Stores, WALLET_DB_VERSION } from "./dbTypes";
-import { Store, Index } from "./query";
+import { Store, Index } from "./util/query";
 
 const DB_NAME = "taler";
 
@@ -21,9 +21,7 @@ export function openTalerDb(
     req.onsuccess = e => {
       req.result.onversionchange = (evt: IDBVersionChangeEvent) => {
         console.log(
-          `handling live db version change from ${evt.oldVersion} to ${
-            evt.newVersion
-          }`,
+          `handling live db version change from ${evt.oldVersion} to 
${evt.newVersion}`,
         );
         req.result.close();
         onVersionChange();
@@ -33,9 +31,7 @@ export function openTalerDb(
     req.onupgradeneeded = e => {
       const db = req.result;
       console.log(
-        `DB: upgrade needed: oldVersion=${e.oldVersion}, newVersion=${
-          e.newVersion
-        }`,
+        `DB: upgrade needed: oldVersion=${e.oldVersion}, 
newVersion=${e.newVersion}`,
       );
       switch (e.oldVersion) {
         case 0: // DB does not exist yet
diff --git a/src/dbTypes.ts b/src/dbTypes.ts
index 8dba28ed..731f0358 100644
--- a/src/dbTypes.ts
+++ b/src/dbTypes.ts
@@ -23,8 +23,8 @@
 /**
  * Imports.
  */
-import { AmountJson } from "./amounts";
-import { Checkable } from "./checkable";
+import { AmountJson } from "./util/amounts";
+import { Checkable } from "./util/checkable";
 import {
   Auditor,
   CoinPaySig,
@@ -35,7 +35,7 @@ import {
   TipResponse,
 } from "./talerTypes";
 
-import { Index, Store } from "./query";
+import { Index, Store } from "./util/query";
 import { Timestamp, OperationError } from "./walletTypes";
 
 /**
@@ -444,30 +444,22 @@ export interface ExchangeRecord {
  * A coin that isn't yet signed by an exchange.
  */
 export interface PlanchetRecord {
-  withdrawSessionId: string;
-  /**
-   * Index of the coin in the withdrawal session.
-   */
-  coinIndex: number;
-
   /**
    * Public key of the coin.
    */
   coinPub: string;
   coinPriv: string;
+  /**
+   * Public key of the reserve, this might be a reserve not
+   * known to the wallet if the planchet is from a tip.
+   */
   reservePub: string;
   denomPubHash: string;
   denomPub: string;
   blindingKey: string;
   withdrawSig: string;
   coinEv: string;
-  exchangeBaseUrl: string;
   coinValue: AmountJson;
-  /**
-   * Set to true if this pre-coin came from a tip.
-   * Until the tip is marked as "accepted", the resulting
-   * coin will not be used for payments.
-   */
   isFromTip: boolean;
 }
 
@@ -511,6 +503,12 @@ export enum CoinStatus {
   Dormant = "dormant",
 }
 
+export enum CoinSource {
+  Withdraw = "withdraw",
+  Refresh = "refresh",
+  Tip = "tip",
+}
+
 /**
  * CoinRecord as stored in the "coins" data store
  * of the wallet database.
@@ -690,11 +688,9 @@ export interface TipRecord {
   exchangeUrl: string;
 
   /**
-   * Domain of the merchant, necessary to uniquely identify the tip since
-   * merchants can freely choose the ID and a malicious merchant might cause a
-   * collision.
+   * Base URL of the merchant that is giving us the tip.
    */
-  merchantDomain: string;
+  merchantBaseUrl: string;
 
   /**
    * Planchets, the members included in TipPlanchetDetail will be sent to the
@@ -702,13 +698,6 @@ export interface TipRecord {
    */
   planchets?: TipPlanchet[];
 
-  /**
-   * Coin public keys from the planchets.
-   * This field is redundant and used for indexing the record via
-   * a multi-entry index to look up tip records by coin public key.
-   */
-  coinPubs: string[];
-
   /**
    * Response if the merchant responded,
    * undefined otherwise.
@@ -716,18 +705,21 @@ export interface TipRecord {
   response?: TipResponse[];
 
   /**
-   * Identifier for the tip, chosen by the merchant.
+   * Tip ID chosen by the wallet.
    */
   tipId: string;
 
+  /**
+   * The merchant's identifier for this tip.
+   */
+  merchantTipId: string;
+
   /**
    * URL to go to once the tip has been accepted.
    */
   nextUrl?: string;
 
   timestamp: Timestamp;
-
-  pickupUrl: string;
 }
 
 /**
@@ -983,13 +975,24 @@ export interface CoinsReturnRecord {
   wire: any;
 }
 
+export interface WithdrawalSourceTip {
+  type: "tip";
+  tipId: string;
+}
+
+export interface WithdrawalSourceReserve {
+  type: "reserve";
+  reservePub: string;
+}
+
+export type WithdrawalSource = WithdrawalSourceTip | WithdrawalSourceReserve
+
 export interface WithdrawalSessionRecord {
   withdrawSessionId: string;
 
-  /**
-   * Reserve that we're withdrawing from.
-   */
-  reservePub: string;
+  source: WithdrawalSource;
+
+  exchangeBaseUrl: string;
 
   /**
    * When was the withdrawal operation started started?
@@ -1010,15 +1013,12 @@ export interface WithdrawalSessionRecord {
 
   denoms: string[];
 
+  planchets: (undefined | PlanchetRecord)[];
+
   /**
    * Coins in this session that are withdrawn are set to true.
    */
   withdrawn: boolean[];
-
-  /**
-   * Coins in this session already have a planchet are set to true.
-   */
-  planchetCreated: boolean[];
 }
 
 export interface BankWithdrawUriRecord {
@@ -1071,11 +1071,7 @@ export namespace Stores {
     constructor() {
       super("proposals", { keyPath: "proposalId" });
     }
-    urlIndex = new Index<string, ProposalRecord>(
-      this,
-      "urlIndex",
-      "url",
-    );
+    urlIndex = new Index<string, ProposalRecord>(this, "urlIndex", "url");
   }
 
   class PurchasesStore extends Store<PurchaseRecord> {
@@ -1140,16 +1136,8 @@ export namespace Stores {
 
   class TipsStore extends Store<TipRecord> {
     constructor() {
-      super("tips", {
-        keyPath: (["tipId", "merchantDomain"] as any) as IDBKeyPath,
-      });
+      super("tips", { keyPath: "tipId" });
     }
-    coinPubIndex = new Index<string, TipRecord>(
-      this,
-      "coinPubIndex",
-      "coinPubs",
-      { multiEntry: true },
-    );
   }
 
   class SenderWiresStore extends Store<SenderWireRecord> {
@@ -1162,11 +1150,6 @@ export namespace Stores {
     constructor() {
       super("withdrawals", { keyPath: "withdrawSessionId" });
     }
-    byReservePub = new Index<string, WithdrawalSessionRecord>(
-      this,
-      "withdrawalsReservePubIndex",
-      "reservePub",
-    );
   }
 
   class BankWithdrawUrisStore extends Store<BankWithdrawUriRecord> {
@@ -1175,24 +1158,6 @@ export namespace Stores {
     }
   }
 
-  class PlanchetsStore extends Store<PlanchetRecord> {
-    constructor() {
-      super("planchets", {
-        keyPath: "coinPub",
-      });
-    }
-    byReservePub = new Index<string, PlanchetRecord>(
-      this,
-      "planchetsReservePubIndex",
-      "reservePub",
-    );
-    byWithdrawalWithIdx = new Index<any, PlanchetRecord>(
-      this,
-      "planchetsByWithdrawalWithIdxIndex",
-      ["withdrawSessionId", "coinIndex"],
-    );
-  }
-
   export const coins = new CoinsStore();
   export const coinsReturns = new Store<CoinsReturnRecord>("coinsReturns", {
     keyPath: "contractTermsHash",
@@ -1201,7 +1166,6 @@ export namespace Stores {
   export const currencies = new CurrenciesStore();
   export const denominations = new DenominationsStore();
   export const exchanges = new ExchangesStore();
-  export const planchets = new PlanchetsStore();
   export const proposals = new ProposalsStore();
   export const refresh = new Store<RefreshSessionRecord>("refresh", {
     keyPath: "refreshSessionId",
diff --git a/src/headless/bank.ts b/src/headless/bank.ts
index 36f61a71..99d7e050 100644
--- a/src/headless/bank.ts
+++ b/src/headless/bank.ts
@@ -25,7 +25,6 @@
  */
 import Axios from "axios";
 import querystring = require("querystring");
-import URI = require("urijs");
 
 export interface BankUser {
   username: string;
@@ -50,9 +49,7 @@ export class Bank {
       amount,
     };
 
-    const reqUrl = new URI("api/withdraw-headless-uri")
-    .absoluteTo(this.bankBaseUrl)
-    .href();
+    const reqUrl = new URL("api/withdraw-headless-uri", this.bankBaseUrl).href;
 
     const resp = await Axios({
       method: "post",
@@ -82,9 +79,7 @@ export class Bank {
     reservePub: string,
     exchangePaytoUri: string,
   ) {
-    const reqUrl = new URI("api/withdraw-headless")
-      .absoluteTo(this.bankBaseUrl)
-      .href();
+    const reqUrl = new URL("api/withdraw-headless", this.bankBaseUrl).href;
 
     const body = {
       auth: { type: "basic" },
@@ -111,7 +106,7 @@ export class Bank {
   }
 
   async registerRandomUser(): Promise<BankUser> {
-    const reqUrl = new URI("api/register").absoluteTo(this.bankBaseUrl).href();
+    const reqUrl = new URL("api/register", this.bankBaseUrl).href;
     const randId = makeId(8);
     const bankUser: BankUser = {
       username: `testuser-${randId}`,
diff --git a/src/headless/helpers.ts b/src/headless/helpers.ts
index 9faf24da..e5338369 100644
--- a/src/headless/helpers.ts
+++ b/src/headless/helpers.ts
@@ -28,13 +28,13 @@ import { SynchronousCryptoWorkerFactory } from 
"../crypto/synchronousWorker";
 import { openTalerDb } from "../db";
 import Axios from "axios";
 import querystring = require("querystring");
-import { HttpRequestLibrary } from "../http";
-import * as amounts from "../amounts";
+import { HttpRequestLibrary } from "../util/http";
+import * as amounts from "../util/amounts";
 import { Bank } from "./bank";
 
 import fs = require("fs");
 import { NodeCryptoWorkerFactory } from "../crypto/nodeProcessWorker";
-import { Logger } from "../logging";
+import { Logger } from "../util/logging";
 
 const logger = new Logger("helpers.ts");
 
@@ -51,7 +51,7 @@ class ConsoleBadge implements Badge {
 }
 
 export class NodeHttpLib implements HttpRequestLibrary {
-  async get(url: string): Promise<import("../http").HttpResponse> {
+  async get(url: string): Promise<import("../util/http").HttpResponse> {
     try {
       const resp = await Axios({
         method: "get",
@@ -70,7 +70,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
   async postJson(
     url: string,
     body: any,
-  ): Promise<import("../http").HttpResponse> {
+  ): Promise<import("../util/http").HttpResponse> {
     try {
       const resp = await Axios({
         method: "post",
diff --git a/src/headless/merchant.ts b/src/headless/merchant.ts
index 423e3d09..1b963073 100644
--- a/src/headless/merchant.ts
+++ b/src/headless/merchant.ts
@@ -24,7 +24,6 @@
  */
 import axios from "axios";
 import { CheckPaymentResponse } from "../talerTypes";
-import URI = require("urijs");
 
 /**
  * Connection to the *internal* merchant backend.
@@ -35,7 +34,7 @@ export class MerchantBackendConnection {
     reason: string,
     refundAmount: string,
   ): Promise<void> {
-    const reqUrl = new URI("refund").absoluteTo(this.merchantBaseUrl).href();
+    const reqUrl = new URL("refund", this.merchantBaseUrl);
     const refundReq = {
       order_id: orderId,
       reason,
@@ -43,7 +42,7 @@ export class MerchantBackendConnection {
     };
     const resp = await axios({
       method: "post",
-      url: reqUrl,
+      url: reqUrl.href,
       data: refundReq,
       responseType: "json",
       headers: {
@@ -64,7 +63,7 @@ export class MerchantBackendConnection {
   constructor(public merchantBaseUrl: string, public apiKey: string) {}
 
   async authorizeTip(amount: string, justification: string) {
-    const reqUrl = new 
URI("tip-authorize").absoluteTo(this.merchantBaseUrl).href();
+    const reqUrl = new URL("tip-authorize", this.merchantBaseUrl).href;
     const tipReq = {
       amount,
       justification,
@@ -90,7 +89,7 @@ export class MerchantBackendConnection {
     summary: string,
     fulfillmentUrl: string,
   ): Promise<{ orderId: string }> {
-    const reqUrl = new URI("order").absoluteTo(this.merchantBaseUrl).href();
+    const reqUrl = new URL("order", this.merchantBaseUrl).href;
     const orderReq = {
       order: {
         amount,
@@ -118,9 +117,7 @@ export class MerchantBackendConnection {
   }
 
   async checkPayment(orderId: string): Promise<CheckPaymentResponse> {
-    const reqUrl = new URI("check-payment")
-      .absoluteTo(this.merchantBaseUrl)
-      .href();
+    const reqUrl = new URL("check-payment", this.merchantBaseUrl).href;
     const resp = await axios({
       method: "get",
       url: reqUrl,
diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
index cb2ff055..9598b9d9 100644
--- a/src/headless/taler-wallet-cli.ts
+++ b/src/headless/taler-wallet-cli.ts
@@ -23,8 +23,8 @@ import { Wallet, OperationFailedAndReportedError } from 
"../wallet";
 import qrcodeGenerator = require("qrcode-generator");
 import * as clk from "./clk";
 import { BridgeIDBFactory, MemoryBackend } from "idb-bridge";
-import { Logger } from "../logging";
-import * as Amounts from "../amounts";
+import { Logger } from "../util/logging";
+import * as Amounts from "../util/amounts";
 import { decodeCrock } from "../crypto/talerCrypto";
 import { Bank } from "./bank";
 
@@ -93,7 +93,6 @@ async function doPay(
 function applyVerbose(verbose: boolean) {
   if (verbose) {
     console.log("enabled verbose logging");
-    Wallet.enableTracing = true;
     BridgeIDBFactory.enableTracing = true;
   }
 }
@@ -217,7 +216,7 @@ walletCli
       } else if (uri.startsWith("taler://tip/")) {
         const res = await wallet.getTipStatus(uri);
         console.log("tip status", res);
-        await wallet.acceptTip(uri);
+        await wallet.acceptTip(res.tipId);
       } else if (uri.startsWith("taler://refund/")) {
         await wallet.applyRefund(uri);
       } else if (uri.startsWith("taler://withdraw/")) {
diff --git a/src/talerTypes.ts b/src/talerTypes.ts
index 1e658d5b..a6581367 100644
--- a/src/talerTypes.ts
+++ b/src/talerTypes.ts
@@ -26,11 +26,11 @@
 /**
  * Imports.
  */
-import { Checkable } from "./checkable";
+import { Checkable } from "./util/checkable";
 
-import * as Amounts from "./amounts";
+import * as Amounts from "./util/amounts";
 
-import { timestampCheck } from "./helpers";
+import { timestampCheck } from "./util/helpers";
 
 /**
  * Denomination as found in the /keys response from the exchange.
diff --git a/src/types-test.ts b/src/types-test.ts
index 56a82644..38cb9260 100644
--- a/src/types-test.ts
+++ b/src/types-test.ts
@@ -15,12 +15,16 @@
  */
 
 import test from "ava";
-import * as Amounts from "./amounts";
+import * as Amounts from "./util/amounts";
 import { ContractTerms } from "./talerTypes";
 
-const amt = (value: number, fraction: number, currency: string): 
Amounts.AmountJson => ({value, fraction, currency});
+const amt = (
+  value: number,
+  fraction: number,
+  currency: string,
+): Amounts.AmountJson => ({ value, fraction, currency });
 
-test("amount addition (simple)", (t) => {
+test("amount addition (simple)", t => {
   const a1 = amt(1, 0, "EUR");
   const a2 = amt(1, 0, "EUR");
   const a3 = amt(2, 0, "EUR");
@@ -28,14 +32,14 @@ test("amount addition (simple)", (t) => {
   t.pass();
 });
 
-test("amount addition (saturation)", (t) => {
+test("amount addition (saturation)", t => {
   const a1 = amt(1, 0, "EUR");
   const res = Amounts.add(amt(Amounts.maxAmountValue, 0, "EUR"), a1);
   t.true(res.saturated);
   t.pass();
 });
 
-test("amount subtraction (simple)", (t) => {
+test("amount subtraction (simple)", t => {
   const a1 = amt(2, 5, "EUR");
   const a2 = amt(1, 0, "EUR");
   const a3 = amt(1, 5, "EUR");
@@ -43,7 +47,7 @@ test("amount subtraction (simple)", (t) => {
   t.pass();
 });
 
-test("amount subtraction (saturation)", (t) => {
+test("amount subtraction (saturation)", t => {
   const a1 = amt(0, 0, "EUR");
   const a2 = amt(1, 0, "EUR");
   let res = Amounts.sub(a1, a2);
@@ -53,8 +57,7 @@ test("amount subtraction (saturation)", (t) => {
   t.pass();
 });
 
-
-test("amount comparison", (t) => {
+test("amount comparison", t => {
   t.is(Amounts.cmp(amt(1, 0, "EUR"), amt(1, 0, "EUR")), 0);
   t.is(Amounts.cmp(amt(1, 1, "EUR"), amt(1, 0, "EUR")), 1);
   t.is(Amounts.cmp(amt(1, 1, "EUR"), amt(1, 2, "EUR")), -1);
@@ -65,18 +68,36 @@ test("amount comparison", (t) => {
   t.pass();
 });
 
-
-test("amount parsing", (t) => {
-  t.is(Amounts.cmp(Amounts.parseOrThrow("TESTKUDOS:0"),
-                   amt(0, 0, "TESTKUDOS")), 0);
-  t.is(Amounts.cmp(Amounts.parseOrThrow("TESTKUDOS:10"),
-                   amt(10, 0, "TESTKUDOS")), 0);
-  t.is(Amounts.cmp(Amounts.parseOrThrow("TESTKUDOS:0.1"),
-                   amt(0, 10000000, "TESTKUDOS")), 0);
-  t.is(Amounts.cmp(Amounts.parseOrThrow("TESTKUDOS:0.00000001"),
-                   amt(0, 1, "TESTKUDOS")), 0);
-  t.is(Amounts.cmp(Amounts.parseOrThrow("TESTKUDOS:4503599627370496.99999999"),
-                   amt(4503599627370496, 99999999, "TESTKUDOS")), 0);
+test("amount parsing", t => {
+  t.is(
+    Amounts.cmp(Amounts.parseOrThrow("TESTKUDOS:0"), amt(0, 0, "TESTKUDOS")),
+    0,
+  );
+  t.is(
+    Amounts.cmp(Amounts.parseOrThrow("TESTKUDOS:10"), amt(10, 0, "TESTKUDOS")),
+    0,
+  );
+  t.is(
+    Amounts.cmp(
+      Amounts.parseOrThrow("TESTKUDOS:0.1"),
+      amt(0, 10000000, "TESTKUDOS"),
+    ),
+    0,
+  );
+  t.is(
+    Amounts.cmp(
+      Amounts.parseOrThrow("TESTKUDOS:0.00000001"),
+      amt(0, 1, "TESTKUDOS"),
+    ),
+    0,
+  );
+  t.is(
+    Amounts.cmp(
+      Amounts.parseOrThrow("TESTKUDOS:4503599627370496.99999999"),
+      amt(4503599627370496, 99999999, "TESTKUDOS"),
+    ),
+    0,
+  );
   t.throws(() => Amounts.parseOrThrow("foo:"));
   t.throws(() => Amounts.parseOrThrow("1.0"));
   t.throws(() => Amounts.parseOrThrow("42"));
@@ -85,14 +106,18 @@ test("amount parsing", (t) => {
   t.throws(() => Amounts.parseOrThrow("EUR:.42"));
   t.throws(() => Amounts.parseOrThrow("EUR:42."));
   t.throws(() => Amounts.parseOrThrow("TESTKUDOS:4503599627370497.99999999"));
-  t.is(Amounts.cmp(Amounts.parseOrThrow("TESTKUDOS:0.99999999"),
-                   amt(0, 99999999, "TESTKUDOS")), 0);
+  t.is(
+    Amounts.cmp(
+      Amounts.parseOrThrow("TESTKUDOS:0.99999999"),
+      amt(0, 99999999, "TESTKUDOS"),
+    ),
+    0,
+  );
   t.throws(() => Amounts.parseOrThrow("TESTKUDOS:0.999999991"));
   t.pass();
 });
 
-
-test("amount stringification", (t) => {
+test("amount stringification", t => {
   t.is(Amounts.toString(amt(0, 0, "TESTKUDOS")), "TESTKUDOS:0");
   t.is(Amounts.toString(amt(4, 94000000, "TESTKUDOS")), "TESTKUDOS:4.94");
   t.is(Amounts.toString(amt(0, 10000000, "TESTKUDOS")), "TESTKUDOS:0.1");
@@ -103,13 +128,12 @@ test("amount stringification", (t) => {
   t.pass();
 });
 
-
-test("contract terms validation", (t) => {
+test("contract terms validation", t => {
   const c = {
     H_wire: "123",
     amount: "EUR:1.5",
     auditors: [],
-    exchanges: [{master_pub: "foo", url: "foo"}],
+    exchanges: [{ master_pub: "foo", url: "foo" }],
     fulfillment_url: "foo",
     max_fee: "EUR:1.5",
     merchant_pub: "12345",
diff --git a/src/amounts.ts b/src/util/amounts.ts
similarity index 100%
rename from src/amounts.ts
rename to src/util/amounts.ts
diff --git a/src/util/assertUnreachable.ts b/src/util/assertUnreachable.ts
new file mode 100644
index 00000000..90f2476b
--- /dev/null
+++ b/src/util/assertUnreachable.ts
@@ -0,0 +1,19 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+export function assertUnreachable(x: never): never {
+  throw new Error("Didn't expect to get here");
+}
\ No newline at end of file
diff --git a/src/util/asyncMemo.ts b/src/util/asyncMemo.ts
new file mode 100644
index 00000000..8b7b1c9b
--- /dev/null
+++ b/src/util/asyncMemo.ts
@@ -0,0 +1,52 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+export interface MemoEntry<T> {
+  p: Promise<T>;
+  t: number;
+  n: number;
+}
+
+export class AsyncOpMemo<T> {
+  n = 0;
+  memo: { [k: string]: MemoEntry<T> } = {};
+  put(key: string, p: Promise<T>): Promise<T> {
+    const n = this.n++;
+    this.memo[key] = {
+      p,
+      n,
+      t: new Date().getTime(),
+    };
+    p.finally(() => {
+      const r = this.memo[key];
+      if (r && r.n === n) {
+        delete this.memo[key];
+      }
+    });
+    return p;
+  }
+  find(key: string): Promise<T> | undefined {
+    const res = this.memo[key];
+    const tNow = new Date().getTime();
+    if (res && res.t < tNow - 10 * 1000) {
+      delete this.memo[key];
+      return;
+    } else if (res) {
+      return res.p;
+    }
+    return;
+  }
+}
\ No newline at end of file
diff --git a/src/checkable.ts b/src/util/checkable.ts
similarity index 100%
rename from src/checkable.ts
rename to src/util/checkable.ts
diff --git a/src/helpers-test.ts b/src/util/helpers-test.ts
similarity index 100%
rename from src/helpers-test.ts
rename to src/util/helpers-test.ts
diff --git a/src/helpers.ts b/src/util/helpers.ts
similarity index 96%
rename from src/helpers.ts
rename to src/util/helpers.ts
index 1983cee9..eb8a1c7b 100644
--- a/src/helpers.ts
+++ b/src/util/helpers.ts
@@ -24,8 +24,7 @@
 import { AmountJson } from "./amounts";
 import * as Amounts from "./amounts";
 
-import URI = require("urijs");
-import { Timestamp } from "./walletTypes";
+import { Timestamp } from "../walletTypes";
 
 /**
  * Show an amount in a form suitable for the user.
@@ -47,11 +46,13 @@ export function canonicalizeBaseUrl(url: string) {
   if (!url.startsWith("http") && !url.startsWith("https")) {
     url = "https://"; + url;
   }
-  const x = new URI(url);
-  x.path(x.path() + "/").normalizePath();
-  x.fragment("");
-  x.query();
-  return x.href();
+  const x = new URL(url);
+  if (!x.pathname.endsWith("/")) {
+    x.pathname = x.pathname + "/";
+  }
+  x.search = "";
+  x.hash = "";
+  return x.href;
 }
 
 
diff --git a/src/http.ts b/src/util/http.ts
similarity index 100%
rename from src/http.ts
rename to src/util/http.ts
diff --git a/src/libtoolVersion-test.ts b/src/util/libtoolVersion-test.ts
similarity index 100%
rename from src/libtoolVersion-test.ts
rename to src/util/libtoolVersion-test.ts
diff --git a/src/libtoolVersion.ts b/src/util/libtoolVersion.ts
similarity index 100%
rename from src/libtoolVersion.ts
rename to src/util/libtoolVersion.ts
diff --git a/src/logging.ts b/src/util/logging.ts
similarity index 95%
rename from src/logging.ts
rename to src/util/logging.ts
index a21943e6..309d1593 100644
--- a/src/logging.ts
+++ b/src/util/logging.ts
@@ -19,7 +19,7 @@ export class Logger {
   info(message: string, ...args: any[]) {
     console.log(`${new Date().toISOString()} ${this.tag} INFO ` + message, 
...args);
   }
-  trace(message: string, ...args: any[]) {
+  trace(message: any, ...args: any[]) {
     console.log(`${new Date().toISOString()} ${this.tag} TRACE ` + message, 
...args)
   }
 }
\ No newline at end of file
diff --git a/src/util/payto-test.ts b/src/util/payto-test.ts
new file mode 100644
index 00000000..82daff16
--- /dev/null
+++ b/src/util/payto-test.ts
@@ -0,0 +1,31 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+import test from "ava";
+
+import { parsePaytoUri } from "./payto";
+
+test("basic payto parsing", (t) => {
+  const r1 = parsePaytoUri("https://example.com/";);
+  t.is(r1, undefined);
+
+  const r2 = parsePaytoUri("payto:blabla");
+  t.is(r2, undefined);
+
+  const r3 = parsePaytoUri("payto://x-taler-bank/123");
+  t.is(r3?.targetType, "x-taler-bank");
+  t.is(r3?.targetPath, "123");
+});
\ No newline at end of file
diff --git a/src/util/payto.ts b/src/util/payto.ts
new file mode 100644
index 00000000..0926fdee
--- /dev/null
+++ b/src/util/payto.ts
@@ -0,0 +1,54 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+interface PaytoUri {
+  targetType: string;
+  targetPath: string;
+  params: { [name: string]: string };
+}
+
+
+export function parsePaytoUri(s: string): PaytoUri | undefined {
+  const pfx = "payto://"
+  if (!s.startsWith(pfx)) {
+    return undefined;
+  }
+
+  const [acct, search] = s.slice(pfx.length).split("?");
+
+  const firstSlashPos = acct.indexOf("/");
+
+  if (firstSlashPos === -1) {
+    return undefined;
+  }
+
+  const targetType = acct.slice(0, firstSlashPos);
+  const targetPath = acct.slice(firstSlashPos + 1);
+
+  const params: { [k: string]: string } = {};
+
+  const searchParams = new URLSearchParams(search || "");
+
+  searchParams.forEach((v, k) => {
+    params[v] = k;
+  });
+
+  return {
+    targetPath,
+    targetType,
+    params,
+  }
+}
\ No newline at end of file
diff --git a/src/promiseUtils.ts b/src/util/promiseUtils.ts
similarity index 100%
rename from src/promiseUtils.ts
rename to src/util/promiseUtils.ts
diff --git a/src/query.ts b/src/util/query.ts
similarity index 100%
rename from src/query.ts
rename to src/util/query.ts
diff --git a/src/taleruri-test.ts b/src/util/taleruri-test.ts
similarity index 67%
rename from src/taleruri-test.ts
rename to src/util/taleruri-test.ts
index 360f565f..02eecf20 100644
--- a/src/taleruri-test.ts
+++ b/src/util/taleruri-test.ts
@@ -1,5 +1,5 @@
 /*
- This file is part of TALER
+ This file is part of GNU Taler
  (C) 2019 GNUnet e.V.
 
  GNU Taler is free software; you can redistribute it and/or modify it under the
@@ -15,9 +15,14 @@
  */
 
 import test from "ava";
-import { parsePayUri, parseWithdrawUri, parseRefundUri, parseTipUri } from 
"./taleruri";
-
-test("taler pay url parsing: http(s)", (t) => {
+import {
+  parsePayUri,
+  parseWithdrawUri,
+  parseRefundUri,
+  parseTipUri,
+} from "./taleruri";
+
+test("taler pay url parsing: http(s)", t => {
   const url1 = "https://example.com/bar?spam=eggs";;
   const r1 = parsePayUri(url1);
   if (!r1) {
@@ -34,8 +39,7 @@ test("taler pay url parsing: http(s)", (t) => {
   }
 });
 
-
-test("taler pay url parsing: wrong scheme", (t) => {
+test("taler pay url parsing: wrong scheme", t => {
   const url1 = "talerfoo://";
   const r1 = parsePayUri(url1);
   t.is(r1, undefined);
@@ -45,8 +49,7 @@ test("taler pay url parsing: wrong scheme", (t) => {
   t.is(r2, undefined);
 });
 
-
-test("taler pay url parsing: defaults", (t) => {
+test("taler pay url parsing: defaults", t => {
   const url1 = "taler://pay/example.com/-/-/myorder";
   const r1 = parsePayUri(url1);
   if (!r1) {
@@ -66,8 +69,7 @@ test("taler pay url parsing: defaults", (t) => {
   t.is(r2.sessionId, "mysession");
 });
 
-
-test("taler pay url parsing: trailing parts", (t) => {
+test("taler pay url parsing: trailing parts", t => {
   const url1 = "taler://pay/example.com/-/-/myorder/mysession/spam/eggs";
   const r1 = parsePayUri(url1);
   if (!r1) {
@@ -78,49 +80,59 @@ test("taler pay url parsing: trailing parts", (t) => {
   t.is(r1.sessionId, "mysession");
 });
 
-
-test("taler pay url parsing: instance", (t) => {
+test("taler pay url parsing: instance", t => {
   const url1 = "taler://pay/example.com/-/myinst/myorder";
   const r1 = parsePayUri(url1);
   if (!r1) {
     t.fail();
     return;
   }
-  t.is(r1.downloadUrl, 
"https://example.com/public/instances/myinst/proposal?order_id=myorder";);
+  t.is(
+    r1.downloadUrl,
+    "https://example.com/public/instances/myinst/proposal?order_id=myorder";,
+  );
 });
 
-
-test("taler pay url parsing: path prefix and instance", (t) => {
+test("taler pay url parsing: path prefix and instance", t => {
   const url1 = "taler://pay/example.com/mypfx/myinst/myorder";
   const r1 = parsePayUri(url1);
   if (!r1) {
     t.fail();
     return;
   }
-  t.is(r1.downloadUrl, 
"https://example.com/mypfx/instances/myinst/proposal?order_id=myorder";);
+  t.is(
+    r1.downloadUrl,
+    "https://example.com/mypfx/instances/myinst/proposal?order_id=myorder";,
+  );
 });
 
-test("taler pay url parsing: complex path prefix", (t) => {
+test("taler pay url parsing: complex path prefix", t => {
   const url1 = "taler://pay/example.com/mypfx%2Fpublic/-/myorder";
   const r1 = parsePayUri(url1);
   if (!r1) {
     t.fail();
     return;
   }
-  t.is(r1.downloadUrl, 
"https://example.com/mypfx/public/proposal?order_id=myorder";);
+  t.is(
+    r1.downloadUrl,
+    "https://example.com/mypfx/public/proposal?order_id=myorder";,
+  );
 });
 
-test("taler pay url parsing: complex path prefix and instance", (t) => {
+test("taler pay url parsing: complex path prefix and instance", t => {
   const url1 = "taler://pay/example.com/mypfx%2Fpublic/foo/myorder";
   const r1 = parsePayUri(url1);
   if (!r1) {
     t.fail();
     return;
   }
-  t.is(r1.downloadUrl, 
"https://example.com/mypfx/public/instances/foo/proposal?order_id=myorder";);
+  t.is(
+    r1.downloadUrl,
+    "https://example.com/mypfx/public/instances/foo/proposal?order_id=myorder";,
+  );
 });
 
-test("taler pay url parsing: non-https #1", (t) => {
+test("taler pay url parsing: non-https #1", t => {
   const url1 = "taler://pay/example.com/-/-/myorder?insecure=1";
   const r1 = parsePayUri(url1);
   if (!r1) {
@@ -130,7 +142,7 @@ test("taler pay url parsing: non-https #1", (t) => {
   t.is(r1.downloadUrl, "http://example.com/public/proposal?order_id=myorder";);
 });
 
-test("taler pay url parsing: non-https #2", (t) => {
+test("taler pay url parsing: non-https #2", t => {
   const url1 = "taler://pay/example.com/-/-/myorder?insecure=2";
   const r1 = parsePayUri(url1);
   if (!r1) {
@@ -140,8 +152,7 @@ test("taler pay url parsing: non-https #2", (t) => {
   t.is(r1.downloadUrl, "https://example.com/public/proposal?order_id=myorder";);
 });
 
-
-test("taler withdraw uri parsing", (t) => {
+test("taler withdraw uri parsing", t => {
   const url1 = "taler://withdraw/bank.example.com/-/12345";
   const r1 = parseWithdrawUri(url1);
   if (!r1) {
@@ -151,56 +162,69 @@ test("taler withdraw uri parsing", (t) => {
   t.is(r1.statusUrl, "https://bank.example.com/api/withdraw-operation/12345";);
 });
 
-
-test("taler refund uri parsing", (t) => {
+test("taler refund uri parsing", t => {
   const url1 = "taler://refund/merchant.example.com/-/-/1234";
   const r1 = parseRefundUri(url1);
   if (!r1) {
     t.fail();
     return;
   }
-  t.is(r1.refundUrl, 
"https://merchant.example.com/public/refund?order_id=1234";);
+  t.is(
+    r1.refundUrl,
+    "https://merchant.example.com/public/refund?order_id=1234";,
+  );
 });
 
-
-test("taler refund uri parsing with instance", (t) => {
+test("taler refund uri parsing with instance", t => {
   const url1 = "taler://refund/merchant.example.com/-/myinst/1234";
   const r1 = parseRefundUri(url1);
   if (!r1) {
     t.fail();
     return;
   }
-  t.is(r1.refundUrl, 
"https://merchant.example.com/public/instances/myinst/refund?order_id=1234";);
+  t.is(
+    r1.refundUrl,
+    
"https://merchant.example.com/public/instances/myinst/refund?order_id=1234";,
+  );
 });
 
-test("taler tip pickup uri", (t) => {
+test("taler tip pickup uri", t => {
   const url1 = "taler://tip/merchant.example.com/-/-/tipid";
   const r1 = parseTipUri(url1);
   if (!r1) {
     t.fail();
     return;
   }
-  t.is(r1.tipPickupUrl, 
"https://merchant.example.com/public/tip-pickup?tip_id=tipid";);
+  t.is(
+    r1.merchantBaseUrl,
+    "https://merchant.example.com/public/tip-pickup?tip_id=tipid";,
+  );
 });
 
-
-test("taler tip pickup uri with instance", (t) => {
+test("taler tip pickup uri with instance", t => {
   const url1 = "taler://tip/merchant.example.com/-/tipm/tipid";
   const r1 = parseTipUri(url1);
   if (!r1) {
     t.fail();
     return;
   }
-  t.is(r1.tipPickupUrl, 
"https://merchant.example.com/public/instances/tipm/tip-pickup?tip_id=tipid";);
+  t.is(
+    r1.merchantBaseUrl,
+    "https://merchant.example.com/public/instances/tipm/";,
+  );
+  t.is(r1.merchantTipId, "tipid");
 });
 
-
-test("taler tip pickup uri with instance and prefix", (t) => {
+test("taler tip pickup uri with instance and prefix", t => {
   const url1 = "taler://tip/merchant.example.com/my%2fpfx/tipm/tipid";
   const r1 = parseTipUri(url1);
   if (!r1) {
     t.fail();
     return;
   }
-  t.is(r1.tipPickupUrl, 
"https://merchant.example.com/my/pfx/instances/tipm/tip-pickup?tip_id=tipid";);
+  t.is(
+    r1.merchantBaseUrl,
+    "https://merchant.example.com/my/pfx/instances/tipm/";,
+  );
+  t.is(r1.merchantTipId, "tipid");
 });
diff --git a/src/taleruri.ts b/src/util/taleruri.ts
similarity index 57%
rename from src/taleruri.ts
rename to src/util/taleruri.ts
index c810def2..aa6705c0 100644
--- a/src/taleruri.ts
+++ b/src/util/taleruri.ts
@@ -1,5 +1,5 @@
 /*
- This file is part of TALER
+ This file is part of GNU Taler
  (C) 2019 GNUnet e.V.
 
  GNU Taler is free software; you can redistribute it and/or modify it under the
@@ -14,9 +14,6 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import URI = require("urijs");
-import { string } from "prop-types";
-
 export interface PayUriResult {
   downloadUrl: string;
   sessionId?: string;
@@ -31,58 +28,47 @@ export interface RefundUriResult {
 }
 
 export interface TipUriResult {
-  tipPickupUrl: string;
-  tipId: string;
-  merchantInstance: string;
+  merchantTipId: string;
   merchantOrigin: string;
+  merchantBaseUrl: string;
 }
 
 export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
-  const parsedUri = new URI(s);
-  if (parsedUri.scheme() !== "taler") {
-    return undefined;
-  }
-  if (parsedUri.authority() != "withdraw") {
+  const pfx = "taler://withdraw/";
+  if (!s.startsWith(pfx)) {
     return undefined;
   }
 
-  let [host, path, withdrawId] = parsedUri.segmentCoded();
+  const rest = s.substring(pfx.length);
+
+  let [host, path, withdrawId] = rest.split("/");
 
   if (path === "-") {
-    path = "/api/withdraw-operation";
+    path = "api/withdraw-operation";
   }
 
   return {
-    statusUrl: new URI({ protocol: "https", hostname: host, path: path })
-      .segmentCoded(withdrawId)
-      .href(),
+    statusUrl: `https://${host}/${path}/${withdrawId}`,
   };
 }
 
 export function parsePayUri(s: string): PayUriResult | undefined {
-  const parsedUri = new URI(s);
-  const query: any = parsedUri.query(true);
-  if (parsedUri.scheme() === "http" || parsedUri.scheme() === "https") {
+  if (s.startsWith("https://";) || s.startsWith("http://";)) {
     return {
       downloadUrl: s,
       sessionId: undefined,
     };
   }
-  if (parsedUri.scheme() != "taler") {
-    return undefined;
-  }
-  if (parsedUri.authority() != "pay") {
+  const pfx = "taler://pay/";
+  if (!s.startsWith(pfx)) {
     return undefined;
   }
 
-  let [
-    _,
-    host,
-    maybePath,
-    maybeInstance,
-    orderId,
-    maybeSessionid,
-  ] = parsedUri.path().split("/");
+  const [path, search] = s.slice(pfx.length).split("?");
+
+  let [host, maybePath, maybeInstance, orderId, maybeSessionid] = path.split(
+    "/",
+  );
 
   if (!host) {
     return undefined;
@@ -107,15 +93,16 @@ export function parsePayUri(s: string): PayUriResult | 
undefined {
   }
 
   let protocol = "https";
-  if (query["insecure"] === "1") {
+  const searchParams = new URLSearchParams(search);
+  if (searchParams.get("insecure") === "1") {
     protocol = "http";
   }
 
-  const downloadUrl = new URI(
-    protocol + "://" + host + "/" + decodeURIComponent(maybePath) + 
maybeInstancePath + "proposal",
-  )
-    .addQuery({ order_id: orderId })
-    .href();
+  const downloadUrl =
+    `${protocol}://${host}/` +
+    decodeURIComponent(maybePath) +
+    maybeInstancePath +
+    `proposal?order_id=${orderId}`;
 
   return {
     downloadUrl,
@@ -124,15 +111,14 @@ export function parsePayUri(s: string): PayUriResult | 
undefined {
 }
 
 export function parseTipUri(s: string): TipUriResult | undefined {
-  const parsedUri = new URI(s);
-  if (parsedUri.scheme() != "taler") {
-    return undefined;
-  }
-  if (parsedUri.authority() != "tip") {
+  const pfx = "taler://tip/";
+  if (!s.startsWith(pfx)) {
     return undefined;
   }
 
-  let [_, host, maybePath, maybeInstance, tipId] = parsedUri.path().split("/");
+  const path = s.slice(pfx.length);
+
+  let [host, maybePath, maybeInstance, tipId] = path.split("/");
 
   if (!host) {
     return undefined;
@@ -156,34 +142,25 @@ export function parseTipUri(s: string): TipUriResult | 
undefined {
     maybeInstancePath = `instances/${maybeInstance}/`;
   }
 
-  const tipPickupUrl = new URI(
-    "https://"; + host + "/" + maybePath + maybeInstancePath + "tip-pickup",
-  ).addQuery({ tip_id: tipId }).href();
+  const merchantBaseUrl = `https://${host}/${maybePath}${maybeInstancePath}`;
 
   return {
-    tipPickupUrl,
-    tipId: tipId,
-    merchantInstance: maybeInstance,
-    merchantOrigin: new URI(tipPickupUrl).origin(),
+    merchantTipId: tipId,
+    merchantOrigin: new URL(merchantBaseUrl).origin,
+    merchantBaseUrl,
   };
 }
 
 export function parseRefundUri(s: string): RefundUriResult | undefined {
-  const parsedUri = new URI(s);
-  if (parsedUri.scheme() != "taler") {
-    return undefined;
-  }
-  if (parsedUri.authority() != "refund") {
+  const pfx = "taler://refund/";
+
+  if (!s.startsWith(pfx)) {
     return undefined;
   }
 
-  let [
-    _,
-    host,
-    maybePath,
-    maybeInstance,
-    orderId,
-  ] = parsedUri.path().split("/");
+  const path = s.slice(pfx.length);
+
+  let [host, maybePath, maybeInstance, orderId] = path.split("/");
 
   if (!host) {
     return undefined;
@@ -207,11 +184,16 @@ export function parseRefundUri(s: string): 
RefundUriResult | undefined {
     maybeInstancePath = `instances/${maybeInstance}/`;
   }
 
-  const refundUrl = new URI(
-    "https://"; + host + "/" + maybePath + maybeInstancePath + "refund",
-  )
-    .addQuery({ order_id: orderId })
-    .href();
+  const refundUrl =
+    "https://"; +
+    host +
+    "/" +
+    maybePath +
+    maybeInstancePath +
+    "refund" +
+    "?order_id=" +
+    orderId;
+
   return {
     refundUrl,
   };
diff --git a/src/timer.ts b/src/util/timer.ts
similarity index 100%
rename from src/timer.ts
rename to src/util/timer.ts
diff --git a/src/wire.ts b/src/util/wire.ts
similarity index 97%
rename from src/wire.ts
rename to src/util/wire.ts
index c06a30bb..63b73d86 100644
--- a/src/wire.ts
+++ b/src/util/wire.ts
@@ -25,7 +25,7 @@
 /**
  * Imports.
  */
-import * as i18n from "./i18n";
+import * as i18n from "../i18n";
 
 /**
  * Short summary of the wire information.
diff --git a/src/wallet-impl/balance.ts b/src/wallet-impl/balance.ts
new file mode 100644
index 00000000..1d8e077a
--- /dev/null
+++ b/src/wallet-impl/balance.ts
@@ -0,0 +1,144 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+  HistoryQuery,
+  HistoryEvent,
+  WalletBalance,
+  WalletBalanceEntry,
+} from "../walletTypes";
+import { oneShotIter, runWithWriteTransaction } from "../util/query";
+import { InternalWalletState } from "./state";
+import { Stores, TipRecord, CoinStatus } from "../dbTypes";
+import * as Amounts from "../util/amounts";
+import { AmountJson } from "../util/amounts";
+import { Logger } from "../util/logging";
+
+const logger = new Logger("withdraw.ts");
+
+/**
+ * Get detailed balance information, sliced by exchange and by currency.
+ */
+export async function getBalances(
+  ws: InternalWalletState,
+): Promise<WalletBalance> {
+  /**
+   * Add amount to a balance field, both for
+   * the slicing by exchange and currency.
+   */
+  function addTo(
+    balance: WalletBalance,
+    field: keyof WalletBalanceEntry,
+    amount: AmountJson,
+    exchange: string,
+  ): void {
+    const z = Amounts.getZero(amount.currency);
+    const balanceIdentity = {
+      available: z,
+      paybackAmount: z,
+      pendingIncoming: z,
+      pendingPayment: z,
+      pendingIncomingDirty: z,
+      pendingIncomingRefresh: z,
+      pendingIncomingWithdraw: z,
+    };
+    let entryCurr = balance.byCurrency[amount.currency];
+    if (!entryCurr) {
+      balance.byCurrency[amount.currency] = entryCurr = {
+        ...balanceIdentity,
+      };
+    }
+    let entryEx = balance.byExchange[exchange];
+    if (!entryEx) {
+      balance.byExchange[exchange] = entryEx = { ...balanceIdentity };
+    }
+    entryCurr[field] = Amounts.add(entryCurr[field], amount).amount;
+    entryEx[field] = Amounts.add(entryEx[field], amount).amount;
+  }
+
+  const balanceStore = {
+    byCurrency: {},
+    byExchange: {},
+  };
+
+  await runWithWriteTransaction(
+    ws.db,
+    [Stores.coins, Stores.refresh, Stores.reserves, Stores.purchases],
+    async tx => {
+      await tx.iter(Stores.coins).forEach(c => {
+        if (c.suspended) {
+          return;
+        }
+        if (c.status === CoinStatus.Fresh) {
+          addTo(balanceStore, "available", c.currentAmount, c.exchangeBaseUrl);
+        }
+        if (c.status === CoinStatus.Dirty) {
+          addTo(
+            balanceStore,
+            "pendingIncoming",
+            c.currentAmount,
+            c.exchangeBaseUrl,
+          );
+          addTo(
+            balanceStore,
+            "pendingIncomingDirty",
+            c.currentAmount,
+            c.exchangeBaseUrl,
+          );
+        }
+      });
+      await tx.iter(Stores.refresh).forEach(r => {
+        // Don't count finished refreshes, since the refresh already resulted
+        // in coins being added to the wallet.
+        if (r.finished) {
+          return;
+        }
+        addTo(
+          balanceStore,
+          "pendingIncoming",
+          r.valueOutput,
+          r.exchangeBaseUrl,
+        );
+        addTo(
+          balanceStore,
+          "pendingIncomingRefresh",
+          r.valueOutput,
+          r.exchangeBaseUrl,
+        );
+      });
+
+      await tx.iter(Stores.purchases).forEach(t => {
+        if (t.finished) {
+          return;
+        }
+        for (const c of t.payReq.coins) {
+          addTo(
+            balanceStore,
+            "pendingPayment",
+            Amounts.parseOrThrow(c.contribution),
+            c.exchange_url,
+          );
+        }
+      });
+    },
+  );
+
+  logger.trace("computed balances:", balanceStore);
+  return balanceStore;
+}
diff --git a/src/wallet-impl/exchanges.ts b/src/wallet-impl/exchanges.ts
new file mode 100644
index 00000000..b3677c6c
--- /dev/null
+++ b/src/wallet-impl/exchanges.ts
@@ -0,0 +1,401 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { InternalWalletState } from "./state";
+import {
+  WALLET_CACHE_BREAKER_CLIENT_VERSION,
+  OperationFailedAndReportedError,
+} from "../wallet";
+import { KeysJson, Denomination, ExchangeWireJson } from "../talerTypes";
+import { getTimestampNow, OperationError } from "../walletTypes";
+import {
+  ExchangeRecord,
+  ExchangeUpdateStatus,
+  Stores,
+  DenominationRecord,
+  DenominationStatus,
+  WireFee,
+} from "../dbTypes";
+import {
+  canonicalizeBaseUrl,
+  extractTalerStamp,
+  extractTalerStampOrThrow,
+} from "../util/helpers";
+import {
+  oneShotGet,
+  oneShotPut,
+  runWithWriteTransaction,
+  oneShotMutate,
+} from "../util/query";
+import * as Amounts from "../util/amounts";
+import { parsePaytoUri } from "../util/payto";
+
+async function denominationRecordFromKeys(
+  ws: InternalWalletState,
+  exchangeBaseUrl: string,
+  denomIn: Denomination,
+): Promise<DenominationRecord> {
+  const denomPubHash = await ws.cryptoApi.hashDenomPub(denomIn.denom_pub);
+  const d: DenominationRecord = {
+    denomPub: denomIn.denom_pub,
+    denomPubHash,
+    exchangeBaseUrl,
+    feeDeposit: Amounts.parseOrThrow(denomIn.fee_deposit),
+    feeRefresh: Amounts.parseOrThrow(denomIn.fee_refresh),
+    feeRefund: Amounts.parseOrThrow(denomIn.fee_refund),
+    feeWithdraw: Amounts.parseOrThrow(denomIn.fee_withdraw),
+    isOffered: true,
+    masterSig: denomIn.master_sig,
+    stampExpireDeposit: extractTalerStampOrThrow(denomIn.stamp_expire_deposit),
+    stampExpireLegal: extractTalerStampOrThrow(denomIn.stamp_expire_legal),
+    stampExpireWithdraw: extractTalerStampOrThrow(
+      denomIn.stamp_expire_withdraw,
+    ),
+    stampStart: extractTalerStampOrThrow(denomIn.stamp_start),
+    status: DenominationStatus.Unverified,
+    value: Amounts.parseOrThrow(denomIn.value),
+  };
+  return d;
+}
+
+async function setExchangeError(
+  ws: InternalWalletState,
+  baseUrl: string,
+  err: OperationError,
+): Promise<void> {
+  const mut = (exchange: ExchangeRecord) => {
+    exchange.lastError = err;
+    return exchange;
+  };
+  await oneShotMutate(ws.db, Stores.exchanges, baseUrl, mut);
+}
+
+/**
+ * Fetch the exchange's /keys and update our database accordingly.
+ *
+ * Exceptions thrown in this method must be caught and reported
+ * in the pending operations.
+ */
+async function updateExchangeWithKeys(
+  ws: InternalWalletState,
+  baseUrl: string,
+): Promise<void> {
+  const existingExchangeRecord = await oneShotGet(
+    ws.db,
+    Stores.exchanges,
+    baseUrl,
+  );
+
+  if (existingExchangeRecord?.updateStatus != ExchangeUpdateStatus.FETCH_KEYS) 
{
+    return;
+  }
+  const keysUrl = new URL("keys", baseUrl);
+  keysUrl.searchParams.set("cacheBreaker", 
WALLET_CACHE_BREAKER_CLIENT_VERSION);
+
+  let keysResp;
+  try {
+    keysResp = await ws.http.get(keysUrl.href);
+  } catch (e) {
+    const m = `Fetching keys failed: ${e.message}`;
+    await setExchangeError(ws, baseUrl, {
+      type: "network",
+      details: {
+        requestUrl: e.config?.url,
+      },
+      message: m,
+    });
+    throw new OperationFailedAndReportedError(m);
+  }
+  let exchangeKeysJson: KeysJson;
+  try {
+    exchangeKeysJson = KeysJson.checked(keysResp.responseJson);
+  } catch (e) {
+    const m = `Parsing /keys response failed: ${e.message}`;
+    await setExchangeError(ws, baseUrl, {
+      type: "protocol-violation",
+      details: {},
+      message: m,
+    });
+    throw new OperationFailedAndReportedError(m);
+  }
+
+  const lastUpdateTimestamp = extractTalerStamp(
+    exchangeKeysJson.list_issue_date,
+  );
+  if (!lastUpdateTimestamp) {
+    const m = `Parsing /keys response failed: invalid list_issue_date.`;
+    await setExchangeError(ws, baseUrl, {
+      type: "protocol-violation",
+      details: {},
+      message: m,
+    });
+    throw new OperationFailedAndReportedError(m);
+  }
+
+  if (exchangeKeysJson.denoms.length === 0) {
+    const m = "exchange doesn't offer any denominations";
+    await setExchangeError(ws, baseUrl, {
+      type: "protocol-violation",
+      details: {},
+      message: m,
+    });
+    throw new OperationFailedAndReportedError(m);
+  }
+
+  const protocolVersion = exchangeKeysJson.version;
+  if (!protocolVersion) {
+    const m = "outdate exchange, no version in /keys response";
+    await setExchangeError(ws, baseUrl, {
+      type: "protocol-violation",
+      details: {},
+      message: m,
+    });
+    throw new OperationFailedAndReportedError(m);
+  }
+
+  const currency = Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value)
+    .currency;
+
+  const newDenominations = await Promise.all(
+    exchangeKeysJson.denoms.map(d =>
+      denominationRecordFromKeys(ws, baseUrl, d),
+    ),
+  );
+
+  await runWithWriteTransaction(
+    ws.db,
+    [Stores.exchanges, Stores.denominations],
+    async tx => {
+      const r = await tx.get(Stores.exchanges, baseUrl);
+      if (!r) {
+        console.warn(`exchange ${baseUrl} no longer present`);
+        return;
+      }
+      if (r.details) {
+        // FIXME: We need to do some consistency checks!
+      }
+      r.details = {
+        auditors: exchangeKeysJson.auditors,
+        currency: currency,
+        lastUpdateTime: lastUpdateTimestamp,
+        masterPublicKey: exchangeKeysJson.master_public_key,
+        protocolVersion: protocolVersion,
+      };
+      r.updateStatus = ExchangeUpdateStatus.FETCH_WIRE;
+      r.lastError = undefined;
+      await tx.put(Stores.exchanges, r);
+
+      for (const newDenom of newDenominations) {
+        const oldDenom = await tx.get(Stores.denominations, [
+          baseUrl,
+          newDenom.denomPub,
+        ]);
+        if (oldDenom) {
+          // FIXME: Do consistency check
+        } else {
+          await tx.put(Stores.denominations, newDenom);
+        }
+      }
+    },
+  );
+}
+
+/**
+ * Fetch wire information for an exchange and store it in the database.
+ *
+ * @param exchangeBaseUrl Exchange base URL, assumed to be already normalized.
+ */
+async function updateExchangeWithWireInfo(
+  ws: InternalWalletState,
+  exchangeBaseUrl: string,
+) {
+  const exchange = await oneShotGet(ws.db, Stores.exchanges, exchangeBaseUrl);
+  if (!exchange) {
+    return;
+  }
+  if (exchange.updateStatus != ExchangeUpdateStatus.FETCH_WIRE) {
+    return;
+  }
+  const reqUrl = new URL("wire", exchangeBaseUrl);
+  reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION)
+
+  const resp = await ws.http.get(reqUrl.href);
+
+  const wiJson = resp.responseJson;
+  if (!wiJson) {
+    throw Error("/wire response malformed");
+  }
+  const wireInfo = ExchangeWireJson.checked(wiJson);
+  const feesForType: { [wireMethod: string]: WireFee[] } = {};
+  for (const wireMethod of Object.keys(wireInfo.fees)) {
+    const feeList: WireFee[] = [];
+    for (const x of wireInfo.fees[wireMethod]) {
+      const startStamp = extractTalerStamp(x.start_date);
+      if (!startStamp) {
+        throw Error("wrong date format");
+      }
+      const endStamp = extractTalerStamp(x.end_date);
+      if (!endStamp) {
+        throw Error("wrong date format");
+      }
+      feeList.push({
+        closingFee: Amounts.parseOrThrow(x.closing_fee),
+        endStamp,
+        sig: x.sig,
+        startStamp,
+        wireFee: Amounts.parseOrThrow(x.wire_fee),
+      });
+    }
+    feesForType[wireMethod] = feeList;
+  }
+
+  await runWithWriteTransaction(ws.db, [Stores.exchanges], async tx => {
+    const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
+    if (!r) {
+      return;
+    }
+    if (r.updateStatus != ExchangeUpdateStatus.FETCH_WIRE) {
+      return;
+    }
+    r.wireInfo = {
+      accounts: wireInfo.accounts,
+      feesForType: feesForType,
+    };
+    r.updateStatus = ExchangeUpdateStatus.FINISHED;
+    r.lastError = undefined;
+    await tx.put(Stores.exchanges, r);
+  });
+}
+
+/**
+ * Update or add exchange DB entry by fetching the /keys and /wire information.
+ * Optionally link the reserve entry to the new or existing
+ * exchange entry in then DB.
+ */
+export async function updateExchangeFromUrl(
+  ws: InternalWalletState,
+  baseUrl: string,
+  force: boolean = false,
+): Promise<ExchangeRecord> {
+  const now = getTimestampNow();
+  baseUrl = canonicalizeBaseUrl(baseUrl);
+
+  const r = await oneShotGet(ws.db, Stores.exchanges, baseUrl);
+  if (!r) {
+    const newExchangeRecord: ExchangeRecord = {
+      baseUrl: baseUrl,
+      details: undefined,
+      wireInfo: undefined,
+      updateStatus: ExchangeUpdateStatus.FETCH_KEYS,
+      updateStarted: now,
+      updateReason: "initial",
+      timestampAdded: getTimestampNow(),
+    };
+    await oneShotPut(ws.db, Stores.exchanges, newExchangeRecord);
+  } else {
+    await runWithWriteTransaction(ws.db, [Stores.exchanges], async t => {
+      const rec = await t.get(Stores.exchanges, baseUrl);
+      if (!rec) {
+        return;
+      }
+      if (rec.updateStatus != ExchangeUpdateStatus.FETCH_KEYS && !force) {
+        return;
+      }
+      if (rec.updateStatus != ExchangeUpdateStatus.FETCH_KEYS && force) {
+        rec.updateReason = "forced";
+      }
+      rec.updateStarted = now;
+      rec.updateStatus = ExchangeUpdateStatus.FETCH_KEYS;
+      rec.lastError = undefined;
+      t.put(Stores.exchanges, rec);
+    });
+  }
+
+  await updateExchangeWithKeys(ws, baseUrl);
+  await updateExchangeWithWireInfo(ws, baseUrl);
+
+  const updatedExchange = await oneShotGet(ws.db, Stores.exchanges, baseUrl);
+
+  if (!updatedExchange) {
+    // This should practically never happen
+    throw Error("exchange not found");
+  }
+  return updatedExchange;
+}
+
+/**
+ * Check if and how an exchange is trusted and/or audited.
+ */
+export async function getExchangeTrust(
+  ws: InternalWalletState,
+  exchangeInfo: ExchangeRecord,
+): Promise<{ isTrusted: boolean; isAudited: boolean }> {
+  let isTrusted = false;
+  let isAudited = false;
+  const exchangeDetails = exchangeInfo.details;
+  if (!exchangeDetails) {
+    throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
+  }
+  const currencyRecord = await oneShotGet(
+    ws.db,
+    Stores.currencies,
+    exchangeDetails.currency,
+  );
+  if (currencyRecord) {
+    for (const trustedExchange of currencyRecord.exchanges) {
+      if (trustedExchange.exchangePub === exchangeDetails.masterPublicKey) {
+        isTrusted = true;
+        break;
+      }
+    }
+    for (const trustedAuditor of currencyRecord.auditors) {
+      for (const exchangeAuditor of exchangeDetails.auditors) {
+        if (trustedAuditor.auditorPub === exchangeAuditor.auditor_pub) {
+          isAudited = true;
+          break;
+        }
+      }
+    }
+  }
+  return { isTrusted, isAudited };
+}
+
+export async function getExchangePaytoUri(
+  ws: InternalWalletState,
+  exchangeBaseUrl: string,
+  supportedTargetTypes: string[],
+): Promise<string> {
+  // We do the update here, since the exchange might not even exist
+  // yet in our database.
+  const exchangeRecord = await updateExchangeFromUrl(ws, exchangeBaseUrl);
+  if (!exchangeRecord) {
+    throw Error(`Exchange '${exchangeBaseUrl}' not found.`);
+  }
+  const exchangeWireInfo = exchangeRecord.wireInfo;
+  if (!exchangeWireInfo) {
+    throw Error(`Exchange wire info for '${exchangeBaseUrl}' not found.`);
+  }
+  for (let account of exchangeWireInfo.accounts) {
+    const res = parsePaytoUri(account.url);
+    if (!res) {
+      continue;
+    }
+    if (supportedTargetTypes.includes(res.targetType)) {
+      return account.url;
+    }
+  }
+  throw Error("no matching exchange account found");
+}
diff --git a/src/wallet-impl/history.ts b/src/wallet-impl/history.ts
new file mode 100644
index 00000000..976dab88
--- /dev/null
+++ b/src/wallet-impl/history.ts
@@ -0,0 +1,172 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+ /**
+  * Imports.
+  */
+import { HistoryQuery, HistoryEvent } from "../walletTypes";
+import { oneShotIter } from "../util/query";
+import { InternalWalletState } from "./state";
+import { Stores, TipRecord } from "../dbTypes";
+import * as Amounts from "../util/amounts";
+import { AmountJson } from "../util/amounts";
+
+/**
+ * Retrive the full event history for this wallet.
+ */
+export async function getHistory(
+  ws: InternalWalletState,
+  historyQuery?: HistoryQuery,
+): Promise<{ history: HistoryEvent[] }> {
+  const history: HistoryEvent[] = [];
+
+  // FIXME: do pagination instead of generating the full history
+
+  // We uniquely identify history rows via their timestamp.
+  // This works as timestamps are guaranteed to be monotonically
+  // increasing even
+
+  const proposals = await oneShotIter(ws.db, Stores.proposals).toArray();
+  for (const p of proposals) {
+    history.push({
+      detail: {
+        contractTermsHash: p.contractTermsHash,
+        merchantName: p.contractTerms.merchant.name,
+      },
+      timestamp: p.timestamp,
+      type: "claim-order",
+      explicit: false,
+    });
+  }
+
+  const withdrawals = await oneShotIter(
+    ws.db,
+    Stores.withdrawalSession,
+  ).toArray();
+  for (const w of withdrawals) {
+    history.push({
+      detail: {
+        withdrawalAmount: w.withdrawalAmount,
+      },
+      timestamp: w.startTimestamp,
+      type: "withdraw",
+      explicit: false,
+    });
+  }
+
+  const purchases = await oneShotIter(ws.db, Stores.purchases).toArray();
+  for (const p of purchases) {
+    history.push({
+      detail: {
+        amount: p.contractTerms.amount,
+        contractTermsHash: p.contractTermsHash,
+        fulfillmentUrl: p.contractTerms.fulfillment_url,
+        merchantName: p.contractTerms.merchant.name,
+      },
+      timestamp: p.timestamp,
+      type: "pay",
+      explicit: false,
+    });
+    if (p.timestamp_refund) {
+      const contractAmount = Amounts.parseOrThrow(p.contractTerms.amount);
+      const amountsPending = Object.keys(p.refundsPending).map(x =>
+        Amounts.parseOrThrow(p.refundsPending[x].refund_amount),
+      );
+      const amountsDone = Object.keys(p.refundsDone).map(x =>
+        Amounts.parseOrThrow(p.refundsDone[x].refund_amount),
+      );
+      const amounts: AmountJson[] = amountsPending.concat(amountsDone);
+      const amount = Amounts.add(
+        Amounts.getZero(contractAmount.currency),
+        ...amounts,
+      ).amount;
+
+      history.push({
+        detail: {
+          contractTermsHash: p.contractTermsHash,
+          fulfillmentUrl: p.contractTerms.fulfillment_url,
+          merchantName: p.contractTerms.merchant.name,
+          refundAmount: amount,
+        },
+        timestamp: p.timestamp_refund,
+        type: "refund",
+        explicit: false,
+      });
+    }
+  }
+
+  const reserves = await oneShotIter(ws.db, Stores.reserves).toArray();
+
+  for (const r of reserves) {
+    const reserveType = r.bankWithdrawStatusUrl ? "taler-bank" : "manual";
+    history.push({
+      detail: {
+        exchangeBaseUrl: r.exchangeBaseUrl,
+        requestedAmount: Amounts.toString(r.initiallyRequestedAmount),
+        reservePub: r.reservePub,
+        reserveType,
+        bankWithdrawStatusUrl: r.bankWithdrawStatusUrl,
+      },
+      timestamp: r.created,
+      type: "reserve-created",
+      explicit: false,
+    });
+    if (r.timestampConfirmed) {
+      history.push({
+        detail: {
+          exchangeBaseUrl: r.exchangeBaseUrl,
+          requestedAmount: Amounts.toString(r.initiallyRequestedAmount),
+          reservePub: r.reservePub,
+          reserveType,
+          bankWithdrawStatusUrl: r.bankWithdrawStatusUrl,
+        },
+        timestamp: r.created,
+        type: "reserve-confirmed",
+        explicit: false,
+      });
+    }
+  }
+
+  const tips: TipRecord[] = await oneShotIter(ws.db, Stores.tips).toArray();
+  for (const tip of tips) {
+    history.push({
+      detail: {
+        accepted: tip.accepted,
+        amount: tip.amount,
+        merchantBaseUrl: tip.merchantBaseUrl,
+        tipId: tip.merchantTipId,
+      },
+      timestamp: tip.timestamp,
+      explicit: false,
+      type: "tip",
+    });
+  }
+
+  await oneShotIter(ws.db, Stores.exchanges).forEach(exchange => {
+    history.push({
+      type: "exchange-added",
+      explicit: false,
+      timestamp: exchange.timestampAdded,
+      detail: {
+        exchangeBaseUrl: exchange.baseUrl,
+      },
+    });
+  });
+
+  history.sort((h1, h2) => Math.sign(h1.timestamp.t_ms - h2.timestamp.t_ms));
+
+  return { history };
+}
diff --git a/src/wallet-impl/pay.ts b/src/wallet-impl/pay.ts
new file mode 100644
index 00000000..d4d2b3cd
--- /dev/null
+++ b/src/wallet-impl/pay.ts
@@ -0,0 +1,822 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { AmountJson } from "../util/amounts";
+import {
+  Auditor,
+  ExchangeHandle,
+  MerchantRefundResponse,
+  PayReq,
+  Proposal,
+  ContractTerms,
+} from "../talerTypes";
+import {
+  Timestamp,
+  CoinSelectionResult,
+  CoinWithDenom,
+  PayCoinInfo,
+  getTimestampNow,
+  PreparePayResult,
+  ConfirmPayResult,
+} from "../walletTypes";
+import {
+  oneShotIter,
+  oneShotIterIndex,
+  oneShotGet,
+  runWithWriteTransaction,
+  oneShotPut,
+  oneShotGetIndexed,
+} from "../util/query";
+import {
+  Stores,
+  CoinStatus,
+  DenominationRecord,
+  ProposalRecord,
+  PurchaseRecord,
+  CoinRecord,
+  ProposalStatus,
+} from "../dbTypes";
+import * as Amounts from "../util/amounts";
+import {
+  amountToPretty,
+  strcmp,
+  extractTalerStamp,
+  canonicalJson,
+} from "../util/helpers";
+import { Logger } from "../util/logging";
+import { InternalWalletState } from "./state";
+import { parsePayUri } from "../util/taleruri";
+import { getTotalRefreshCost, refresh } from "./refresh";
+import { acceptRefundResponse } from "./refund";
+import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
+
+export interface SpeculativePayData {
+  payCoinInfo: PayCoinInfo;
+  exchangeUrl: string;
+  orderDownloadId: string;
+  proposal: ProposalRecord;
+}
+
+interface CoinsForPaymentArgs {
+  allowedAuditors: Auditor[];
+  allowedExchanges: ExchangeHandle[];
+  depositFeeLimit: AmountJson;
+  paymentAmount: AmountJson;
+  wireFeeAmortization: number;
+  wireFeeLimit: AmountJson;
+  wireFeeTime: Timestamp;
+  wireMethod: string;
+}
+
+interface SelectPayCoinsResult {
+  cds: CoinWithDenom[];
+  totalFees: AmountJson;
+}
+
+const logger = new Logger("pay.ts");
+
+/**
+ * Select coins for a payment under the merchant's constraints.
+ *
+ * @param denoms all available denoms, used to compute refresh fees
+ */
+export function selectPayCoins(
+  denoms: DenominationRecord[],
+  cds: CoinWithDenom[],
+  paymentAmount: AmountJson,
+  depositFeeLimit: AmountJson,
+): SelectPayCoinsResult | undefined {
+  if (cds.length === 0) {
+    return undefined;
+  }
+  // Sort by ascending deposit fee and denomPub if deposit fee is the same
+  // (to guarantee deterministic results)
+  cds.sort(
+    (o1, o2) =>
+      Amounts.cmp(o1.denom.feeDeposit, o2.denom.feeDeposit) ||
+      strcmp(o1.denom.denomPub, o2.denom.denomPub),
+  );
+  const currency = cds[0].denom.value.currency;
+  const cdsResult: CoinWithDenom[] = [];
+  let accDepositFee: AmountJson = Amounts.getZero(currency);
+  let accAmount: AmountJson = Amounts.getZero(currency);
+  for (const { coin, denom } of cds) {
+    if (coin.suspended) {
+      continue;
+    }
+    if (coin.status !== CoinStatus.Fresh) {
+      continue;
+    }
+    if (Amounts.cmp(denom.feeDeposit, coin.currentAmount) >= 0) {
+      continue;
+    }
+    cdsResult.push({ coin, denom });
+    accDepositFee = Amounts.add(denom.feeDeposit, accDepositFee).amount;
+    let leftAmount = Amounts.sub(
+      coin.currentAmount,
+      Amounts.sub(paymentAmount, accAmount).amount,
+    ).amount;
+    accAmount = Amounts.add(coin.currentAmount, accAmount).amount;
+    const coversAmount = Amounts.cmp(accAmount, paymentAmount) >= 0;
+    const coversAmountWithFee =
+      Amounts.cmp(
+        accAmount,
+        Amounts.add(paymentAmount, denom.feeDeposit).amount,
+      ) >= 0;
+    const isBelowFee = Amounts.cmp(accDepositFee, depositFeeLimit) <= 0;
+
+    logger.trace("candidate coin selection", {
+      coversAmount,
+      isBelowFee,
+      accDepositFee,
+      accAmount,
+      paymentAmount,
+    });
+
+    if ((coversAmount && isBelowFee) || coversAmountWithFee) {
+      const depositFeeToCover = Amounts.sub(accDepositFee, depositFeeLimit)
+        .amount;
+      leftAmount = Amounts.sub(leftAmount, depositFeeToCover).amount;
+      logger.trace("deposit fee to cover", amountToPretty(depositFeeToCover));
+      let totalFees: AmountJson = Amounts.getZero(currency);
+      if (coversAmountWithFee && !isBelowFee) {
+        // these are the fees the customer has to pay
+        // because the merchant doesn't cover them
+        totalFees = Amounts.sub(depositFeeLimit, accDepositFee).amount;
+      }
+      totalFees = Amounts.add(
+        totalFees,
+        getTotalRefreshCost(denoms, denom, leftAmount),
+      ).amount;
+      return { cds: cdsResult, totalFees };
+    }
+  }
+  return undefined;
+}
+
+/**
+ * Get exchanges and associated coins that are still spendable, but only
+ * if the sum the coins' remaining value covers the payment amount and fees.
+ */
+async function getCoinsForPayment(
+  ws: InternalWalletState,
+  args: CoinsForPaymentArgs,
+): Promise<CoinSelectionResult | undefined> {
+  const {
+    allowedAuditors,
+    allowedExchanges,
+    depositFeeLimit,
+    paymentAmount,
+    wireFeeAmortization,
+    wireFeeLimit,
+    wireFeeTime,
+    wireMethod,
+  } = args;
+
+  let remainingAmount = paymentAmount;
+
+  const exchanges = await oneShotIter(ws.db, Stores.exchanges).toArray();
+
+  for (const exchange of exchanges) {
+    let isOkay: boolean = false;
+    const exchangeDetails = exchange.details;
+    if (!exchangeDetails) {
+      continue;
+    }
+    const exchangeFees = exchange.wireInfo;
+    if (!exchangeFees) {
+      continue;
+    }
+
+    // is the exchange explicitly allowed?
+    for (const allowedExchange of allowedExchanges) {
+      if (allowedExchange.master_pub === exchangeDetails.masterPublicKey) {
+        isOkay = true;
+        break;
+      }
+    }
+
+    // is the exchange allowed because of one of its auditors?
+    if (!isOkay) {
+      for (const allowedAuditor of allowedAuditors) {
+        for (const auditor of exchangeDetails.auditors) {
+          if (auditor.auditor_pub === allowedAuditor.auditor_pub) {
+            isOkay = true;
+            break;
+          }
+        }
+        if (isOkay) {
+          break;
+        }
+      }
+    }
+
+    if (!isOkay) {
+      continue;
+    }
+
+    const coins = await oneShotIterIndex(
+      ws.db,
+      Stores.coins.exchangeBaseUrlIndex,
+      exchange.baseUrl,
+    ).toArray();
+
+    const denoms = await oneShotIterIndex(
+      ws.db,
+      Stores.denominations.exchangeBaseUrlIndex,
+      exchange.baseUrl,
+    ).toArray();
+
+    if (!coins || coins.length === 0) {
+      continue;
+    }
+
+    // Denomination of the first coin, we assume that all other
+    // coins have the same currency
+    const firstDenom = await oneShotGet(ws.db, Stores.denominations, [
+      exchange.baseUrl,
+      coins[0].denomPub,
+    ]);
+    if (!firstDenom) {
+      throw Error("db inconsistent");
+    }
+    const currency = firstDenom.value.currency;
+    const cds: CoinWithDenom[] = [];
+    for (const coin of coins) {
+      const denom = await oneShotGet(ws.db, Stores.denominations, [
+        exchange.baseUrl,
+        coin.denomPub,
+      ]);
+      if (!denom) {
+        throw Error("db inconsistent");
+      }
+      if (denom.value.currency !== currency) {
+        console.warn(
+          `same pubkey for different currencies at exchange 
${exchange.baseUrl}`,
+        );
+        continue;
+      }
+      if (coin.suspended) {
+        continue;
+      }
+      if (coin.status !== CoinStatus.Fresh) {
+        continue;
+      }
+      cds.push({ coin, denom });
+    }
+
+    let totalFees = Amounts.getZero(currency);
+    let wireFee: AmountJson | undefined;
+    for (const fee of exchangeFees.feesForType[wireMethod] || []) {
+      if (fee.startStamp <= wireFeeTime && fee.endStamp >= wireFeeTime) {
+        wireFee = fee.wireFee;
+        break;
+      }
+    }
+
+    if (wireFee) {
+      const amortizedWireFee = Amounts.divide(wireFee, wireFeeAmortization);
+      if (Amounts.cmp(wireFeeLimit, amortizedWireFee) < 0) {
+        totalFees = Amounts.add(amortizedWireFee, totalFees).amount;
+        remainingAmount = Amounts.add(amortizedWireFee, 
remainingAmount).amount;
+      }
+    }
+
+    const res = selectPayCoins(denoms, cds, remainingAmount, depositFeeLimit);
+
+    if (res) {
+      totalFees = Amounts.add(totalFees, res.totalFees).amount;
+      return {
+        cds: res.cds,
+        exchangeUrl: exchange.baseUrl,
+        totalAmount: remainingAmount,
+        totalFees,
+      };
+    }
+  }
+  return undefined;
+}
+
+/**
+ * Record all information that is necessary to
+ * pay for a proposal in the wallet's database.
+ */
+async function recordConfirmPay(
+  ws: InternalWalletState,
+  proposal: ProposalRecord,
+  payCoinInfo: PayCoinInfo,
+  chosenExchange: string,
+): Promise<PurchaseRecord> {
+  const payReq: PayReq = {
+    coins: payCoinInfo.sigs,
+    merchant_pub: proposal.contractTerms.merchant_pub,
+    mode: "pay",
+    order_id: proposal.contractTerms.order_id,
+  };
+  const t: PurchaseRecord = {
+    abortDone: false,
+    abortRequested: false,
+    contractTerms: proposal.contractTerms,
+    contractTermsHash: proposal.contractTermsHash,
+    finished: false,
+    lastSessionId: undefined,
+    merchantSig: proposal.merchantSig,
+    payReq,
+    refundsDone: {},
+    refundsPending: {},
+    timestamp: getTimestampNow(),
+    timestamp_refund: undefined,
+  };
+
+  await runWithWriteTransaction(
+    ws.db,
+    [Stores.coins, Stores.purchases],
+    async tx => {
+      await tx.put(Stores.purchases, t);
+      for (let c of payCoinInfo.updatedCoins) {
+        await tx.put(Stores.coins, c);
+      }
+    },
+  );
+
+  ws.badge.showNotification();
+  ws.notifier.notify();
+  return t;
+}
+
+function getNextUrl(contractTerms: ContractTerms): string {
+  const fu = new URL(contractTerms.fulfillment_url)
+  fu.searchParams.set("order_id", contractTerms.order_id);
+  return fu.href;
+}
+
+export async function abortFailedPayment(
+  ws: InternalWalletState,
+  contractTermsHash: string,
+): Promise<void> {
+  const purchase = await oneShotGet(ws.db, Stores.purchases, 
contractTermsHash);
+  if (!purchase) {
+    throw Error("Purchase not found, unable to abort with refund");
+  }
+  if (purchase.finished) {
+    throw Error("Purchase already finished, not aborting");
+  }
+  if (purchase.abortDone) {
+    console.warn("abort requested on already aborted purchase");
+    return;
+  }
+
+  purchase.abortRequested = true;
+
+  // From now on, we can't retry payment anymore,
+  // so mark this in the DB in case the /pay abort
+  // does not complete on the first try.
+  await oneShotPut(ws.db, Stores.purchases, purchase);
+
+  let resp;
+
+  const abortReq = { ...purchase.payReq, mode: "abort-refund" };
+
+  const payUrl = new URL("pay", purchase.contractTerms.merchant_base_url).href;
+
+  try {
+    resp = await ws.http.postJson(payUrl, abortReq);
+  } catch (e) {
+    // Gives the user the option to retry / abort and refresh
+    console.log("aborting payment failed", e);
+    throw e;
+  }
+
+  const refundResponse = MerchantRefundResponse.checked(resp.responseJson);
+  await acceptRefundResponse(ws, refundResponse);
+
+  await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
+    const p = await tx.get(Stores.purchases, purchase.contractTermsHash);
+    if (!p) {
+      return;
+    }
+    p.abortDone = true;
+    await tx.put(Stores.purchases, p);
+  });
+}
+
+/**
+ * Download a proposal and store it in the database.
+ * Returns an id for it to retrieve it later.
+ *
+ * @param sessionId Current session ID, if the proposal is being
+ *  downloaded in the context of a session ID.
+ */
+async function downloadProposal(
+  ws: InternalWalletState,
+  url: string,
+  sessionId?: string,
+): Promise<string> {
+  const oldProposal = await oneShotGetIndexed(
+    ws.db,
+    Stores.proposals.urlIndex,
+    url,
+  );
+  if (oldProposal) {
+    return oldProposal.proposalId;
+  }
+
+  const { priv, pub } = await ws.cryptoApi.createEddsaKeypair();
+  const parsed_url = new URL(url);
+  parsed_url.searchParams.set("nonce", pub);
+  const urlWithNonce = parsed_url.href;
+  console.log("downloading contract from '" + urlWithNonce + "'");
+  let resp;
+  try {
+    resp = await ws.http.get(urlWithNonce);
+  } catch (e) {
+    console.log("contract download failed", e);
+    throw e;
+  }
+
+  const proposal = Proposal.checked(resp.responseJson);
+
+  const contractTermsHash = await ws.cryptoApi.hashString(
+    canonicalJson(proposal.contract_terms),
+  );
+
+  const proposalId = encodeCrock(getRandomBytes(32));
+
+  const proposalRecord: ProposalRecord = {
+    contractTerms: proposal.contract_terms,
+    contractTermsHash,
+    merchantSig: proposal.sig,
+    noncePriv: priv,
+    timestamp: getTimestampNow(),
+    url,
+    downloadSessionId: sessionId,
+    proposalId: proposalId,
+    proposalStatus: ProposalStatus.PROPOSED,
+  };
+  await oneShotPut(ws.db, Stores.proposals, proposalRecord);
+  ws.notifier.notify();
+
+  return proposalId;
+}
+
+async function submitPay(
+  ws: InternalWalletState,
+  contractTermsHash: string,
+  sessionId: string | undefined,
+): Promise<ConfirmPayResult> {
+  const purchase = await oneShotGet(ws.db, Stores.purchases, 
contractTermsHash);
+  if (!purchase) {
+    throw Error("Purchase not found: " + contractTermsHash);
+  }
+  if (purchase.abortRequested) {
+    throw Error("not submitting payment for aborted purchase");
+  }
+  let resp;
+  const payReq = { ...purchase.payReq, session_id: sessionId };
+
+  const payUrl = new URL("pay", purchase.contractTerms.merchant_base_url).href;
+
+  try {
+    resp = await ws.http.postJson(payUrl, payReq);
+  } catch (e) {
+    // Gives the user the option to retry / abort and refresh
+    console.log("payment failed", e);
+    throw e;
+  }
+  const merchantResp = resp.responseJson;
+  console.log("got success from pay URL");
+
+  const merchantPub = purchase.contractTerms.merchant_pub;
+  const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
+    merchantResp.sig,
+    contractTermsHash,
+    merchantPub,
+  );
+  if (!valid) {
+    console.error("merchant payment signature invalid");
+    // FIXME: properly display error
+    throw Error("merchant payment signature invalid");
+  }
+  purchase.finished = true;
+  const modifiedCoins: CoinRecord[] = [];
+  for (const pc of purchase.payReq.coins) {
+    const c = await oneShotGet(ws.db, Stores.coins, pc.coin_pub);
+    if (!c) {
+      console.error("coin not found");
+      throw Error("coin used in payment not found");
+    }
+    c.status = CoinStatus.Dirty;
+    modifiedCoins.push(c);
+  }
+
+  await runWithWriteTransaction(
+    ws.db,
+    [Stores.coins, Stores.purchases],
+    async tx => {
+      for (let c of modifiedCoins) {
+        tx.put(Stores.coins, c);
+      }
+      tx.put(Stores.purchases, purchase);
+    },
+  );
+
+  for (const c of purchase.payReq.coins) {
+    refresh(ws, c.coin_pub);
+  }
+
+  const nextUrl = getNextUrl(purchase.contractTerms);
+  ws.cachedNextUrl[purchase.contractTerms.fulfillment_url] = {
+    nextUrl,
+    lastSessionId: sessionId,
+  };
+
+  return { nextUrl };
+}
+
+/**
+ * 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.
+ */
+export async function preparePay(
+  ws: InternalWalletState,
+  talerPayUri: string,
+): Promise<PreparePayResult> {
+  const uriResult = parsePayUri(talerPayUri);
+
+  if (!uriResult) {
+    return {
+      status: "error",
+      error: "URI not supported",
+    };
+  }
+
+  let proposalId: string;
+  try {
+    proposalId = await downloadProposal(
+      ws,
+      uriResult.downloadUrl,
+      uriResult.sessionId,
+    );
+  } catch (e) {
+    return {
+      status: "error",
+      error: e.toString(),
+    };
+  }
+  const proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
+  if (!proposal) {
+    throw Error(`could not get proposal ${proposalId}`);
+  }
+
+  console.log("proposal", proposal);
+
+  const differentPurchase = await oneShotGetIndexed(
+    ws.db,
+    Stores.purchases.fulfillmentUrlIndex,
+    proposal.contractTerms.fulfillment_url,
+  );
+
+  let fulfillmentUrl = proposal.contractTerms.fulfillment_url;
+  let doublePurchaseDetection = false;
+  if (fulfillmentUrl.startsWith("http")) {
+    doublePurchaseDetection = true;
+  }
+
+  if (differentPurchase && doublePurchaseDetection) {
+    // We do this check to prevent merchant B to find out if we bought a
+    // digital product with merchant A by abusing the existing payment
+    // redirect feature.
+    if (
+      differentPurchase.contractTerms.merchant_pub !=
+      proposal.contractTerms.merchant_pub
+    ) {
+      console.warn(
+        "merchant with different public key offered contract with same 
fulfillment URL as an existing purchase",
+      );
+    } else {
+      if (uriResult.sessionId) {
+        await submitPay(
+          ws,
+          differentPurchase.contractTermsHash,
+          uriResult.sessionId,
+        );
+      }
+      return {
+        status: "paid",
+        contractTerms: differentPurchase.contractTerms,
+        nextUrl: getNextUrl(differentPurchase.contractTerms),
+      };
+    }
+  }
+
+  // First check if we already payed for it.
+  const purchase = await oneShotGet(
+    ws.db,
+    Stores.purchases,
+    proposal.contractTermsHash,
+  );
+
+  if (!purchase) {
+    const paymentAmount = Amounts.parseOrThrow(proposal.contractTerms.amount);
+    let wireFeeLimit;
+    if (proposal.contractTerms.max_wire_fee) {
+      wireFeeLimit = Amounts.parseOrThrow(proposal.contractTerms.max_wire_fee);
+    } else {
+      wireFeeLimit = Amounts.getZero(paymentAmount.currency);
+    }
+    // If not already payed, check if we could pay for it.
+    const res = await getCoinsForPayment(ws, {
+      allowedAuditors: proposal.contractTerms.auditors,
+      allowedExchanges: proposal.contractTerms.exchanges,
+      depositFeeLimit: Amounts.parseOrThrow(proposal.contractTerms.max_fee),
+      paymentAmount,
+      wireFeeAmortization: proposal.contractTerms.wire_fee_amortization || 1,
+      wireFeeLimit,
+      // FIXME: parse this properly
+      wireFeeTime: extractTalerStamp(proposal.contractTerms.timestamp) || {
+        t_ms: 0,
+      },
+      wireMethod: proposal.contractTerms.wire_method,
+    });
+
+    if (!res) {
+      console.log("not confirming payment, insufficient coins");
+      return {
+        status: "insufficient-balance",
+        contractTerms: proposal.contractTerms,
+        proposalId: proposal.proposalId,
+      };
+    }
+
+    // Only create speculative signature if we don't already have one for this 
proposal
+    if (
+      !ws.speculativePayData ||
+      (ws.speculativePayData &&
+        ws.speculativePayData.orderDownloadId !== proposalId)
+    ) {
+      const { exchangeUrl, cds, totalAmount } = res;
+      const payCoinInfo = await ws.cryptoApi.signDeposit(
+        proposal.contractTerms,
+        cds,
+        totalAmount,
+      );
+      ws.speculativePayData = {
+        exchangeUrl,
+        payCoinInfo,
+        proposal,
+        orderDownloadId: proposalId,
+      };
+      logger.trace("created speculative pay data for payment");
+    }
+
+    return {
+      status: "payment-possible",
+      contractTerms: proposal.contractTerms,
+      proposalId: proposal.proposalId,
+      totalFees: res.totalFees,
+    };
+  }
+
+  if (uriResult.sessionId) {
+    await submitPay(ws, purchase.contractTermsHash, uriResult.sessionId);
+  }
+
+  return {
+    status: "paid",
+    contractTerms: proposal.contractTerms,
+    nextUrl: getNextUrl(purchase.contractTerms),
+  };
+}
+
+/**
+ * Get the speculative pay data, but only if coins have not changed in between.
+ */
+async function getSpeculativePayData(
+  ws: InternalWalletState,
+  proposalId: string,
+): Promise<SpeculativePayData | undefined> {
+  const sp = ws.speculativePayData;
+  if (!sp) {
+    return;
+  }
+  if (sp.orderDownloadId !== proposalId) {
+    return;
+  }
+  const coinKeys = sp.payCoinInfo.updatedCoins.map(x => x.coinPub);
+  const coins: CoinRecord[] = [];
+  for (let coinKey of coinKeys) {
+    const cc = await oneShotGet(ws.db, Stores.coins, coinKey);
+    if (cc) {
+      coins.push(cc);
+    }
+  }
+  for (let i = 0; i < coins.length; i++) {
+    const specCoin = sp.payCoinInfo.originalCoins[i];
+    const currentCoin = coins[i];
+
+    // Coin does not exist anymore!
+    if (!currentCoin) {
+      return;
+    }
+    if (Amounts.cmp(specCoin.currentAmount, currentCoin.currentAmount) !== 0) {
+      return;
+    }
+  }
+  return sp;
+}
+
+/**
+ * Add a contract to the wallet and sign coins, and send them.
+ */
+export async function confirmPay(
+  ws: InternalWalletState,
+  proposalId: string,
+  sessionIdOverride: string | undefined,
+): Promise<ConfirmPayResult> {
+  logger.trace(
+    `executing confirmPay with proposalId ${proposalId} and sessionIdOverride 
${sessionIdOverride}`,
+  );
+  const proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
+
+  if (!proposal) {
+    throw Error(`proposal with id ${proposalId} not found`);
+  }
+
+  const sessionId = sessionIdOverride || proposal.downloadSessionId;
+
+  let purchase = await oneShotGet(
+    ws.db,
+    Stores.purchases,
+    proposal.contractTermsHash,
+  );
+
+  if (purchase) {
+    return submitPay(ws, purchase.contractTermsHash, sessionId);
+  }
+
+  const contractAmount = Amounts.parseOrThrow(proposal.contractTerms.amount);
+
+  let wireFeeLimit;
+  if (!proposal.contractTerms.max_wire_fee) {
+    wireFeeLimit = Amounts.getZero(contractAmount.currency);
+  } else {
+    wireFeeLimit = Amounts.parseOrThrow(proposal.contractTerms.max_wire_fee);
+  }
+
+  const res = await getCoinsForPayment(ws, {
+    allowedAuditors: proposal.contractTerms.auditors,
+    allowedExchanges: proposal.contractTerms.exchanges,
+    depositFeeLimit: Amounts.parseOrThrow(proposal.contractTerms.max_fee),
+    paymentAmount: Amounts.parseOrThrow(proposal.contractTerms.amount),
+    wireFeeAmortization: proposal.contractTerms.wire_fee_amortization || 1,
+    wireFeeLimit,
+    // FIXME: parse this properly
+    wireFeeTime: extractTalerStamp(proposal.contractTerms.timestamp) || {
+      t_ms: 0,
+    },
+    wireMethod: proposal.contractTerms.wire_method,
+  });
+
+  logger.trace("coin selection result", res);
+
+  if (!res) {
+    // Should not happen, since checkPay should be called first
+    console.log("not confirming payment, insufficient coins");
+    throw Error("insufficient balance");
+  }
+
+  const sd = await getSpeculativePayData(ws, proposalId);
+  if (!sd) {
+    const { exchangeUrl, cds, totalAmount } = res;
+    const payCoinInfo = await ws.cryptoApi.signDeposit(
+      proposal.contractTerms,
+      cds,
+      totalAmount,
+    );
+    purchase = await recordConfirmPay(ws, proposal, payCoinInfo, exchangeUrl);
+  } else {
+    purchase = await recordConfirmPay(
+      ws,
+      sd.proposal,
+      sd.payCoinInfo,
+      sd.exchangeUrl,
+    );
+  }
+
+  return submitPay(ws, purchase.contractTermsHash, sessionId);
+}
diff --git a/src/wallet-impl/payback.ts b/src/wallet-impl/payback.ts
new file mode 100644
index 00000000..5bf5ff06
--- /dev/null
+++ b/src/wallet-impl/payback.ts
@@ -0,0 +1,88 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+  oneShotIter,
+  runWithWriteTransaction,
+  oneShotGet,
+  oneShotPut,
+} from "../util/query";
+import { InternalWalletState } from "./state";
+import { Stores, TipRecord, CoinStatus } from "../dbTypes";
+
+import { Logger } from "../util/logging";
+import { PaybackConfirmation } from "../talerTypes";
+import { updateExchangeFromUrl } from "./exchanges";
+
+const logger = new Logger("payback.ts");
+
+export async function payback(
+  ws: InternalWalletState,
+  coinPub: string,
+): Promise<void> {
+  let coin = await oneShotGet(ws.db, Stores.coins, coinPub);
+  if (!coin) {
+    throw Error(`Coin ${coinPub} not found, can't request payback`);
+  }
+  const reservePub = coin.reservePub;
+  if (!reservePub) {
+    throw Error(`Can't request payback for a refreshed coin`);
+  }
+  const reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
+  if (!reserve) {
+    throw Error(`Reserve of coin ${coinPub} not found`);
+  }
+  switch (coin.status) {
+    case CoinStatus.Dormant:
+      throw Error(`Can't do payback for coin ${coinPub} since it's dormant`);
+  }
+  coin.status = CoinStatus.Dormant;
+  // Even if we didn't get the payback yet, we suspend withdrawal, since
+  // technically we might update reserve status before we get the response
+  // from the reserve for the payback request.
+  reserve.hasPayback = true;
+  await runWithWriteTransaction(
+    ws.db,
+    [Stores.coins, Stores.reserves],
+    async tx => {
+      await tx.put(Stores.coins, coin!!);
+      await tx.put(Stores.reserves, reserve);
+    },
+  );
+  ws.notifier.notify();
+
+  const paybackRequest = await ws.cryptoApi.createPaybackRequest(coin);
+  const reqUrl = new URL("payback", coin.exchangeBaseUrl);
+  const resp = await ws.http.postJson(reqUrl.href, paybackRequest);
+  if (resp.status !== 200) {
+    throw Error();
+  }
+  const paybackConfirmation = PaybackConfirmation.checked(resp.responseJson);
+  if (paybackConfirmation.reserve_pub !== coin.reservePub) {
+    throw Error(`Coin's reserve doesn't match reserve on payback`);
+  }
+  coin = await oneShotGet(ws.db, Stores.coins, coinPub);
+  if (!coin) {
+    throw Error(`Coin ${coinPub} not found, can't confirm payback`);
+  }
+  coin.status = CoinStatus.Dormant;
+  await oneShotPut(ws.db, Stores.coins, coin);
+  ws.notifier.notify();
+  await updateExchangeFromUrl(ws, coin.exchangeBaseUrl, true);
+}
diff --git a/src/wallet-impl/pending.ts b/src/wallet-impl/pending.ts
new file mode 100644
index 00000000..a66571a3
--- /dev/null
+++ b/src/wallet-impl/pending.ts
@@ -0,0 +1,208 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+ /**
+  * Imports.
+  */
+import { PendingOperationInfo, PendingOperationsResponse } from 
"../walletTypes";
+import { oneShotIter } from "../util/query";
+import { InternalWalletState } from "./state";
+import { Stores, ExchangeUpdateStatus, ReserveRecordStatus, CoinStatus, 
ProposalStatus } from "../dbTypes";
+
+export async function getPendingOperations(
+  ws: InternalWalletState,
+): Promise<PendingOperationsResponse> {
+  const pendingOperations: PendingOperationInfo[] = [];
+  const exchanges = await oneShotIter(ws.db, Stores.exchanges).toArray();
+  for (let e of exchanges) {
+    switch (e.updateStatus) {
+      case ExchangeUpdateStatus.FINISHED:
+        if (e.lastError) {
+          pendingOperations.push({
+            type: "bug",
+            message:
+              "Exchange record is in FINISHED state but has lastError set",
+            details: {
+              exchangeBaseUrl: e.baseUrl,
+            },
+          });
+        }
+        if (!e.details) {
+          pendingOperations.push({
+            type: "bug",
+            message:
+              "Exchange record does not have details, but no update in 
progress.",
+            details: {
+              exchangeBaseUrl: e.baseUrl,
+            },
+          });
+        }
+        if (!e.wireInfo) {
+          pendingOperations.push({
+            type: "bug",
+            message:
+              "Exchange record does not have wire info, but no update in 
progress.",
+            details: {
+              exchangeBaseUrl: e.baseUrl,
+            },
+          });
+        }
+        break;
+      case ExchangeUpdateStatus.FETCH_KEYS:
+        pendingOperations.push({
+          type: "exchange-update",
+          stage: "fetch-keys",
+          exchangeBaseUrl: e.baseUrl,
+          lastError: e.lastError,
+          reason: e.updateReason || "unknown",
+        });
+        break;
+      case ExchangeUpdateStatus.FETCH_WIRE:
+        pendingOperations.push({
+          type: "exchange-update",
+          stage: "fetch-wire",
+          exchangeBaseUrl: e.baseUrl,
+          lastError: e.lastError,
+          reason: e.updateReason || "unknown",
+        });
+        break;
+      default:
+        pendingOperations.push({
+          type: "bug",
+          message: "Unknown exchangeUpdateStatus",
+          details: {
+            exchangeBaseUrl: e.baseUrl,
+            exchangeUpdateStatus: e.updateStatus,
+          },
+        });
+        break;
+    }
+  }
+  await oneShotIter(ws.db, Stores.reserves).forEach(reserve => {
+    const reserveType = reserve.bankWithdrawStatusUrl
+      ? "taler-bank"
+      : "manual";
+    switch (reserve.reserveStatus) {
+      case ReserveRecordStatus.DORMANT:
+        // nothing to report as pending
+        break;
+      case ReserveRecordStatus.WITHDRAWING:
+      case ReserveRecordStatus.UNCONFIRMED:
+      case ReserveRecordStatus.QUERYING_STATUS:
+      case ReserveRecordStatus.REGISTERING_BANK:
+        pendingOperations.push({
+          type: "reserve",
+          stage: reserve.reserveStatus,
+          timestampCreated: reserve.created,
+          reserveType,
+          reservePub: reserve.reservePub,
+        });
+        break;
+      case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+        pendingOperations.push({
+          type: "reserve",
+          stage: reserve.reserveStatus,
+          timestampCreated: reserve.created,
+          reserveType,
+          reservePub: reserve.reservePub,
+          bankWithdrawConfirmUrl: reserve.bankWithdrawConfirmUrl,
+        });
+        break;
+      default:
+        pendingOperations.push({
+          type: "bug",
+          message: "Unknown reserve record status",
+          details: {
+            reservePub: reserve.reservePub,
+            reserveStatus: reserve.reserveStatus,
+          },
+        });
+        break;
+    }
+  });
+
+  await oneShotIter(ws.db, Stores.refresh).forEach(r => {
+    if (r.finished) {
+      return;
+    }
+    let refreshStatus: string;
+    if (r.norevealIndex === undefined) {
+      refreshStatus = "melt";
+    } else {
+      refreshStatus = "reveal";
+    }
+
+    pendingOperations.push({
+      type: "refresh",
+      oldCoinPub: r.meltCoinPub,
+      refreshStatus,
+      refreshOutputSize: r.newDenoms.length,
+      refreshSessionId: r.refreshSessionId,
+    });
+  });
+
+  await oneShotIter(ws.db, Stores.coins).forEach(coin => {
+    if (coin.status == CoinStatus.Dirty) {
+      pendingOperations.push({
+        type: "dirty-coin",
+        coinPub: coin.coinPub,
+      });
+    }
+  });
+
+  await oneShotIter(ws.db, Stores.withdrawalSession).forEach(ws => {
+    const numCoinsWithdrawn = ws.withdrawn.reduce(
+      (a, x) => a + (x ? 1 : 0),
+      0,
+    );
+    const numCoinsTotal = ws.withdrawn.length;
+    if (numCoinsWithdrawn < numCoinsTotal) {
+      pendingOperations.push({
+        type: "withdraw",
+        numCoinsTotal,
+        numCoinsWithdrawn,
+        source: ws.source,
+        withdrawSessionId: ws.withdrawSessionId,
+      });
+    }
+  });
+
+  await oneShotIter(ws.db, Stores.proposals).forEach(proposal => {
+    if (proposal.proposalStatus == ProposalStatus.PROPOSED) {
+      pendingOperations.push({
+        type: "proposal",
+        merchantBaseUrl: proposal.contractTerms.merchant_base_url,
+        proposalId: proposal.proposalId,
+        proposalTimestamp: proposal.timestamp,
+      });
+    }
+  });
+
+  await oneShotIter(ws.db, Stores.tips).forEach(tip => {
+    if (tip.accepted && !tip.pickedUp) {
+      pendingOperations.push({
+        type: "tip",
+        merchantBaseUrl: tip.merchantBaseUrl,
+        tipId: tip.tipId,
+        merchantTipId: tip.merchantTipId,
+      });
+    }
+  });
+
+  return {
+    pendingOperations,
+  };
+}
diff --git a/src/wallet-impl/refresh.ts b/src/wallet-impl/refresh.ts
new file mode 100644
index 00000000..7e7270ed
--- /dev/null
+++ b/src/wallet-impl/refresh.ts
@@ -0,0 +1,416 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { AmountJson } from "../util/amounts";
+import * as Amounts from "../util/amounts";
+import {
+  DenominationRecord,
+  Stores,
+  CoinStatus,
+  RefreshPlanchetRecord,
+  CoinRecord,
+  RefreshSessionRecord,
+} from "../dbTypes";
+import { amountToPretty } from "../util/helpers";
+import {
+  oneShotGet,
+  oneShotMutate,
+  runWithWriteTransaction,
+  TransactionAbort,
+  oneShotIterIndex,
+} from "../util/query";
+import { InternalWalletState } from "./state";
+import { Logger } from "../util/logging";
+import { getWithdrawDenomList } from "./withdraw";
+import { updateExchangeFromUrl } from "./exchanges";
+
+const logger = new Logger("refresh.ts");
+
+/**
+ * Get the amount that we lose when refreshing a coin of the given denomination
+ * with a certain amount left.
+ *
+ * If the amount left is zero, then the refresh cost
+ * is also considered to be zero.  If a refresh isn't possible (e.g. due to 
lack of
+ * the right denominations), then the cost is the full amount left.
+ *
+ * Considers refresh fees, withdrawal fees after refresh and amounts too small
+ * to refresh.
+ */
+export function getTotalRefreshCost(
+  denoms: DenominationRecord[],
+  refreshedDenom: DenominationRecord,
+  amountLeft: AmountJson,
+): AmountJson {
+  const withdrawAmount = Amounts.sub(amountLeft, refreshedDenom.feeRefresh)
+    .amount;
+  const withdrawDenoms = getWithdrawDenomList(withdrawAmount, denoms);
+  const resultingAmount = Amounts.add(
+    Amounts.getZero(withdrawAmount.currency),
+    ...withdrawDenoms.map(d => d.value),
+  ).amount;
+  const totalCost = Amounts.sub(amountLeft, resultingAmount).amount;
+  logger.trace(
+    "total refresh cost for",
+    amountToPretty(amountLeft),
+    "is",
+    amountToPretty(totalCost),
+  );
+  return totalCost;
+}
+
+async function refreshMelt(
+  ws: InternalWalletState,
+  refreshSessionId: string,
+): Promise<void> {
+  const refreshSession = await oneShotGet(
+    ws.db,
+    Stores.refresh,
+    refreshSessionId,
+  );
+  if (!refreshSession) {
+    return;
+  }
+  if (refreshSession.norevealIndex !== undefined) {
+    return;
+  }
+
+  const coin = await oneShotGet(
+    ws.db,
+    Stores.coins,
+    refreshSession.meltCoinPub,
+  );
+
+  if (!coin) {
+    console.error("can't melt coin, it does not exist");
+    return;
+  }
+
+  const reqUrl = new URL("refresh/melt", refreshSession.exchangeBaseUrl);
+  const meltReq = {
+    coin_pub: coin.coinPub,
+    confirm_sig: refreshSession.confirmSig,
+    denom_pub_hash: coin.denomPubHash,
+    denom_sig: coin.denomSig,
+    rc: refreshSession.hash,
+    value_with_fee: refreshSession.valueWithFee,
+  };
+  logger.trace("melt request:", meltReq);
+  const resp = await ws.http.postJson(reqUrl.href, meltReq);
+
+  logger.trace("melt response:", resp.responseJson);
+
+  if (resp.status !== 200) {
+    console.error(resp.responseJson);
+    throw Error("refresh failed");
+  }
+
+  const respJson = resp.responseJson;
+
+  const norevealIndex = respJson.noreveal_index;
+
+  if (typeof norevealIndex !== "number") {
+    throw Error("invalid response");
+  }
+
+  refreshSession.norevealIndex = norevealIndex;
+
+  await oneShotMutate(ws.db, Stores.refresh, refreshSessionId, rs => {
+    if (rs.norevealIndex !== undefined) {
+      return;
+    }
+    if (rs.finished) {
+      return;
+    }
+    rs.norevealIndex = norevealIndex;
+    return rs;
+  });
+
+  ws.notifier.notify();
+}
+
+async function refreshReveal(
+  ws: InternalWalletState,
+  refreshSessionId: string,
+): Promise<void> {
+  const refreshSession = await oneShotGet(
+    ws.db,
+    Stores.refresh,
+    refreshSessionId,
+  );
+  if (!refreshSession) {
+    return;
+  }
+  const norevealIndex = refreshSession.norevealIndex;
+  if (norevealIndex === undefined) {
+    throw Error("can't reveal without melting first");
+  }
+  const privs = Array.from(refreshSession.transferPrivs);
+  privs.splice(norevealIndex, 1);
+
+  const planchets = refreshSession.planchetsForGammas[norevealIndex];
+  if (!planchets) {
+    throw Error("refresh index error");
+  }
+
+  const meltCoinRecord = await oneShotGet(
+    ws.db,
+    Stores.coins,
+    refreshSession.meltCoinPub,
+  );
+  if (!meltCoinRecord) {
+    throw Error("inconsistent database");
+  }
+
+  const evs = planchets.map((x: RefreshPlanchetRecord) => x.coinEv);
+
+  const linkSigs: string[] = [];
+  for (let i = 0; i < refreshSession.newDenoms.length; i++) {
+    const linkSig = await ws.cryptoApi.signCoinLink(
+      meltCoinRecord.coinPriv,
+      refreshSession.newDenomHashes[i],
+      refreshSession.meltCoinPub,
+      refreshSession.transferPubs[norevealIndex],
+      planchets[i].coinEv,
+    );
+    linkSigs.push(linkSig);
+  }
+
+  const req = {
+    coin_evs: evs,
+    new_denoms_h: refreshSession.newDenomHashes,
+    rc: refreshSession.hash,
+    transfer_privs: privs,
+    transfer_pub: refreshSession.transferPubs[norevealIndex],
+    link_sigs: linkSigs,
+  };
+
+  const reqUrl = new URL("refresh/reveal", refreshSession.exchangeBaseUrl);
+  logger.trace("reveal request:", req);
+
+  let resp;
+  try {
+    resp = await ws.http.postJson(reqUrl.href, req);
+  } catch (e) {
+    console.error("got error during /refresh/reveal request");
+    console.error(e);
+    return;
+  }
+
+  logger.trace("session:", refreshSession);
+  logger.trace("reveal response:", resp);
+
+  if (resp.status !== 200) {
+    console.error("error: /refresh/reveal returned status " + resp.status);
+    return;
+  }
+
+  const respJson = resp.responseJson;
+
+  if (!respJson.ev_sigs || !Array.isArray(respJson.ev_sigs)) {
+    console.error("/refresh/reveal did not contain ev_sigs");
+    return;
+  }
+
+  const exchange = oneShotGet(
+    ws.db,
+    Stores.exchanges,
+    refreshSession.exchangeBaseUrl,
+  );
+  if (!exchange) {
+    console.error(`exchange ${refreshSession.exchangeBaseUrl} not found`);
+    return;
+  }
+
+  const coins: CoinRecord[] = [];
+
+  for (let i = 0; i < respJson.ev_sigs.length; i++) {
+    const denom = await oneShotGet(ws.db, Stores.denominations, [
+      refreshSession.exchangeBaseUrl,
+      refreshSession.newDenoms[i],
+    ]);
+    if (!denom) {
+      console.error("denom not found");
+      continue;
+    }
+    const pc =
+      refreshSession.planchetsForGammas[refreshSession.norevealIndex!][i];
+    const denomSig = await ws.cryptoApi.rsaUnblind(
+      respJson.ev_sigs[i].ev_sig,
+      pc.blindingKey,
+      denom.denomPub,
+    );
+    const coin: CoinRecord = {
+      blindingKey: pc.blindingKey,
+      coinPriv: pc.privateKey,
+      coinPub: pc.publicKey,
+      currentAmount: denom.value,
+      denomPub: denom.denomPub,
+      denomPubHash: denom.denomPubHash,
+      denomSig,
+      exchangeBaseUrl: refreshSession.exchangeBaseUrl,
+      reservePub: undefined,
+      status: CoinStatus.Fresh,
+      coinIndex: -1,
+      withdrawSessionId: "",
+    };
+
+    coins.push(coin);
+  }
+
+  refreshSession.finished = true;
+
+  await runWithWriteTransaction(
+    ws.db,
+    [Stores.coins, Stores.refresh],
+    async tx => {
+      const rs = await tx.get(Stores.refresh, refreshSessionId);
+      if (!rs) {
+        return;
+      }
+      if (rs.finished) {
+        return;
+      }
+      for (let coin of coins) {
+        await tx.put(Stores.coins, coin);
+      }
+      await tx.put(Stores.refresh, refreshSession);
+    },
+  );
+  ws.notifier.notify();
+}
+
+export async function processRefreshSession(
+  ws: InternalWalletState,
+  refreshSessionId: string,
+) {
+  const refreshSession = await oneShotGet(
+    ws.db,
+    Stores.refresh,
+    refreshSessionId,
+  );
+  if (!refreshSession) {
+    return;
+  }
+  if (refreshSession.finished) {
+    return;
+  }
+  if (typeof refreshSession.norevealIndex !== "number") {
+    await refreshMelt(ws, refreshSession.refreshSessionId);
+  }
+  await refreshReveal(ws, refreshSession.refreshSessionId);
+  logger.trace("refresh finished");
+}
+
+export async function refresh(
+  ws: InternalWalletState,
+  oldCoinPub: string,
+  force: boolean = false,
+): Promise<void> {
+  const coin = await oneShotGet(ws.db, Stores.coins, oldCoinPub);
+  if (!coin) {
+    console.warn("can't refresh, coin not in database");
+    return;
+  }
+  switch (coin.status) {
+    case CoinStatus.Dirty:
+      break;
+    case CoinStatus.Dormant:
+      return;
+    case CoinStatus.Fresh:
+      if (!force) {
+        return;
+      }
+      break;
+  }
+
+  const exchange = await updateExchangeFromUrl(ws, coin.exchangeBaseUrl);
+  if (!exchange) {
+    throw Error("db inconsistent: exchange of coin not found");
+  }
+
+  const oldDenom = await oneShotGet(ws.db, Stores.denominations, [
+    exchange.baseUrl,
+    coin.denomPub,
+  ]);
+
+  if (!oldDenom) {
+    throw Error("db inconsistent: denomination for coin not found");
+  }
+
+  const availableDenoms: DenominationRecord[] = await oneShotIterIndex(
+    ws.db,
+    Stores.denominations.exchangeBaseUrlIndex,
+    exchange.baseUrl,
+  ).toArray();
+
+  const availableAmount = Amounts.sub(coin.currentAmount, oldDenom.feeRefresh)
+    .amount;
+
+  const newCoinDenoms = getWithdrawDenomList(availableAmount, availableDenoms);
+
+  if (newCoinDenoms.length === 0) {
+    logger.trace(
+      `not refreshing, available amount ${amountToPretty(
+        availableAmount,
+      )} too small`,
+    );
+    await oneShotMutate(ws.db, Stores.coins, oldCoinPub, x => {
+      if (x.status != coin.status) {
+        // Concurrent modification?
+        return;
+      }
+      x.status = CoinStatus.Dormant;
+      return x;
+    });
+    ws.notifier.notify();
+    return;
+  }
+
+  const refreshSession: RefreshSessionRecord = await 
ws.cryptoApi.createRefreshSession(
+    exchange.baseUrl,
+    3,
+    coin,
+    newCoinDenoms,
+    oldDenom.feeRefresh,
+  );
+
+  function mutateCoin(c: CoinRecord): CoinRecord {
+    const r = Amounts.sub(c.currentAmount, refreshSession.valueWithFee);
+    if (r.saturated) {
+      // Something else must have written the coin value
+      throw TransactionAbort;
+    }
+    c.currentAmount = r.amount;
+    c.status = CoinStatus.Dormant;
+    return c;
+  }
+
+  // Store refresh session and subtract refreshed amount from
+  // coin in the same transaction.
+  await runWithWriteTransaction(
+    ws.db,
+    [Stores.refresh, Stores.coins],
+    async tx => {
+      await tx.put(Stores.refresh, refreshSession);
+      await tx.mutate(Stores.coins, coin.coinPub, mutateCoin);
+    },
+  );
+  logger.info(`created refresh session ${refreshSession.refreshSessionId}`);
+  ws.notifier.notify();
+
+  await processRefreshSession(ws, refreshSession.refreshSessionId);
+}
diff --git a/src/wallet-impl/refund.ts b/src/wallet-impl/refund.ts
new file mode 100644
index 00000000..2a9dea14
--- /dev/null
+++ b/src/wallet-impl/refund.ts
@@ -0,0 +1,245 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+  MerchantRefundResponse,
+  RefundRequest,
+  MerchantRefundPermission,
+} from "../talerTypes";
+import { PurchaseRecord, Stores, CoinRecord, CoinStatus } from "../dbTypes";
+import { getTimestampNow } from "../walletTypes";
+import {
+  oneShotMutate,
+  oneShotGet,
+  runWithWriteTransaction,
+  oneShotIterIndex,
+} from "../util/query";
+import { InternalWalletState } from "./state";
+import { parseRefundUri } from "../util/taleruri";
+import { Logger } from "../util/logging";
+import { AmountJson } from "../util/amounts";
+import * as Amounts from "../util/amounts";
+import { getTotalRefreshCost, refresh } from "./refresh";
+
+const logger = new Logger("refund.ts");
+
+export async function getFullRefundFees(
+  ws: InternalWalletState,
+  refundPermissions: MerchantRefundPermission[],
+): Promise<AmountJson> {
+  if (refundPermissions.length === 0) {
+    throw Error("no refunds given");
+  }
+  const coin0 = await oneShotGet(
+    ws.db,
+    Stores.coins,
+    refundPermissions[0].coin_pub,
+  );
+  if (!coin0) {
+    throw Error("coin not found");
+  }
+  let feeAcc = Amounts.getZero(
+    Amounts.parseOrThrow(refundPermissions[0].refund_amount).currency,
+  );
+
+  const denoms = await oneShotIterIndex(
+    ws.db,
+    Stores.denominations.exchangeBaseUrlIndex,
+    coin0.exchangeBaseUrl,
+  ).toArray();
+
+  for (const rp of refundPermissions) {
+    const coin = await oneShotGet(ws.db, Stores.coins, rp.coin_pub);
+    if (!coin) {
+      throw Error("coin not found");
+    }
+    const denom = await oneShotGet(ws.db, Stores.denominations, [
+      coin0.exchangeBaseUrl,
+      coin.denomPub,
+    ]);
+    if (!denom) {
+      throw Error(`denom not found (${coin.denomPub})`);
+    }
+    // FIXME:  this assumes that the refund already happened.
+    // When it hasn't, the refresh cost is inaccurate.  To fix this,
+    // we need introduce a flag to tell if a coin was refunded or
+    // refreshed normally (and what about incremental refunds?)
+    const refundAmount = Amounts.parseOrThrow(rp.refund_amount);
+    const refundFee = Amounts.parseOrThrow(rp.refund_fee);
+    const refreshCost = getTotalRefreshCost(
+      denoms,
+      denom,
+      Amounts.sub(refundAmount, refundFee).amount,
+    );
+    feeAcc = Amounts.add(feeAcc, refreshCost, refundFee).amount;
+  }
+  return feeAcc;
+}
+
+async function submitRefunds(
+  ws: InternalWalletState,
+  contractTermsHash: string,
+): Promise<void> {
+  const purchase = await oneShotGet(ws.db, Stores.purchases, 
contractTermsHash);
+  if (!purchase) {
+    console.error(
+      "not submitting refunds, contract terms not found:",
+      contractTermsHash,
+    );
+    return;
+  }
+  const pendingKeys = Object.keys(purchase.refundsPending);
+  if (pendingKeys.length === 0) {
+    return;
+  }
+  for (const pk of pendingKeys) {
+    const perm = purchase.refundsPending[pk];
+    const req: RefundRequest = {
+      coin_pub: perm.coin_pub,
+      h_contract_terms: purchase.contractTermsHash,
+      merchant_pub: purchase.contractTerms.merchant_pub,
+      merchant_sig: perm.merchant_sig,
+      refund_amount: perm.refund_amount,
+      refund_fee: perm.refund_fee,
+      rtransaction_id: perm.rtransaction_id,
+    };
+    console.log("sending refund permission", perm);
+    // FIXME: not correct once we support multiple exchanges per payment
+    const exchangeUrl = purchase.payReq.coins[0].exchange_url;
+    const reqUrl = new URL("refund", exchangeUrl);
+    const resp = await ws.http.postJson(reqUrl.href, req);
+    if (resp.status !== 200) {
+      console.error("refund failed", resp);
+      continue;
+    }
+
+    // Transactionally mark successful refunds as done
+    const transformPurchase = (
+      t: PurchaseRecord | undefined,
+    ): PurchaseRecord | undefined => {
+      if (!t) {
+        console.warn("purchase not found, not updating refund");
+        return;
+      }
+      if (t.refundsPending[pk]) {
+        t.refundsDone[pk] = t.refundsPending[pk];
+        delete t.refundsPending[pk];
+      }
+      return t;
+    };
+    const transformCoin = (
+      c: CoinRecord | undefined,
+    ): CoinRecord | undefined => {
+      if (!c) {
+        console.warn("coin not found, can't apply refund");
+        return;
+      }
+      const refundAmount = Amounts.parseOrThrow(perm.refund_amount);
+      const refundFee = Amounts.parseOrThrow(perm.refund_fee);
+      c.status = CoinStatus.Dirty;
+      c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
+      c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
+
+      return c;
+    };
+
+    await runWithWriteTransaction(
+      ws.db,
+      [Stores.purchases, Stores.coins],
+      async tx => {
+        await tx.mutate(Stores.purchases, contractTermsHash, 
transformPurchase);
+        await tx.mutate(Stores.coins, perm.coin_pub, transformCoin);
+      },
+    );
+    refresh(ws, perm.coin_pub);
+  }
+
+  ws.badge.showNotification();
+  ws.notifier.notify();
+}
+
+export async function acceptRefundResponse(
+  ws: InternalWalletState,
+  refundResponse: MerchantRefundResponse,
+): Promise<string> {
+  const refundPermissions = refundResponse.refund_permissions;
+
+  if (!refundPermissions.length) {
+    console.warn("got empty refund list");
+    throw Error("empty refund");
+  }
+
+  /**
+   * Add refund to purchase if not already added.
+   */
+  function f(t: PurchaseRecord | undefined): PurchaseRecord | undefined {
+    if (!t) {
+      console.error("purchase not found, not adding refunds");
+      return;
+    }
+
+    t.timestamp_refund = getTimestampNow();
+
+    for (const perm of refundPermissions) {
+      if (
+        !t.refundsPending[perm.merchant_sig] &&
+        !t.refundsDone[perm.merchant_sig]
+      ) {
+        t.refundsPending[perm.merchant_sig] = perm;
+      }
+    }
+    return t;
+  }
+
+  const hc = refundResponse.h_contract_terms;
+
+  // Add the refund permissions to the purchase within a DB transaction
+  await oneShotMutate(ws.db, Stores.purchases, hc, f);
+  ws.notifier.notify();
+
+  await submitRefunds(ws, hc);
+
+  return hc;
+}
+
+/**
+ * Accept a refund, return the contract hash for the contract
+ * that was involved in the refund.
+ */
+export async function applyRefund(
+  ws: InternalWalletState,
+  talerRefundUri: string,
+): Promise<string> {
+  const parseResult = parseRefundUri(talerRefundUri);
+
+  if (!parseResult) {
+    throw Error("invalid refund URI");
+  }
+
+  const refundUrl = parseResult.refundUrl;
+
+  logger.trace("processing refund");
+  let resp;
+  try {
+    resp = await ws.http.get(refundUrl);
+  } catch (e) {
+    console.error("error downloading refund permission", e);
+    throw e;
+  }
+
+  const refundResponse = MerchantRefundResponse.checked(resp.responseJson);
+  return acceptRefundResponse(ws, refundResponse);
+}
diff --git a/src/wallet-impl/reserves.ts b/src/wallet-impl/reserves.ts
new file mode 100644
index 00000000..265eddce
--- /dev/null
+++ b/src/wallet-impl/reserves.ts
@@ -0,0 +1,567 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+  CreateReserveRequest,
+  CreateReserveResponse,
+  getTimestampNow,
+  ConfirmReserveRequest,
+  OperationError,
+} from "../walletTypes";
+import { canonicalizeBaseUrl } from "../util/helpers";
+import { InternalWalletState } from "./state";
+import {
+  ReserveRecordStatus,
+  ReserveRecord,
+  CurrencyRecord,
+  Stores,
+  WithdrawalSessionRecord,
+} from "../dbTypes";
+import {
+  oneShotMutate,
+  oneShotPut,
+  oneShotGet,
+  runWithWriteTransaction,
+  TransactionAbort,
+} from "../util/query";
+import { Logger } from "../util/logging";
+import * as Amounts from "../util/amounts";
+import { updateExchangeFromUrl, getExchangeTrust } from "./exchanges";
+import { WithdrawOperationStatusResponse, ReserveStatus } from "../talerTypes";
+import { assertUnreachable } from "../util/assertUnreachable";
+import { OperationFailedAndReportedError } from "../wallet";
+import { encodeCrock } from "../crypto/talerCrypto";
+import { randomBytes } from "../crypto/primitives/nacl-fast";
+import {
+  getVerifiedWithdrawDenomList,
+  processWithdrawSession,
+} from "./withdraw";
+
+const logger = new Logger("reserves.ts");
+
+/**
+ * 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.
+ */
+export async function createReserve(
+  ws: InternalWalletState,
+  req: CreateReserveRequest,
+): Promise<CreateReserveResponse> {
+  const keypair = await ws.cryptoApi.createEddsaKeypair();
+  const now = getTimestampNow();
+  const canonExchange = canonicalizeBaseUrl(req.exchange);
+
+  let reserveStatus;
+  if (req.bankWithdrawStatusUrl) {
+    reserveStatus = ReserveRecordStatus.REGISTERING_BANK;
+  } else {
+    reserveStatus = ReserveRecordStatus.UNCONFIRMED;
+  }
+
+  const currency = req.amount.currency;
+
+  const reserveRecord: ReserveRecord = {
+    created: now,
+    withdrawAllocatedAmount: Amounts.getZero(currency),
+    withdrawCompletedAmount: Amounts.getZero(currency),
+    withdrawRemainingAmount: Amounts.getZero(currency),
+    exchangeBaseUrl: canonExchange,
+    hasPayback: false,
+    initiallyRequestedAmount: req.amount,
+    reservePriv: keypair.priv,
+    reservePub: keypair.pub,
+    senderWire: req.senderWire,
+    timestampConfirmed: undefined,
+    timestampReserveInfoPosted: undefined,
+    bankWithdrawStatusUrl: req.bankWithdrawStatusUrl,
+    exchangeWire: req.exchangeWire,
+    reserveStatus,
+    lastStatusQuery: undefined,
+  };
+
+  const senderWire = req.senderWire;
+  if (senderWire) {
+    const rec = {
+      paytoUri: senderWire,
+    };
+    await oneShotPut(ws.db, Stores.senderWires, rec);
+  }
+
+  const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange);
+  const exchangeDetails = exchangeInfo.details;
+  if (!exchangeDetails) {
+    throw Error("exchange not updated");
+  }
+  const { isAudited, isTrusted } = await getExchangeTrust(ws, exchangeInfo);
+  let currencyRecord = await oneShotGet(
+    ws.db,
+    Stores.currencies,
+    exchangeDetails.currency,
+  );
+  if (!currencyRecord) {
+    currencyRecord = {
+      auditors: [],
+      exchanges: [],
+      fractionalDigits: 2,
+      name: exchangeDetails.currency,
+    };
+  }
+
+  if (!isAudited && !isTrusted) {
+    currencyRecord.exchanges.push({
+      baseUrl: req.exchange,
+      exchangePub: exchangeDetails.masterPublicKey,
+    });
+  }
+
+  const cr: CurrencyRecord = currencyRecord;
+
+  const resp = await runWithWriteTransaction(
+    ws.db,
+    [Stores.currencies, Stores.reserves, Stores.bankWithdrawUris],
+    async tx => {
+      // Check if we have already created a reserve for that 
bankWithdrawStatusUrl
+      if (reserveRecord.bankWithdrawStatusUrl) {
+        const bwi = await tx.get(
+          Stores.bankWithdrawUris,
+          reserveRecord.bankWithdrawStatusUrl,
+        );
+        if (bwi) {
+          const otherReserve = await tx.get(Stores.reserves, bwi.reservePub);
+          if (otherReserve) {
+            logger.trace(
+              "returning existing reserve for bankWithdrawStatusUri",
+            );
+            return {
+              exchange: otherReserve.exchangeBaseUrl,
+              reservePub: otherReserve.reservePub,
+            };
+          }
+        }
+        await tx.put(Stores.bankWithdrawUris, {
+          reservePub: reserveRecord.reservePub,
+          talerWithdrawUri: reserveRecord.bankWithdrawStatusUrl,
+        });
+      }
+      await tx.put(Stores.currencies, cr);
+      await tx.put(Stores.reserves, reserveRecord);
+      const r: CreateReserveResponse = {
+        exchange: canonExchange,
+        reservePub: keypair.pub,
+      };
+      return r;
+    },
+  );
+
+  // Asynchronously process the reserve, but return
+  // to the caller already.
+  processReserve(ws, resp.reservePub).catch(e => {
+    console.error("Processing reserve failed:", e);
+  });
+
+  return resp;
+}
+
+/**
+ * First fetch information requred 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.
+ */
+export async function processReserve(
+  ws: InternalWalletState,
+  reservePub: string,
+): Promise<void> {
+  const p = ws.memoProcessReserve.find(reservePub);
+  if (p) {
+    return p;
+  } else {
+    return ws.memoProcessReserve.put(
+      reservePub,
+      processReserveImpl(ws, reservePub),
+    );
+  }
+}
+
+async function registerReserveWithBank(
+  ws: InternalWalletState,
+  reservePub: string,
+): Promise<void> {
+  let reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
+  switch (reserve?.reserveStatus) {
+    case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+    case ReserveRecordStatus.REGISTERING_BANK:
+      break;
+    default:
+      return;
+  }
+  const bankStatusUrl = reserve.bankWithdrawStatusUrl;
+  if (!bankStatusUrl) {
+    return;
+  }
+  console.log("making selection");
+  if (reserve.timestampReserveInfoPosted) {
+    throw Error("bank claims that reserve info selection is not done");
+  }
+  const bankResp = await ws.http.postJson(bankStatusUrl, {
+    reserve_pub: reservePub,
+    selected_exchange: reserve.exchangeWire,
+  });
+  console.log("got response", bankResp);
+  await oneShotMutate(ws.db, Stores.reserves, reservePub, r => {
+    switch (r.reserveStatus) {
+      case ReserveRecordStatus.REGISTERING_BANK:
+      case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+        break;
+      default:
+        return;
+    }
+    r.timestampReserveInfoPosted = getTimestampNow();
+    r.reserveStatus = ReserveRecordStatus.WAIT_CONFIRM_BANK;
+    return r;
+  });
+  return processReserveBankStatus(ws, reservePub);
+}
+
+export async function processReserveBankStatus(
+  ws: InternalWalletState,
+  reservePub: string,
+): Promise<void> {
+  let reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
+  switch (reserve?.reserveStatus) {
+    case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+    case ReserveRecordStatus.REGISTERING_BANK:
+      break;
+    default:
+      return;
+  }
+  const bankStatusUrl = reserve.bankWithdrawStatusUrl;
+  if (!bankStatusUrl) {
+    return;
+  }
+
+  let status: WithdrawOperationStatusResponse;
+  try {
+    const statusResp = await ws.http.get(bankStatusUrl);
+    status = WithdrawOperationStatusResponse.checked(statusResp.responseJson);
+  } catch (e) {
+    throw e;
+  }
+
+  if (status.selection_done) {
+    if (reserve.reserveStatus === ReserveRecordStatus.REGISTERING_BANK) {
+      await registerReserveWithBank(ws, reservePub);
+      return await processReserveBankStatus(ws, reservePub);
+    }
+  } else {
+    await registerReserveWithBank(ws, reservePub);
+    return await processReserveBankStatus(ws, reservePub);
+  }
+
+  if (status.transfer_done) {
+    await oneShotMutate(ws.db, Stores.reserves, reservePub, r => {
+      switch (r.reserveStatus) {
+        case ReserveRecordStatus.REGISTERING_BANK:
+        case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+          break;
+        default:
+          return;
+      }
+      const now = getTimestampNow();
+      r.timestampConfirmed = now;
+      r.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
+      return r;
+    });
+    await processReserveImpl(ws, reservePub);
+  } else {
+    await oneShotMutate(ws.db, Stores.reserves, reservePub, r => {
+      switch (r.reserveStatus) {
+        case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+          break;
+        default:
+          return;
+      }
+      r.bankWithdrawConfirmUrl = status.confirm_transfer_url;
+      return r;
+    });
+  }
+}
+
+async function setReserveError(
+  ws: InternalWalletState,
+  reservePub: string,
+  err: OperationError,
+): Promise<void> {
+  const mut = (reserve: ReserveRecord) => {
+    reserve.lastError = err;
+    return reserve;
+  };
+  await oneShotMutate(ws.db, Stores.reserves, reservePub, mut);
+}
+
+/**
+ * Update the information about a reserve that is stored in the wallet
+ * by quering the reserve's exchange.
+ */
+async function updateReserve(
+  ws: InternalWalletState,
+  reservePub: string,
+): Promise<void> {
+  const reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
+  if (!reserve) {
+    throw Error("reserve not in db");
+  }
+
+  if (reserve.timestampConfirmed === undefined) {
+    throw Error("reserve not confirmed yet");
+  }
+
+  if (reserve.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
+    return;
+  }
+
+  const reqUrl = new URL("reserve/status", reserve.exchangeBaseUrl);
+  reqUrl.searchParams.set("reserve_pub", reservePub);
+  let resp;
+  try {
+    resp = await ws.http.get(reqUrl.href);
+  } catch (e) {
+    if (e.response?.status === 404) {
+      return;
+    } else {
+      const m = e.message;
+      setReserveError(ws, reservePub, {
+        type: "network",
+        details: {},
+        message: m,
+      });
+      throw new OperationFailedAndReportedError(m);
+    }
+  }
+  const reserveInfo = ReserveStatus.checked(resp.responseJson);
+  const balance = Amounts.parseOrThrow(reserveInfo.balance);
+  await oneShotMutate(ws.db, Stores.reserves, reserve.reservePub, r => {
+    if (r.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
+      return;
+    }
+
+    // FIXME: check / compare history!
+    if (!r.lastStatusQuery) {
+      // FIXME: check if this matches initial expectations
+      r.withdrawRemainingAmount = balance;
+    } else {
+      const expectedBalance = Amounts.sub(
+        r.withdrawAllocatedAmount,
+        r.withdrawCompletedAmount,
+      );
+      const cmp = Amounts.cmp(balance, expectedBalance.amount);
+      if (cmp == 0) {
+        // Nothing changed.
+        return;
+      }
+      if (cmp > 0) {
+        const extra = Amounts.sub(balance, expectedBalance.amount).amount;
+        r.withdrawRemainingAmount = Amounts.add(
+          r.withdrawRemainingAmount,
+          extra,
+        ).amount;
+      } else {
+        // We're missing some money.
+      }
+    }
+    r.lastStatusQuery = getTimestampNow();
+    r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
+    return r;
+  });
+  ws.notifier.notify();
+}
+
+async function processReserveImpl(
+  ws: InternalWalletState,
+  reservePub: string,
+): Promise<void> {
+  const reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
+  if (!reserve) {
+    console.log("not processing reserve: reserve does not exist");
+    return;
+  }
+  logger.trace(
+    `Processing reserve ${reservePub} with status ${reserve.reserveStatus}`,
+  );
+  switch (reserve.reserveStatus) {
+    case ReserveRecordStatus.UNCONFIRMED:
+      // nothing to do
+      break;
+    case ReserveRecordStatus.REGISTERING_BANK:
+      await processReserveBankStatus(ws, reservePub);
+      return processReserveImpl(ws, reservePub);
+    case ReserveRecordStatus.QUERYING_STATUS:
+      await updateReserve(ws, reservePub);
+      return processReserveImpl(ws, reservePub);
+    case ReserveRecordStatus.WITHDRAWING:
+      await depleteReserve(ws, reservePub);
+      break;
+    case ReserveRecordStatus.DORMANT:
+      // nothing to do
+      break;
+    case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+      await processReserveBankStatus(ws, reservePub);
+      break;
+    default:
+      console.warn("unknown reserve record status:", reserve.reserveStatus);
+      assertUnreachable(reserve.reserveStatus);
+      break;
+  }
+}
+
+export async function confirmReserve(
+  ws: InternalWalletState,
+  req: ConfirmReserveRequest,
+): Promise<void> {
+  const now = getTimestampNow();
+  await oneShotMutate(ws.db, Stores.reserves, req.reservePub, reserve => {
+    if (reserve.reserveStatus !== ReserveRecordStatus.UNCONFIRMED) {
+      return;
+    }
+    reserve.timestampConfirmed = now;
+    reserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
+    return reserve;
+  });
+
+  ws.notifier.notify();
+
+  processReserve(ws, req.reservePub).catch(e => {
+    console.log("processing reserve failed:", e);
+  });
+}
+
+/**
+ * Withdraw coins from a reserve until it is empty.
+ *
+ * When finished, marks the reserve as depleted by setting
+ * the depleted timestamp.
+ */
+async function depleteReserve(
+  ws: InternalWalletState,
+  reservePub: string,
+): Promise<void> {
+  const reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
+  if (!reserve) {
+    return;
+  }
+  if (reserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
+    return;
+  }
+  logger.trace(`depleting reserve ${reservePub}`);
+
+  const withdrawAmount = reserve.withdrawRemainingAmount;
+
+  logger.trace(`getting denom list`);
+
+  const denomsForWithdraw = await getVerifiedWithdrawDenomList(
+    ws,
+    reserve.exchangeBaseUrl,
+    withdrawAmount,
+  );
+  logger.trace(`got denom list`);
+  if (denomsForWithdraw.length === 0) {
+    const m = `Unable to withdraw from reserve, no denominations are available 
to withdraw.`;
+    await setReserveError(ws, reserve.reservePub, {
+      type: "internal",
+      message: m,
+      details: {},
+    });
+    console.log(m);
+    throw new OperationFailedAndReportedError(m);
+  }
+
+  logger.trace("selected denominations");
+
+  const withdrawalSessionId = encodeCrock(randomBytes(32));
+
+  const withdrawalRecord: WithdrawalSessionRecord = {
+    withdrawSessionId: withdrawalSessionId,
+    exchangeBaseUrl: reserve.exchangeBaseUrl,
+    source: {
+      type: "reserve",
+      reservePub: reserve.reservePub,
+    },
+    withdrawalAmount: Amounts.toString(withdrawAmount),
+    startTimestamp: getTimestampNow(),
+    denoms: denomsForWithdraw.map(x => x.denomPub),
+    withdrawn: denomsForWithdraw.map(x => false),
+    planchets: denomsForWithdraw.map(x => undefined),
+  };
+
+  const totalCoinValue = Amounts.sum(denomsForWithdraw.map(x => x.value))
+    .amount;
+  const totalCoinWithdrawFee = Amounts.sum(
+    denomsForWithdraw.map(x => x.feeWithdraw),
+  ).amount;
+  const totalWithdrawAmount = Amounts.add(totalCoinValue, totalCoinWithdrawFee)
+    .amount;
+
+  function mutateReserve(r: ReserveRecord): ReserveRecord {
+    const remaining = Amounts.sub(
+      r.withdrawRemainingAmount,
+      totalWithdrawAmount,
+    );
+    if (remaining.saturated) {
+      console.error("can't create planchets, saturated");
+      throw TransactionAbort;
+    }
+    const allocated = Amounts.add(
+      r.withdrawAllocatedAmount,
+      totalWithdrawAmount,
+    );
+    if (allocated.saturated) {
+      console.error("can't create planchets, saturated");
+      throw TransactionAbort;
+    }
+    r.withdrawRemainingAmount = remaining.amount;
+    r.withdrawAllocatedAmount = allocated.amount;
+    r.reserveStatus = ReserveRecordStatus.DORMANT;
+
+    return r;
+  }
+
+  const success = await runWithWriteTransaction(
+    ws.db,
+    [Stores.withdrawalSession, Stores.reserves],
+    async tx => {
+      const myReserve = await tx.get(Stores.reserves, reservePub);
+      if (!myReserve) {
+        return false;
+      }
+      if (myReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
+        return false;
+      }
+      await tx.mutate(Stores.reserves, reserve.reservePub, mutateReserve);
+      await tx.put(Stores.withdrawalSession, withdrawalRecord);
+      return true;
+    },
+  );
+
+  if (success) {
+    console.log("processing new withdraw session");
+    await processWithdrawSession(ws, withdrawalSessionId);
+  } else {
+    console.trace("withdraw session already existed");
+  }
+}
diff --git a/src/wallet-impl/return.ts b/src/wallet-impl/return.ts
new file mode 100644
index 00000000..9cf12052
--- /dev/null
+++ b/src/wallet-impl/return.ts
@@ -0,0 +1,274 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+  HistoryQuery,
+  HistoryEvent,
+  WalletBalance,
+  WalletBalanceEntry,
+  ReturnCoinsRequest,
+  CoinWithDenom,
+} from "../walletTypes";
+import { oneShotIter, runWithWriteTransaction, oneShotGet, oneShotIterIndex, 
oneShotPut } from "../util/query";
+import { InternalWalletState } from "./state";
+import { Stores, TipRecord, CoinStatus, CoinsReturnRecord, CoinRecord } from 
"../dbTypes";
+import * as Amounts from "../util/amounts";
+import { AmountJson } from "../util/amounts";
+import { Logger } from "../util/logging";
+import { canonicalJson } from "../util/helpers";
+import { ContractTerms } from "../talerTypes";
+import { selectPayCoins } from "./pay";
+
+const logger = new Logger("return.ts");
+
+async function getCoinsForReturn(
+  ws: InternalWalletState,
+  exchangeBaseUrl: string,
+  amount: AmountJson,
+): Promise<CoinWithDenom[] | undefined> {
+  const exchange = await oneShotGet(
+    ws.db,
+    Stores.exchanges,
+    exchangeBaseUrl,
+  );
+  if (!exchange) {
+    throw Error(`Exchange ${exchangeBaseUrl} not known to the wallet`);
+  }
+
+  const coins: CoinRecord[] = await oneShotIterIndex(
+    ws.db,
+    Stores.coins.exchangeBaseUrlIndex,
+    exchange.baseUrl,
+  ).toArray();
+
+  if (!coins || !coins.length) {
+    return [];
+  }
+
+  const denoms = await oneShotIterIndex(
+    ws.db,
+    Stores.denominations.exchangeBaseUrlIndex,
+    exchange.baseUrl,
+  ).toArray();
+
+  // Denomination of the first coin, we assume that all other
+  // coins have the same currency
+  const firstDenom = await oneShotGet(ws.db, Stores.denominations, [
+    exchange.baseUrl,
+    coins[0].denomPub,
+  ]);
+  if (!firstDenom) {
+    throw Error("db inconsistent");
+  }
+  const currency = firstDenom.value.currency;
+
+  const cds: CoinWithDenom[] = [];
+  for (const coin of coins) {
+    const denom = await oneShotGet(ws.db, Stores.denominations, [
+      exchange.baseUrl,
+      coin.denomPub,
+    ]);
+    if (!denom) {
+      throw Error("db inconsistent");
+    }
+    if (denom.value.currency !== currency) {
+      console.warn(
+        `same pubkey for different currencies at exchange ${exchange.baseUrl}`,
+      );
+      continue;
+    }
+    if (coin.suspended) {
+      continue;
+    }
+    if (coin.status !== CoinStatus.Fresh) {
+      continue;
+    }
+    cds.push({ coin, denom });
+  }
+
+  const res = selectPayCoins(denoms, cds, amount, amount);
+  if (res) {
+    return res.cds;
+  }
+  return undefined;
+}
+
+
+/**
+ * Trigger paying coins back into the user's account.
+ */
+export async function returnCoins(
+  ws: InternalWalletState,
+  req: ReturnCoinsRequest,
+): Promise<void> {
+  logger.trace("got returnCoins request", req);
+  const wireType = (req.senderWire as any).type;
+  logger.trace("wireType", wireType);
+  if (!wireType || typeof wireType !== "string") {
+    console.error(`wire type must be a non-empty string, not ${wireType}`);
+    return;
+  }
+  const stampSecNow = Math.floor(new Date().getTime() / 1000);
+  const exchange = await oneShotGet(ws.db, Stores.exchanges, req.exchange);
+  if (!exchange) {
+    console.error(`Exchange ${req.exchange} not known to the wallet`);
+    return;
+  }
+  const exchangeDetails = exchange.details;
+  if (!exchangeDetails) {
+    throw Error("exchange information needs to be updated first.");
+  }
+  logger.trace("selecting coins for return:", req);
+  const cds = await getCoinsForReturn(ws, req.exchange, req.amount);
+  logger.trace(cds);
+
+  if (!cds) {
+    throw Error("coin return impossible, can't select coins");
+  }
+
+  const { priv, pub } = await ws.cryptoApi.createEddsaKeypair();
+
+  const wireHash = await ws.cryptoApi.hashString(
+    canonicalJson(req.senderWire),
+  );
+
+  const contractTerms: ContractTerms = {
+    H_wire: wireHash,
+    amount: Amounts.toString(req.amount),
+    auditors: [],
+    exchanges: [
+      { master_pub: exchangeDetails.masterPublicKey, url: exchange.baseUrl },
+    ],
+    extra: {},
+    fulfillment_url: "",
+    locations: [],
+    max_fee: Amounts.toString(req.amount),
+    merchant: {},
+    merchant_pub: pub,
+    order_id: "none",
+    pay_deadline: `/Date(${stampSecNow + 30 * 5})/`,
+    wire_transfer_deadline: `/Date(${stampSecNow + 60 * 5})/`,
+    merchant_base_url: "taler://return-to-account",
+    products: [],
+    refund_deadline: `/Date(${stampSecNow + 60 * 5})/`,
+    timestamp: `/Date(${stampSecNow})/`,
+    wire_method: wireType,
+  };
+
+  const contractTermsHash = await ws.cryptoApi.hashString(
+    canonicalJson(contractTerms),
+  );
+
+  const payCoinInfo = await ws.cryptoApi.signDeposit(
+    contractTerms,
+    cds,
+    Amounts.parseOrThrow(contractTerms.amount),
+  );
+
+  logger.trace("pci", payCoinInfo);
+
+  const coins = payCoinInfo.sigs.map(s => ({ coinPaySig: s }));
+
+  const coinsReturnRecord: CoinsReturnRecord = {
+    coins,
+    contractTerms,
+    contractTermsHash,
+    exchange: exchange.baseUrl,
+    merchantPriv: priv,
+    wire: req.senderWire,
+  };
+
+  await runWithWriteTransaction(
+    ws.db,
+    [Stores.coinsReturns, Stores.coins],
+    async tx => {
+      await tx.put(Stores.coinsReturns, coinsReturnRecord);
+      for (let c of payCoinInfo.updatedCoins) {
+        await tx.put(Stores.coins, c);
+      }
+    },
+  );
+  ws.badge.showNotification();
+  ws.notifier.notify();
+
+  depositReturnedCoins(ws, coinsReturnRecord);
+}
+
+async function depositReturnedCoins(
+  ws: InternalWalletState,
+  coinsReturnRecord: CoinsReturnRecord,
+): Promise<void> {
+  for (const c of coinsReturnRecord.coins) {
+    if (c.depositedSig) {
+      continue;
+    }
+    const req = {
+      H_wire: coinsReturnRecord.contractTerms.H_wire,
+      coin_pub: c.coinPaySig.coin_pub,
+      coin_sig: c.coinPaySig.coin_sig,
+      contribution: c.coinPaySig.contribution,
+      denom_pub: c.coinPaySig.denom_pub,
+      h_contract_terms: coinsReturnRecord.contractTermsHash,
+      merchant_pub: coinsReturnRecord.contractTerms.merchant_pub,
+      pay_deadline: coinsReturnRecord.contractTerms.pay_deadline,
+      refund_deadline: coinsReturnRecord.contractTerms.refund_deadline,
+      timestamp: coinsReturnRecord.contractTerms.timestamp,
+      ub_sig: c.coinPaySig.ub_sig,
+      wire: coinsReturnRecord.wire,
+      wire_transfer_deadline: coinsReturnRecord.contractTerms.pay_deadline,
+    };
+    logger.trace("req", req);
+    const reqUrl = new URL("deposit", coinsReturnRecord.exchange);
+    const resp = await ws.http.postJson(reqUrl.href, req);
+    if (resp.status !== 200) {
+      console.error("deposit failed due to status code", resp);
+      continue;
+    }
+    const respJson = resp.responseJson;
+    if (respJson.status !== "DEPOSIT_OK") {
+      console.error("deposit failed", resp);
+      continue;
+    }
+
+    if (!respJson.sig) {
+      console.error("invalid 'sig' field", resp);
+      continue;
+    }
+
+    // FIXME: verify signature
+
+    // For every successful deposit, we replace the old record with an updated 
one
+    const currentCrr = await oneShotGet(
+      ws.db,
+      Stores.coinsReturns,
+      coinsReturnRecord.contractTermsHash,
+    );
+    if (!currentCrr) {
+      console.error("database inconsistent");
+      continue;
+    }
+    for (const nc of currentCrr.coins) {
+      if (nc.coinPaySig.coin_pub === c.coinPaySig.coin_pub) {
+        nc.depositedSig = respJson.sig;
+      }
+    }
+    await oneShotPut(ws.db, Stores.coinsReturns, currentCrr);
+    ws.notifier.notify();
+  }
+}
diff --git a/src/wallet-impl/state.ts b/src/wallet-impl/state.ts
new file mode 100644
index 00000000..3d6bb8bd
--- /dev/null
+++ b/src/wallet-impl/state.ts
@@ -0,0 +1,32 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { HttpRequestLibrary } from "../util/http";
+import { Badge, Notifier, NextUrlResult } from "../walletTypes";
+import { SpeculativePayData } from "./pay";
+import { CryptoApi } from "../crypto/cryptoApi";
+import { AsyncOpMemo } from "../util/asyncMemo";
+
+export interface InternalWalletState {
+  db: IDBDatabase;
+  http: HttpRequestLibrary;
+  badge: Badge;
+  notifier: Notifier;
+  cryptoApi: CryptoApi;
+  speculativePayData: SpeculativePayData | undefined;
+  cachedNextUrl: { [fulfillmentUrl: string]: NextUrlResult };
+  memoProcessReserve: AsyncOpMemo<void>;
+}
\ No newline at end of file
diff --git a/src/wallet-impl/tip.ts b/src/wallet-impl/tip.ts
new file mode 100644
index 00000000..b102d026
--- /dev/null
+++ b/src/wallet-impl/tip.ts
@@ -0,0 +1,246 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+ 
+import { oneShotGet, oneShotPut, oneShotMutate, runWithWriteTransaction } from 
"../util/query";
+import { InternalWalletState } from "./state";
+import { parseTipUri } from "../util/taleruri";
+import { TipStatus, getTimestampNow } from "../walletTypes";
+import { TipPickupGetResponse, TipPlanchetDetail, TipResponse } from 
"../talerTypes";
+import * as Amounts from "../util/amounts";
+import { Stores, PlanchetRecord, WithdrawalSessionRecord } from "../dbTypes";
+import { getWithdrawDetailsForAmount, getVerifiedWithdrawDenomList, 
processWithdrawSession } from "./withdraw";
+import { getTalerStampSec } from "../util/helpers";
+import { updateExchangeFromUrl } from "./exchanges";
+import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
+
+
+export async function getTipStatus(
+  ws: InternalWalletState,
+  talerTipUri: string): Promise<TipStatus> {
+  const res = parseTipUri(talerTipUri);
+  if (!res) {
+    throw Error("invalid taler://tip URI");
+  }
+
+  const tipStatusUrl = new URL("tip-pickup", res.merchantBaseUrl);
+  tipStatusUrl.searchParams.set("tip_id", res.merchantTipId);
+  console.log("checking tip status from", tipStatusUrl.href);
+  const merchantResp = await ws.http.get(tipStatusUrl.href);
+  console.log("resp:", merchantResp.responseJson);
+  const tipPickupStatus = TipPickupGetResponse.checked(
+    merchantResp.responseJson,
+  );
+
+  console.log("status", tipPickupStatus);
+
+  let amount = Amounts.parseOrThrow(tipPickupStatus.amount);
+
+  let tipRecord = await oneShotGet(ws.db, Stores.tips, [
+    res.merchantTipId,
+    res.merchantOrigin,
+  ]);
+
+  if (!tipRecord) {
+    const withdrawDetails = await getWithdrawDetailsForAmount(
+      ws,
+      tipPickupStatus.exchange_url,
+      amount,
+    );
+
+    const tipId = encodeCrock(getRandomBytes(32));
+
+    tipRecord = {
+      tipId,
+      accepted: false,
+      amount,
+      deadline: getTalerStampSec(tipPickupStatus.stamp_expire)!,
+      exchangeUrl: tipPickupStatus.exchange_url,
+      merchantBaseUrl: res.merchantBaseUrl,
+      nextUrl: undefined,
+      pickedUp: false,
+      planchets: undefined,
+      response: undefined,
+      timestamp: getTimestampNow(),
+      merchantTipId: res.merchantTipId,
+      totalFees: Amounts.add(
+        withdrawDetails.overhead,
+        withdrawDetails.withdrawFee,
+      ).amount,
+    };
+    await oneShotPut(ws.db, Stores.tips, tipRecord);
+  }
+
+  const tipStatus: TipStatus = {
+    accepted: !!tipRecord && tipRecord.accepted,
+    amount: Amounts.parseOrThrow(tipPickupStatus.amount),
+    amountLeft: Amounts.parseOrThrow(tipPickupStatus.amount_left),
+    exchangeUrl: tipPickupStatus.exchange_url,
+    nextUrl: tipPickupStatus.extra.next_url,
+    merchantOrigin: res.merchantOrigin,
+    merchantTipId: res.merchantTipId,
+    expirationTimestamp: getTalerStampSec(tipPickupStatus.stamp_expire)!,
+    timestamp: getTalerStampSec(tipPickupStatus.stamp_created)!,
+    totalFees: tipRecord.totalFees,
+    tipId: tipRecord.tipId,
+  };
+
+  return tipStatus;
+}
+
+export async function processTip(
+  ws: InternalWalletState,
+  tipId: string,
+) {
+  let tipRecord = await oneShotGet(ws.db, Stores.tips, tipId);
+  if (!tipRecord) {
+    return;
+  }
+
+  if (tipRecord.pickedUp) {
+    console.log("tip already picked up");
+    return;
+  }
+
+  if (!tipRecord.planchets) {
+    await updateExchangeFromUrl(ws, tipRecord.exchangeUrl);
+    const denomsForWithdraw = await getVerifiedWithdrawDenomList(
+      ws,
+      tipRecord.exchangeUrl,
+      tipRecord.amount,
+    );
+
+    const planchets = await Promise.all(
+      denomsForWithdraw.map(d => ws.cryptoApi.createTipPlanchet(d)),
+    );
+
+    await oneShotMutate(ws.db, Stores.tips, tipId, r => {
+      if (!r.planchets) {
+        r.planchets = planchets;
+      }
+      return r;
+    });
+  }
+
+  tipRecord = await oneShotGet(ws.db, Stores.tips, tipId);
+  if (!tipRecord) {
+    throw Error("tip not in database");
+  }
+
+  if (!tipRecord.planchets) {
+    throw Error("invariant violated");
+  }
+
+  console.log("got planchets for tip!");
+
+  // Planchets in the form that the merchant expects
+  const planchetsDetail: TipPlanchetDetail[] = tipRecord.planchets.map(p => ({
+    coin_ev: p.coinEv,
+    denom_pub_hash: p.denomPubHash,
+  }));
+
+  let merchantResp;
+
+  const tipStatusUrl = new URL("tip-pickup", tipRecord.merchantBaseUrl);
+
+  try {
+    const req = { planchets: planchetsDetail, tip_id: tipRecord.merchantTipId 
};
+    merchantResp = await ws.http.postJson(tipStatusUrl.href, req);
+    console.log("got merchant resp:", merchantResp);
+  } catch (e) {
+    console.log("tipping failed", e);
+    throw e;
+  }
+
+  const response = TipResponse.checked(merchantResp.responseJson);
+
+  if (response.reserve_sigs.length !== tipRecord.planchets.length) {
+    throw Error("number of tip responses does not match requested planchets");
+  }
+
+  const planchets: PlanchetRecord[] = [];
+
+  for (let i = 0; i < tipRecord.planchets.length; i++) {
+    const tipPlanchet = tipRecord.planchets[i];
+    const planchet: PlanchetRecord = {
+      blindingKey: tipPlanchet.blindingKey,
+      coinEv: tipPlanchet.coinEv,
+      coinPriv: tipPlanchet.coinPriv,
+      coinPub: tipPlanchet.coinPub,
+      coinValue: tipPlanchet.coinValue,
+      denomPub: tipPlanchet.denomPub,
+      denomPubHash: tipPlanchet.denomPubHash,
+      reservePub: response.reserve_pub,
+      withdrawSig: response.reserve_sigs[i].reserve_sig,
+      isFromTip: true,
+    };
+    planchets.push(planchet);
+  }
+
+  const withdrawalSessionId = encodeCrock(getRandomBytes(32));
+
+  const withdrawalSession: WithdrawalSessionRecord = {
+    denoms: planchets.map((x) => x.denomPub),
+    exchangeBaseUrl: tipRecord.exchangeUrl,
+    planchets: planchets,
+    source: {
+      type: "tip",
+      tipId: tipRecord.tipId,
+    },
+    startTimestamp: getTimestampNow(),
+    withdrawSessionId: withdrawalSessionId,
+    withdrawalAmount: Amounts.toString(tipRecord.amount),
+    withdrawn: planchets.map((x) => false),
+  };
+
+
+  await runWithWriteTransaction(ws.db, [Stores.tips, 
Stores.withdrawalSession], async (tx) => {
+    const tr = await tx.get(Stores.tips, tipId);
+    if (!tr) {
+      return;
+    }
+    if (tr.pickedUp) {
+      return;
+    }
+    tr.pickedUp = true;
+
+    await tx.put(Stores.tips, tr);
+    await tx.put(Stores.withdrawalSession, withdrawalSession);
+  });
+
+  await processWithdrawSession(ws, withdrawalSessionId);
+
+  ws.notifier.notify();
+  ws.badge.showNotification();
+  return;
+}
+
+export async function acceptTip(
+  ws: InternalWalletState,
+  tipId: string,
+): Promise<void> {
+  const tipRecord = await oneShotGet(ws.db, Stores.tips, tipId);
+  if (!tipRecord) {
+    console.log("tip not found");
+    return;
+  }
+
+  tipRecord.accepted = true;
+  await oneShotPut(ws.db, Stores.tips, tipRecord);
+
+  await processTip(ws, tipId);
+  return;
+}
diff --git a/src/wallet-impl/withdraw.ts b/src/wallet-impl/withdraw.ts
new file mode 100644
index 00000000..4e2d8055
--- /dev/null
+++ b/src/wallet-impl/withdraw.ts
@@ -0,0 +1,577 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { AmountJson } from "../util/amounts";
+import {
+  DenominationRecord,
+  Stores,
+  DenominationStatus,
+  CoinStatus,
+  CoinRecord,
+  PlanchetRecord,
+} from "../dbTypes";
+import * as Amounts from "../util/amounts";
+import {
+  getTimestampNow,
+  AcceptWithdrawalResponse,
+  DownloadedWithdrawInfo,
+  ReserveCreationInfo,
+  WithdrawDetails,
+} from "../walletTypes";
+import { WithdrawOperationStatusResponse } from "../talerTypes";
+import { InternalWalletState } from "./state";
+import { parseWithdrawUri } from "../util/taleruri";
+import { Logger } from "../util/logging";
+import {
+  oneShotGet,
+  oneShotPut,
+  oneShotIterIndex,
+  oneShotGetIndexed,
+  runWithWriteTransaction,
+} from "../util/query";
+import {
+  updateExchangeFromUrl,
+  getExchangePaytoUri,
+  getExchangeTrust,
+} from "./exchanges";
+import { createReserve, processReserveBankStatus } from "./reserves";
+import { WALLET_PROTOCOL_VERSION } from "../wallet";
+
+import * as LibtoolVersion from "../util/libtoolVersion";
+
+const logger = new Logger("withdraw.ts");
+
+function isWithdrawableDenom(d: DenominationRecord) {
+  const now = getTimestampNow();
+  const started = now.t_ms >= d.stampStart.t_ms;
+  const stillOkay = d.stampExpireWithdraw.t_ms + 60 * 1000 > now.t_ms;
+  return started && stillOkay;
+}
+
+/**
+ * Get a list of denominations (with repetitions possible)
+ * whose total value is as close as possible to the available
+ * amount, but never larger.
+ */
+export function getWithdrawDenomList(
+  amountAvailable: AmountJson,
+  denoms: DenominationRecord[],
+): DenominationRecord[] {
+  let remaining = Amounts.copy(amountAvailable);
+  const ds: DenominationRecord[] = [];
+
+  denoms = denoms.filter(isWithdrawableDenom);
+  denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value));
+
+  // This is an arbitrary number of coins
+  // we can withdraw in one go.  It's not clear if this limit
+  // is useful ...
+  for (let i = 0; i < 1000; i++) {
+    let found = false;
+    for (const d of denoms) {
+      const cost = Amounts.add(d.value, d.feeWithdraw).amount;
+      if (Amounts.cmp(remaining, cost) < 0) {
+        continue;
+      }
+      found = true;
+      remaining = Amounts.sub(remaining, cost).amount;
+      ds.push(d);
+      break;
+    }
+    if (!found) {
+      break;
+    }
+  }
+  return ds;
+}
+
+/**
+ * Get information about a withdrawal from
+ * a taler://withdraw URI.
+ */
+export async function getWithdrawalInfo(
+  ws: InternalWalletState,
+  talerWithdrawUri: string,
+): Promise<DownloadedWithdrawInfo> {
+  const uriResult = parseWithdrawUri(talerWithdrawUri);
+  if (!uriResult) {
+    throw Error("can't parse URL");
+  }
+  const resp = await ws.http.get(uriResult.statusUrl);
+  console.log("resp:", resp.responseJson);
+  const status = WithdrawOperationStatusResponse.checked(resp.responseJson);
+  return {
+    amount: Amounts.parseOrThrow(status.amount),
+    confirmTransferUrl: status.confirm_transfer_url,
+    extractedStatusUrl: uriResult.statusUrl,
+    selectionDone: status.selection_done,
+    senderWire: status.sender_wire,
+    suggestedExchange: status.suggested_exchange,
+    transferDone: status.transfer_done,
+    wireTypes: status.wire_types,
+  };
+}
+
+export async function acceptWithdrawal(
+  ws: InternalWalletState,
+  talerWithdrawUri: string,
+  selectedExchange: string,
+): Promise<AcceptWithdrawalResponse> {
+  const withdrawInfo = await getWithdrawalInfo(ws, talerWithdrawUri);
+  const exchangeWire = await getExchangePaytoUri(
+    ws,
+    selectedExchange,
+    withdrawInfo.wireTypes,
+  );
+  const reserve = await createReserve(ws, {
+    amount: withdrawInfo.amount,
+    bankWithdrawStatusUrl: withdrawInfo.extractedStatusUrl,
+    exchange: selectedExchange,
+    senderWire: withdrawInfo.senderWire,
+    exchangeWire: exchangeWire,
+  });
+  // We do this here, as the reserve should be registered before we return,
+  // so that we can redirect the user to the bank's status page.
+  await processReserveBankStatus(ws, reserve.reservePub);
+  console.log("acceptWithdrawal: returning");
+  return {
+    reservePub: reserve.reservePub,
+    confirmTransferUrl: withdrawInfo.confirmTransferUrl,
+  };
+}
+
+async function getPossibleDenoms(
+  ws: InternalWalletState,
+  exchangeBaseUrl: string,
+): Promise<DenominationRecord[]> {
+  return await oneShotIterIndex(
+    ws.db,
+    Stores.denominations.exchangeBaseUrlIndex,
+    exchangeBaseUrl,
+  ).filter(d => {
+    return (
+      d.status === DenominationStatus.Unverified ||
+      d.status === DenominationStatus.VerifiedGood
+    );
+  });
+}
+
+/**
+ * Given a planchet, withdraw a coin from the exchange.
+ */
+async function processPlanchet(
+  ws: InternalWalletState,
+  withdrawalSessionId: string,
+  coinIdx: number,
+): Promise<void> {
+  const withdrawalSession = await oneShotGet(
+    ws.db,
+    Stores.withdrawalSession,
+    withdrawalSessionId,
+  );
+  if (!withdrawalSession) {
+    return;
+  }
+  if (withdrawalSession.withdrawn[coinIdx]) {
+    return;
+  }
+  if (withdrawalSession.source.type === "reserve") {
+
+  }
+  const planchet = withdrawalSession.planchets[coinIdx];
+  if (!planchet) {
+    console.log("processPlanchet: planchet not found");
+    return;
+  }
+  const exchange = await oneShotGet(
+    ws.db,
+    Stores.exchanges,
+    withdrawalSession.exchangeBaseUrl,
+  );
+  if (!exchange) {
+    console.error("db inconsistent: exchange for planchet not found");
+    return;
+  }
+
+  const denom = await oneShotGet(ws.db, Stores.denominations, [
+    withdrawalSession.exchangeBaseUrl,
+    planchet.denomPub,
+  ]);
+
+  if (!denom) {
+    console.error("db inconsistent: denom for planchet not found");
+    return;
+  }
+
+  const wd: any = {};
+  wd.denom_pub_hash = planchet.denomPubHash;
+  wd.reserve_pub = planchet.reservePub;
+  wd.reserve_sig = planchet.withdrawSig;
+  wd.coin_ev = planchet.coinEv;
+  const reqUrl = new URL("reserve/withdraw", exchange.baseUrl).href;
+  const resp = await ws.http.postJson(reqUrl, wd);
+
+  const r = resp.responseJson;
+
+  const denomSig = await ws.cryptoApi.rsaUnblind(
+    r.ev_sig,
+    planchet.blindingKey,
+    planchet.denomPub,
+  );
+
+  const coin: CoinRecord = {
+    blindingKey: planchet.blindingKey,
+    coinPriv: planchet.coinPriv,
+    coinPub: planchet.coinPub,
+    currentAmount: planchet.coinValue,
+    denomPub: planchet.denomPub,
+    denomPubHash: planchet.denomPubHash,
+    denomSig,
+    exchangeBaseUrl: withdrawalSession.exchangeBaseUrl,
+    reservePub: planchet.reservePub,
+    status: CoinStatus.Fresh,
+    coinIndex: coinIdx,
+    withdrawSessionId: withdrawalSessionId,
+  };
+
+  await runWithWriteTransaction(
+    ws.db,
+    [Stores.coins, Stores.withdrawalSession, Stores.reserves],
+    async tx => {
+      const ws = await tx.get(
+        Stores.withdrawalSession,
+        withdrawalSessionId,
+      );
+      if (!ws) {
+        return;
+      }
+      if (ws.withdrawn[coinIdx]) {
+        // Already withdrawn
+        return;
+      }
+      ws.withdrawn[coinIdx] = true;
+      await tx.put(Stores.withdrawalSession, ws);
+      if (!planchet.isFromTip) {
+        const r = await tx.get(Stores.reserves, planchet.reservePub);
+        if (r) {
+          r.withdrawCompletedAmount = Amounts.add(
+            r.withdrawCompletedAmount,
+            Amounts.add(denom.value, denom.feeWithdraw).amount,
+          ).amount;
+          await tx.put(Stores.reserves, r);
+        }
+      }
+      await tx.add(Stores.coins, coin);
+    },
+  );
+  ws.notifier.notify();
+  logger.trace(`withdraw of one coin ${coin.coinPub} finished`);
+}
+
+/**
+ * Get a list of denominations to withdraw from the given exchange for the
+ * given amount, making sure that all denominations' signatures are verified.
+ *
+ * Writes to the DB in order to record the result from verifying
+ * denominations.
+ */
+export async function getVerifiedWithdrawDenomList(
+  ws: InternalWalletState,
+  exchangeBaseUrl: string,
+  amount: AmountJson,
+): Promise<DenominationRecord[]> {
+  const exchange = await oneShotGet(ws.db, Stores.exchanges, exchangeBaseUrl);
+  if (!exchange) {
+    console.log("exchange not found");
+    throw Error(`exchange ${exchangeBaseUrl} not found`);
+  }
+  const exchangeDetails = exchange.details;
+  if (!exchangeDetails) {
+    console.log("exchange details not available");
+    throw Error(`exchange ${exchangeBaseUrl} details not available`);
+  }
+
+  console.log("getting possible denoms");
+
+  const possibleDenoms = await getPossibleDenoms(ws, exchange.baseUrl);
+
+  console.log("got possible denoms");
+
+  let allValid = false;
+
+  let selectedDenoms: DenominationRecord[];
+
+  do {
+    allValid = true;
+    const nextPossibleDenoms = [];
+    selectedDenoms = getWithdrawDenomList(amount, possibleDenoms);
+    console.log("got withdraw denom list");
+    for (const denom of selectedDenoms || []) {
+      if (denom.status === DenominationStatus.Unverified) {
+        console.log(
+          "checking validity",
+          denom,
+          exchangeDetails.masterPublicKey,
+        );
+        const valid = await ws.cryptoApi.isValidDenom(
+          denom,
+          exchangeDetails.masterPublicKey,
+        );
+        console.log("done checking validity");
+        if (!valid) {
+          denom.status = DenominationStatus.VerifiedBad;
+          allValid = false;
+        } else {
+          denom.status = DenominationStatus.VerifiedGood;
+          nextPossibleDenoms.push(denom);
+        }
+        await oneShotPut(ws.db, Stores.denominations, denom);
+      } else {
+        nextPossibleDenoms.push(denom);
+      }
+    }
+  } while (selectedDenoms.length > 0 && !allValid);
+
+  console.log("returning denoms");
+
+  return selectedDenoms;
+}
+
+async function processWithdrawCoin(
+  ws: InternalWalletState,
+  withdrawalSessionId: string,
+  coinIndex: number,
+) {
+  logger.info("starting withdraw for coin");
+  const withdrawalSession = await oneShotGet(
+    ws.db,
+    Stores.withdrawalSession,
+    withdrawalSessionId,
+  );
+  if (!withdrawalSession) {
+    console.log("ws doesn't exist");
+    return;
+  }
+
+  const coin = await oneShotGetIndexed(
+    ws.db,
+    Stores.coins.byWithdrawalWithIdx,
+    [withdrawalSessionId, coinIndex],
+  );
+
+  if (coin) {
+    console.log("coin already exists");
+    return;
+  }
+
+  if (withdrawalSession.planchets[coinIndex]) {
+    return processPlanchet(ws, withdrawalSessionId, coinIndex);
+  } else {
+    const src = withdrawalSession.source;
+    if (src.type !== "reserve") {
+      throw Error("invalid state");
+    }
+    const reserve = await oneShotGet(ws.db, Stores.reserves, src.reservePub)
+    if (!reserve) {
+      return;
+    }
+    const denom = await oneShotGet(ws.db, Stores.denominations, [
+      withdrawalSession.exchangeBaseUrl,
+      withdrawalSession.denoms[coinIndex],
+    ]);
+    if (!denom) {
+      return;
+    }
+    const r = await ws.cryptoApi.createPlanchet({
+      denomPub: denom.denomPub,
+      feeWithdraw: denom.feeWithdraw,
+      reservePriv: reserve.reservePriv,
+      reservePub: reserve.reservePub,
+      value: denom.value,
+    });
+    const newPlanchet: PlanchetRecord = {
+      blindingKey: r.blindingKey,
+      coinEv: r.coinEv,
+      coinPriv: r.coinPriv,
+      coinPub: r.coinPub,
+      coinValue: r.coinValue,
+      denomPub: r.denomPub,
+      denomPubHash: r.denomPubHash,
+      isFromTip: false,
+      reservePub: r.reservePub,
+      withdrawSig: r.withdrawSig,
+    };
+    await runWithWriteTransaction(
+      ws.db,
+      [Stores.withdrawalSession],
+      async tx => {
+        const myWs = await tx.get(
+          Stores.withdrawalSession,
+          withdrawalSessionId,
+        );
+        if (!myWs) {
+          return;
+        }
+        if (myWs.planchets[coinIndex]) {
+          return;
+        }
+        myWs.planchets[coinIndex] = newPlanchet;
+        await tx.put(Stores.withdrawalSession, myWs);
+      },
+    );
+    await processPlanchet(ws, withdrawalSessionId, coinIndex);
+  }
+}
+
+export async function processWithdrawSession(
+  ws: InternalWalletState,
+  withdrawalSessionId: string,
+): Promise<void> {
+  logger.trace("processing withdraw session", withdrawalSessionId);
+  const withdrawalSession = await oneShotGet(
+    ws.db,
+    Stores.withdrawalSession,
+    withdrawalSessionId,
+  );
+  if (!withdrawalSession) {
+    logger.trace("withdraw session doesn't exist");
+    return;
+  }
+
+  const ps = withdrawalSession.denoms.map((d, i) =>
+    processWithdrawCoin(ws, withdrawalSessionId, i),
+  );
+  await Promise.all(ps);
+  ws.badge.showNotification();
+  return;
+}
+
+export async function getWithdrawDetailsForAmount(
+  ws: InternalWalletState,
+  baseUrl: string,
+  amount: AmountJson,
+): Promise<ReserveCreationInfo> {
+  const exchangeInfo = await updateExchangeFromUrl(ws, baseUrl);
+  const exchangeDetails = exchangeInfo.details;
+  if (!exchangeDetails) {
+    throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
+  }
+  const exchangeWireInfo = exchangeInfo.wireInfo;
+  if (!exchangeWireInfo) {
+    throw Error(`exchange ${exchangeInfo.baseUrl} wire details not available`);
+  }
+
+  const selectedDenoms = await getVerifiedWithdrawDenomList(
+    ws,
+    baseUrl,
+    amount,
+  );
+  let acc = Amounts.getZero(amount.currency);
+  for (const d of selectedDenoms) {
+    acc = Amounts.add(acc, d.feeWithdraw).amount;
+  }
+  const actualCoinCost = selectedDenoms
+    .map((d: DenominationRecord) => Amounts.add(d.value, d.feeWithdraw).amount)
+    .reduce((a, b) => Amounts.add(a, b).amount);
+
+  const exchangeWireAccounts: string[] = [];
+  for (let account of exchangeWireInfo.accounts) {
+    exchangeWireAccounts.push(account.url);
+  }
+
+  const { isTrusted, isAudited } = await getExchangeTrust(ws, exchangeInfo);
+
+  let earliestDepositExpiration = selectedDenoms[0].stampExpireDeposit;
+  for (let i = 1; i < selectedDenoms.length; i++) {
+    const expireDeposit = selectedDenoms[i].stampExpireDeposit;
+    if (expireDeposit.t_ms < earliestDepositExpiration.t_ms) {
+      earliestDepositExpiration = expireDeposit;
+    }
+  }
+
+  const possibleDenoms = await oneShotIterIndex(
+    ws.db,
+    Stores.denominations.exchangeBaseUrlIndex,
+    baseUrl,
+  ).filter(d => d.isOffered);
+
+  const trustedAuditorPubs = [];
+  const currencyRecord = await oneShotGet(
+    ws.db,
+    Stores.currencies,
+    amount.currency,
+  );
+  if (currencyRecord) {
+    trustedAuditorPubs.push(...currencyRecord.auditors.map(a => a.auditorPub));
+  }
+
+  let versionMatch;
+  if (exchangeDetails.protocolVersion) {
+    versionMatch = LibtoolVersion.compare(
+      WALLET_PROTOCOL_VERSION,
+      exchangeDetails.protocolVersion,
+    );
+
+    if (
+      versionMatch &&
+      !versionMatch.compatible &&
+      versionMatch.currentCmp === -1
+    ) {
+      console.warn(
+        `wallet version ${WALLET_PROTOCOL_VERSION} might be outdated ` +
+          `(exchange has ${exchangeDetails.protocolVersion}), checking for 
updates`,
+      );
+    }
+  }
+
+  const ret: ReserveCreationInfo = {
+    earliestDepositExpiration,
+    exchangeInfo,
+    exchangeWireAccounts,
+    exchangeVersion: exchangeDetails.protocolVersion || "unknown",
+    isAudited,
+    isTrusted,
+    numOfferedDenoms: possibleDenoms.length,
+    overhead: Amounts.sub(amount, actualCoinCost).amount,
+    selectedDenoms,
+    trustedAuditorPubs,
+    versionMatch,
+    walletVersion: WALLET_PROTOCOL_VERSION,
+    wireFees: exchangeWireInfo,
+    withdrawFee: acc,
+  };
+  return ret;
+}
+
+export async function getWithdrawDetailsForUri(
+  ws: InternalWalletState,
+  talerWithdrawUri: string,
+  maybeSelectedExchange?: string,
+): Promise<WithdrawDetails> {
+  const info = await getWithdrawalInfo(ws, talerWithdrawUri);
+  let rci: ReserveCreationInfo | undefined = undefined;
+  if (maybeSelectedExchange) {
+    rci = await getWithdrawDetailsForAmount(
+      ws,
+      maybeSelectedExchange,
+      info.amount,
+    );
+  }
+  return {
+    withdrawInfo: info,
+    reserveCreationInfo: rci,
+  };
+}
diff --git a/src/wallet-test.ts b/src/wallet-test.ts
index fef11ae5..cc8532f0 100644
--- a/src/wallet-test.ts
+++ b/src/wallet-test.ts
@@ -14,7 +14,6 @@
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-
 import test from "ava";
 
 import * as dbTypes from "./dbTypes";
@@ -22,9 +21,9 @@ import * as types from "./walletTypes";
 
 import * as wallet from "./wallet";
 
-import { AmountJson} from "./amounts";
-import * as Amounts from "./amounts";
-
+import { AmountJson } from "./util/amounts";
+import * as Amounts from "./util/amounts";
+import { selectPayCoins } from "./wallet-impl/pay";
 
 function a(x: string): AmountJson {
   const amt = Amounts.parse(x);
@@ -34,8 +33,11 @@ function a(x: string): AmountJson {
   return amt;
 }
 
-
-function fakeCwd(current: string, value: string, feeDeposit: string): 
types.CoinWithDenom {
+function fakeCwd(
+  current: string,
+  value: string,
+  feeDeposit: string,
+): types.CoinWithDenom {
   return {
     coin: {
       blindingKey: "(mock)",
@@ -71,14 +73,13 @@ function fakeCwd(current: string, value: string, 
feeDeposit: string): types.Coin
   };
 }
 
-
-test("coin selection 1", (t) => {
+test("coin selection 1", t => {
   const cds: types.CoinWithDenom[] = [
     fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.1"),
     fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.0"),
   ];
 
-  const res = wallet.selectPayCoins([], cds, a("EUR:2.0"), a("EUR:0.1"));
+  const res = selectPayCoins([], cds, a("EUR:2.0"), a("EUR:0.1"));
   if (!res) {
     t.fail();
     return;
@@ -87,15 +88,14 @@ test("coin selection 1", (t) => {
   t.pass();
 });
 
-
-test("coin selection 2", (t) => {
+test("coin selection 2", t => {
   const cds: types.CoinWithDenom[] = [
     fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
     fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.0"),
     // Merchant covers the fee, this one shouldn't be used
     fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.0"),
   ];
-  const res = wallet.selectPayCoins([], cds, a("EUR:2.0"), a("EUR:0.5"));
+  const res = selectPayCoins([], cds, a("EUR:2.0"), a("EUR:0.5"));
   if (!res) {
     t.fail();
     return;
@@ -104,15 +104,14 @@ test("coin selection 2", (t) => {
   t.pass();
 });
 
-
-test("coin selection 3", (t) => {
+test("coin selection 3", t => {
   const cds: types.CoinWithDenom[] = [
     fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
     fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
     // this coin should be selected instead of previous one with fee
     fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.0"),
   ];
-  const res = wallet.selectPayCoins([], cds, a("EUR:2.0"), a("EUR:0.5"));
+  const res = selectPayCoins([], cds, a("EUR:2.0"), a("EUR:0.5"));
   if (!res) {
     t.fail();
     return;
@@ -121,14 +120,13 @@ test("coin selection 3", (t) => {
   t.pass();
 });
 
-
-test("coin selection 4", (t) => {
+test("coin selection 4", t => {
   const cds: types.CoinWithDenom[] = [
     fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
     fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
     fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
   ];
-  const res = wallet.selectPayCoins([], cds, a("EUR:2.0"), a("EUR:0.2"));
+  const res = selectPayCoins([], cds, a("EUR:2.0"), a("EUR:0.2"));
   if (!res) {
     t.fail();
     return;
@@ -137,25 +135,23 @@ test("coin selection 4", (t) => {
   t.pass();
 });
 
-
-test("coin selection 5", (t) => {
+test("coin selection 5", t => {
   const cds: types.CoinWithDenom[] = [
     fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
     fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
     fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
   ];
-  const res = wallet.selectPayCoins([], cds, a("EUR:4.0"), a("EUR:0.2"));
+  const res = selectPayCoins([], cds, a("EUR:4.0"), a("EUR:0.2"));
   t.true(!res);
   t.pass();
 });
 
-
-test("coin selection 6", (t) => {
+test("coin selection 6", t => {
   const cds: types.CoinWithDenom[] = [
     fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
     fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
   ];
-  const res = wallet.selectPayCoins([], cds, a("EUR:2.0"), a("EUR:0.2"));
+  const res = selectPayCoins([], cds, a("EUR:2.0"), a("EUR:0.2"));
   t.true(!res);
   t.pass();
 });
diff --git a/src/wallet.ts b/src/wallet.ts
index 8fe8d367..91f6c0cc 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -1,17 +1,17 @@
 /*
- This file is part of TALER
+ This file is part of GNU Taler
  (C) 2015-2019 GNUnet e.V.
 
- TALER is free software; you can redistribute it and/or modify it under the
+ GNU Taler is free software; you can redistribute it and/or modify it under the
  terms of the GNU General Public License as published by the Free Software
  Foundation; either version 3, or (at your option) any later version.
 
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 
  You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
 /**
@@ -23,93 +23,59 @@
  * Imports.
  */
 import { CryptoApi, CryptoWorkerFactory } from "./crypto/cryptoApi";
+import { HttpRequestLibrary } from "./util/http";
 import {
-  amountToPretty,
-  canonicalJson,
-  canonicalizeBaseUrl,
-  getTalerStampSec,
-  strcmp,
-  extractTalerStamp,
-  extractTalerStampOrThrow,
-} from "./helpers";
-import { HttpRequestLibrary } from "./http";
-import * as LibtoolVersion from "./libtoolVersion";
-import {
-  TransactionAbort,
   oneShotPut,
   oneShotGet,
   runWithWriteTransaction,
   oneShotIter,
   oneShotIterIndex,
-  oneShotGetIndexed,
-  oneShotMutate,
-} from "./query";
+} from "./util/query";
+
+import { AmountJson } from "./util/amounts";
+import * as Amounts from "./util/amounts";
 
-import { AmountJson } from "./amounts";
-import * as Amounts from "./amounts";
+import {
+  acceptWithdrawal,
+  getWithdrawalInfo,
+  getWithdrawDetailsForUri,
+  getWithdrawDetailsForAmount,
+} from "./wallet-impl/withdraw";
 
-import URI = require("urijs");
+import {
+  abortFailedPayment,
+  preparePay,
+  confirmPay,
+  SpeculativePayData,
+} from "./wallet-impl/pay";
 
 import {
   CoinRecord,
   CoinStatus,
-  CoinsReturnRecord,
   CurrencyRecord,
   DenominationRecord,
-  DenominationStatus,
   ExchangeRecord,
   PlanchetRecord,
   ProposalRecord,
   PurchaseRecord,
-  RefreshPlanchetRecord,
-  RefreshSessionRecord,
   ReserveRecord,
   Stores,
-  TipRecord,
-  WireFee,
-  WithdrawalSessionRecord,
-  ExchangeUpdateStatus,
   ReserveRecordStatus,
-  ProposalStatus,
 } from "./dbTypes";
-import {
-  Auditor,
-  ContractTerms,
-  Denomination,
-  ExchangeHandle,
-  ExchangeWireJson,
-  KeysJson,
-  MerchantRefundPermission,
-  MerchantRefundResponse,
-  PayReq,
-  PaybackConfirmation,
-  Proposal,
-  RefundRequest,
-  ReserveStatus,
-  TipPlanchetDetail,
-  TipResponse,
-  WithdrawOperationStatusResponse,
-  TipPickupGetResponse,
-} from "./talerTypes";
+import { MerchantRefundPermission } from "./talerTypes";
 import {
   Badge,
   BenchmarkResult,
-  CoinSelectionResult,
-  CoinWithDenom,
   ConfirmPayResult,
   ConfirmReserveRequest,
   CreateReserveRequest,
   CreateReserveResponse,
   HistoryEvent,
-  NextUrlResult,
   Notifier,
-  PayCoinInfo,
-  ReserveCreationInfo,
   ReturnCoinsRequest,
   SenderWireInfos,
   TipStatus,
   WalletBalance,
-  WalletBalanceEntry,
   PreparePayResult,
   DownloadedWithdrawInfo,
   WithdrawDetails,
@@ -118,26 +84,32 @@ import {
   PendingOperationInfo,
   PendingOperationsResponse,
   HistoryQuery,
-  getTimestampNow,
-  OperationError,
-  Timestamp,
 } from "./walletTypes";
-import {
-  parsePayUri,
-  parseWithdrawUri,
-  parseTipUri,
-  parseRefundUri,
-} from "./taleruri";
-import { Logger } from "./logging";
-import { randomBytes } from "./crypto/primitives/nacl-fast";
-import { encodeCrock, getRandomBytes } from "./crypto/talerCrypto";
+import { Logger } from "./util/logging";
 
-interface SpeculativePayData {
-  payCoinInfo: PayCoinInfo;
-  exchangeUrl: string;
-  orderDownloadId: string;
-  proposal: ProposalRecord;
-}
+import { assertUnreachable } from "./util/assertUnreachable";
+
+import { applyRefund, getFullRefundFees } from "./wallet-impl/refund";
+
+import {
+  updateExchangeFromUrl,
+  getExchangeTrust,
+  getExchangePaytoUri,
+} from "./wallet-impl/exchanges";
+import { processReserve } from "./wallet-impl/reserves";
+
+import { AsyncOpMemo } from "./util/asyncMemo";
+
+import { InternalWalletState } from "./wallet-impl/state";
+import { createReserve, confirmReserve } from "./wallet-impl/reserves";
+import { processRefreshSession, refresh } from "./wallet-impl/refresh";
+import { processWithdrawSession } from "./wallet-impl/withdraw";
+import { getHistory } from "./wallet-impl/history";
+import { getPendingOperations } from "./wallet-impl/pending";
+import { getBalances } from "./wallet-impl/balance";
+import { acceptTip, getTipStatus } from "./wallet-impl/tip";
+import { returnCoins } from "./wallet-impl/return";
+import { payback } from "./wallet-impl/payback";
 
 /**
  * Wallet protocol version spoken with the exchange
@@ -147,7 +119,7 @@ interface SpeculativePayData {
  */
 export const WALLET_PROTOCOL_VERSION = "3:0:0";
 
-const WALLET_CACHE_BREAKER_CLIENT_VERSION = "2";
+export const WALLET_CACHE_BREAKER_CLIENT_VERSION = "2";
 
 const builtinCurrencies: CurrencyRecord[] = [
   {
@@ -164,186 +136,6 @@ const builtinCurrencies: CurrencyRecord[] = [
   },
 ];
 
-function isWithdrawableDenom(d: DenominationRecord) {
-  const now = getTimestampNow();
-  const started = now.t_ms >= d.stampStart.t_ms;
-  const stillOkay = d.stampExpireWithdraw.t_ms + 60 * 1000 > now.t_ms;
-  return started && stillOkay;
-}
-
-interface SelectPayCoinsResult {
-  cds: CoinWithDenom[];
-  totalFees: AmountJson;
-}
-
-function assertUnreachable(x: never): never {
-  throw new Error("Didn't expect to get here");
-}
-
-/**
- * Get the amount that we lose when refreshing a coin of the given denomination
- * with a certain amount left.
- *
- * If the amount left is zero, then the refresh cost
- * is also considered to be zero.  If a refresh isn't possible (e.g. due to 
lack of
- * the right denominations), then the cost is the full amount left.
- *
- * Considers refresh fees, withdrawal fees after refresh and amounts too small
- * to refresh.
- */
-export function getTotalRefreshCost(
-  denoms: DenominationRecord[],
-  refreshedDenom: DenominationRecord,
-  amountLeft: AmountJson,
-): AmountJson {
-  const withdrawAmount = Amounts.sub(amountLeft, refreshedDenom.feeRefresh)
-    .amount;
-  const withdrawDenoms = getWithdrawDenomList(withdrawAmount, denoms);
-  const resultingAmount = Amounts.add(
-    Amounts.getZero(withdrawAmount.currency),
-    ...withdrawDenoms.map(d => d.value),
-  ).amount;
-  const totalCost = Amounts.sub(amountLeft, resultingAmount).amount;
-  Wallet.enableTracing &&
-    console.log(
-      "total refresh cost for",
-      amountToPretty(amountLeft),
-      "is",
-      amountToPretty(totalCost),
-    );
-  return totalCost;
-}
-
-/**
- * Select coins for a payment under the merchant's constraints.
- *
- * @param denoms all available denoms, used to compute refresh fees
- */
-export function selectPayCoins(
-  denoms: DenominationRecord[],
-  cds: CoinWithDenom[],
-  paymentAmount: AmountJson,
-  depositFeeLimit: AmountJson,
-): SelectPayCoinsResult | undefined {
-  if (cds.length === 0) {
-    return undefined;
-  }
-  // Sort by ascending deposit fee and denomPub if deposit fee is the same
-  // (to guarantee deterministic results)
-  cds.sort(
-    (o1, o2) =>
-      Amounts.cmp(o1.denom.feeDeposit, o2.denom.feeDeposit) ||
-      strcmp(o1.denom.denomPub, o2.denom.denomPub),
-  );
-  const currency = cds[0].denom.value.currency;
-  const cdsResult: CoinWithDenom[] = [];
-  let accDepositFee: AmountJson = Amounts.getZero(currency);
-  let accAmount: AmountJson = Amounts.getZero(currency);
-  for (const { coin, denom } of cds) {
-    if (coin.suspended) {
-      continue;
-    }
-    if (coin.status !== CoinStatus.Fresh) {
-      continue;
-    }
-    if (Amounts.cmp(denom.feeDeposit, coin.currentAmount) >= 0) {
-      continue;
-    }
-    cdsResult.push({ coin, denom });
-    accDepositFee = Amounts.add(denom.feeDeposit, accDepositFee).amount;
-    let leftAmount = Amounts.sub(
-      coin.currentAmount,
-      Amounts.sub(paymentAmount, accAmount).amount,
-    ).amount;
-    accAmount = Amounts.add(coin.currentAmount, accAmount).amount;
-    const coversAmount = Amounts.cmp(accAmount, paymentAmount) >= 0;
-    const coversAmountWithFee =
-      Amounts.cmp(
-        accAmount,
-        Amounts.add(paymentAmount, denom.feeDeposit).amount,
-      ) >= 0;
-    const isBelowFee = Amounts.cmp(accDepositFee, depositFeeLimit) <= 0;
-
-    Wallet.enableTracing &&
-      console.log("candidate coin selection", {
-        coversAmount,
-        isBelowFee,
-        accDepositFee,
-        accAmount,
-        paymentAmount,
-      });
-
-    if ((coversAmount && isBelowFee) || coversAmountWithFee) {
-      const depositFeeToCover = Amounts.sub(accDepositFee, depositFeeLimit)
-        .amount;
-      leftAmount = Amounts.sub(leftAmount, depositFeeToCover).amount;
-      Wallet.enableTracing &&
-        console.log("deposit fee to cover", amountToPretty(depositFeeToCover));
-
-      let totalFees: AmountJson = Amounts.getZero(currency);
-      if (coversAmountWithFee && !isBelowFee) {
-        // these are the fees the customer has to pay
-        // because the merchant doesn't cover them
-        totalFees = Amounts.sub(depositFeeLimit, accDepositFee).amount;
-      }
-      totalFees = Amounts.add(
-        totalFees,
-        getTotalRefreshCost(denoms, denom, leftAmount),
-      ).amount;
-      return { cds: cdsResult, totalFees };
-    }
-  }
-  return undefined;
-}
-
-/**
- * Get a list of denominations (with repetitions possible)
- * whose total value is as close as possible to the available
- * amount, but never larger.
- */
-function getWithdrawDenomList(
-  amountAvailable: AmountJson,
-  denoms: DenominationRecord[],
-): DenominationRecord[] {
-  let remaining = Amounts.copy(amountAvailable);
-  const ds: DenominationRecord[] = [];
-
-  denoms = denoms.filter(isWithdrawableDenom);
-  denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value));
-
-  // This is an arbitrary number of coins
-  // we can withdraw in one go.  It's not clear if this limit
-  // is useful ...
-  for (let i = 0; i < 1000; i++) {
-    let found = false;
-    for (const d of denoms) {
-      const cost = Amounts.add(d.value, d.feeWithdraw).amount;
-      if (Amounts.cmp(remaining, cost) < 0) {
-        continue;
-      }
-      found = true;
-      remaining = Amounts.sub(remaining, cost).amount;
-      ds.push(d);
-      break;
-    }
-    if (!found) {
-      break;
-    }
-  }
-  return ds;
-}
-
-interface CoinsForPaymentArgs {
-  allowedAuditors: Auditor[];
-  allowedExchanges: ExchangeHandle[];
-  depositFeeLimit: AmountJson;
-  paymentAmount: AmountJson;
-  wireFeeAmortization: number;
-  wireFeeLimit: AmountJson;
-  wireFeeTime: Timestamp;
-  wireMethod: string;
-}
-
 /**
  * This error is thrown when an
  */
@@ -358,60 +150,27 @@ export class OperationFailedAndReportedError extends 
Error {
 
 const logger = new Logger("wallet.ts");
 
-interface MemoEntry<T> {
-  p: Promise<T>;
-  t: number;
-  n: number;
-}
-
-class AsyncOpMemo<T> {
-  n = 0;
-  memo: { [k: string]: MemoEntry<T> } = {};
-  put(key: string, p: Promise<T>): Promise<T> {
-    const n = this.n++;
-    this.memo[key] = {
-      p,
-      n,
-      t: new Date().getTime(),
-    };
-    p.finally(() => {
-      const r = this.memo[key];
-      if (r && r.n === n) {
-        delete this.memo[key];
-      }
-    });
-    return p;
-  }
-  find(key: string): Promise<T> | undefined {
-    const res = this.memo[key];
-    const tNow = new Date().getTime();
-    if (res && res.t < tNow - 10 * 1000) {
-      delete this.memo[key];
-      return;
-    } else if (res) {
-      return res.p;
-    }
-    return;
-  }
-}
-
 /**
  * The platform-independent wallet implementation.
  */
 export class Wallet {
-  /**
-   * IndexedDB database used by the wallet.
-   */
-  db: IDBDatabase;
-  static enableTracing = false;
-  private http: HttpRequestLibrary;
-  private badge: Badge;
-  private notifier: Notifier;
-  private cryptoApi: CryptoApi;
-  private speculativePayData: SpeculativePayData | undefined;
-  private cachedNextUrl: { [fulfillmentUrl: string]: NextUrlResult } = {};
+  private ws: InternalWalletState;
+
+  get db(): IDBDatabase {
+    return this.ws.db;
+  }
+
+  private get badge(): Badge {
+    return this.ws.badge;
+  }
+
+  private get cryptoApi(): CryptoApi {
+    return this.ws.cryptoApi;
+  }
 
-  private memoProcessReserve = new AsyncOpMemo<void>();
+  private get notifier(): Notifier {
+    return this.ws.notifier;
+  }
 
   constructor(
     db: IDBDatabase,
@@ -420,11 +179,25 @@ export class Wallet {
     notifier: Notifier,
     cryptoWorkerFactory: CryptoWorkerFactory,
   ) {
-    this.db = db;
-    this.http = http;
-    this.badge = badge;
-    this.notifier = notifier;
-    this.cryptoApi = new CryptoApi(cryptoWorkerFactory);
+    this.ws = {
+      badge,
+      cachedNextUrl: {},
+      cryptoApi: new CryptoApi(cryptoWorkerFactory),
+      db,
+      http,
+      notifier,
+      speculativePayData: undefined,
+      memoProcessReserve: new AsyncOpMemo<void>(),
+    };
+  }
+
+  getExchangePaytoUri(exchangeBaseUrl: string, supportedTargetTypes: string[]) 
{
+    return getExchangePaytoUri(this.ws, exchangeBaseUrl, supportedTargetTypes);
+  }
+
+
+  getWithdrawDetailsForAmount(baseUrl: any, amount: AmountJson): any {
+    return getWithdrawDetailsForAmount(this.ws, baseUrl, amount);
   }
 
   /**
@@ -443,7 +216,7 @@ export class Wallet {
         await this.updateExchangeFromUrl(pending.exchangeBaseUrl);
         break;
       case "planchet":
-        await this.processPlanchet(pending.coinPub);
+        // Nothing to do, since the withdraw session will process the planchet
         break;
       case "refresh":
         await this.processRefreshSession(pending.refreshSessionId);
@@ -535,272 +308,6 @@ export class Wallet {
     );
   }
 
-  private async getCoinsForReturn(
-    exchangeBaseUrl: string,
-    amount: AmountJson,
-  ): Promise<CoinWithDenom[] | undefined> {
-    const exchange = await oneShotGet(
-      this.db,
-      Stores.exchanges,
-      exchangeBaseUrl,
-    );
-    if (!exchange) {
-      throw Error(`Exchange ${exchangeBaseUrl} not known to the wallet`);
-    }
-
-    const coins: CoinRecord[] = await oneShotIterIndex(
-      this.db,
-      Stores.coins.exchangeBaseUrlIndex,
-      exchange.baseUrl,
-    ).toArray();
-
-    if (!coins || !coins.length) {
-      return [];
-    }
-
-    const denoms = await oneShotIterIndex(
-      this.db,
-      Stores.denominations.exchangeBaseUrlIndex,
-      exchange.baseUrl,
-    ).toArray();
-
-    // Denomination of the first coin, we assume that all other
-    // coins have the same currency
-    const firstDenom = await oneShotGet(this.db, Stores.denominations, [
-      exchange.baseUrl,
-      coins[0].denomPub,
-    ]);
-    if (!firstDenom) {
-      throw Error("db inconsistent");
-    }
-    const currency = firstDenom.value.currency;
-
-    const cds: CoinWithDenom[] = [];
-    for (const coin of coins) {
-      const denom = await oneShotGet(this.db, Stores.denominations, [
-        exchange.baseUrl,
-        coin.denomPub,
-      ]);
-      if (!denom) {
-        throw Error("db inconsistent");
-      }
-      if (denom.value.currency !== currency) {
-        console.warn(
-          `same pubkey for different currencies at exchange 
${exchange.baseUrl}`,
-        );
-        continue;
-      }
-      if (coin.suspended) {
-        continue;
-      }
-      if (coin.status !== CoinStatus.Fresh) {
-        continue;
-      }
-      cds.push({ coin, denom });
-    }
-
-    const res = selectPayCoins(denoms, cds, amount, amount);
-    if (res) {
-      return res.cds;
-    }
-    return undefined;
-  }
-
-  /**
-   * Get exchanges and associated coins that are still spendable, but only
-   * if the sum the coins' remaining value covers the payment amount and fees.
-   */
-  private async getCoinsForPayment(
-    args: CoinsForPaymentArgs,
-  ): Promise<CoinSelectionResult | undefined> {
-    const {
-      allowedAuditors,
-      allowedExchanges,
-      depositFeeLimit,
-      paymentAmount,
-      wireFeeAmortization,
-      wireFeeLimit,
-      wireFeeTime,
-      wireMethod,
-    } = args;
-
-    let remainingAmount = paymentAmount;
-
-    const exchanges = await oneShotIter(this.db, Stores.exchanges).toArray();
-
-    for (const exchange of exchanges) {
-      let isOkay: boolean = false;
-      const exchangeDetails = exchange.details;
-      if (!exchangeDetails) {
-        continue;
-      }
-      const exchangeFees = exchange.wireInfo;
-      if (!exchangeFees) {
-        continue;
-      }
-
-      // is the exchange explicitly allowed?
-      for (const allowedExchange of allowedExchanges) {
-        if (allowedExchange.master_pub === exchangeDetails.masterPublicKey) {
-          isOkay = true;
-          break;
-        }
-      }
-
-      // is the exchange allowed because of one of its auditors?
-      if (!isOkay) {
-        for (const allowedAuditor of allowedAuditors) {
-          for (const auditor of exchangeDetails.auditors) {
-            if (auditor.auditor_pub === allowedAuditor.auditor_pub) {
-              isOkay = true;
-              break;
-            }
-          }
-          if (isOkay) {
-            break;
-          }
-        }
-      }
-
-      if (!isOkay) {
-        continue;
-      }
-
-      const coins = await oneShotIterIndex(
-        this.db,
-        Stores.coins.exchangeBaseUrlIndex,
-        exchange.baseUrl,
-      ).toArray();
-
-      const denoms = await oneShotIterIndex(
-        this.db,
-        Stores.denominations.exchangeBaseUrlIndex,
-        exchange.baseUrl,
-      ).toArray();
-
-      if (!coins || coins.length === 0) {
-        continue;
-      }
-
-      // Denomination of the first coin, we assume that all other
-      // coins have the same currency
-      const firstDenom = await oneShotGet(this.db, Stores.denominations, [
-        exchange.baseUrl,
-        coins[0].denomPub,
-      ]);
-      if (!firstDenom) {
-        throw Error("db inconsistent");
-      }
-      const currency = firstDenom.value.currency;
-      const cds: CoinWithDenom[] = [];
-      for (const coin of coins) {
-        const denom = await oneShotGet(this.db, Stores.denominations, [
-          exchange.baseUrl,
-          coin.denomPub,
-        ]);
-        if (!denom) {
-          throw Error("db inconsistent");
-        }
-        if (denom.value.currency !== currency) {
-          console.warn(
-            `same pubkey for different currencies at exchange 
${exchange.baseUrl}`,
-          );
-          continue;
-        }
-        if (coin.suspended) {
-          continue;
-        }
-        if (coin.status !== CoinStatus.Fresh) {
-          continue;
-        }
-        cds.push({ coin, denom });
-      }
-
-      let totalFees = Amounts.getZero(currency);
-      let wireFee: AmountJson | undefined;
-      for (const fee of exchangeFees.feesForType[wireMethod] || []) {
-        if (fee.startStamp <= wireFeeTime && fee.endStamp >= wireFeeTime) {
-          wireFee = fee.wireFee;
-          break;
-        }
-      }
-
-      if (wireFee) {
-        const amortizedWireFee = Amounts.divide(wireFee, wireFeeAmortization);
-        if (Amounts.cmp(wireFeeLimit, amortizedWireFee) < 0) {
-          totalFees = Amounts.add(amortizedWireFee, totalFees).amount;
-          remainingAmount = Amounts.add(amortizedWireFee, remainingAmount)
-            .amount;
-        }
-      }
-
-      const res = selectPayCoins(denoms, cds, remainingAmount, 
depositFeeLimit);
-
-      if (res) {
-        totalFees = Amounts.add(totalFees, res.totalFees).amount;
-        return {
-          cds: res.cds,
-          exchangeUrl: exchange.baseUrl,
-          totalAmount: remainingAmount,
-          totalFees,
-        };
-      }
-    }
-    return undefined;
-  }
-
-  /**
-   * Record all information that is necessary to
-   * pay for a proposal in the wallet's database.
-   */
-  private async recordConfirmPay(
-    proposal: ProposalRecord,
-    payCoinInfo: PayCoinInfo,
-    chosenExchange: string,
-  ): Promise<PurchaseRecord> {
-    const payReq: PayReq = {
-      coins: payCoinInfo.sigs,
-      merchant_pub: proposal.contractTerms.merchant_pub,
-      mode: "pay",
-      order_id: proposal.contractTerms.order_id,
-    };
-    const t: PurchaseRecord = {
-      abortDone: false,
-      abortRequested: false,
-      contractTerms: proposal.contractTerms,
-      contractTermsHash: proposal.contractTermsHash,
-      finished: false,
-      lastSessionId: undefined,
-      merchantSig: proposal.merchantSig,
-      payReq,
-      refundsDone: {},
-      refundsPending: {},
-      timestamp: getTimestampNow(),
-      timestamp_refund: undefined,
-    };
-
-    await runWithWriteTransaction(
-      this.db,
-      [Stores.coins, Stores.purchases],
-      async tx => {
-        await tx.put(Stores.purchases, t);
-        for (let c of payCoinInfo.updatedCoins) {
-          await tx.put(Stores.coins, c);
-        }
-      },
-    );
-
-    this.badge.showNotification();
-    this.notifier.notify();
-    return t;
-  }
-
-  getNextUrl(contractTerms: ContractTerms): string {
-    const fu = new URI(contractTerms.fulfillment_url);
-    fu.addSearch("order_id", contractTerms.order_id);
-    return fu.href();
-  }
-
   /**
    * Check if a payment for the given taler://pay/ URI is possible.
    *
@@ -808,305 +315,7 @@ export class Wallet {
    * yet send to the merchant.
    */
   async preparePay(talerPayUri: string): Promise<PreparePayResult> {
-    const uriResult = parsePayUri(talerPayUri);
-
-    if (!uriResult) {
-      return {
-        status: "error",
-        error: "URI not supported",
-      };
-    }
-
-    let proposalId: string;
-    try {
-      proposalId = await this.downloadProposal(
-        uriResult.downloadUrl,
-        uriResult.sessionId,
-      );
-    } catch (e) {
-      return {
-        status: "error",
-        error: e.toString(),
-      };
-    }
-    const proposal = await this.getProposal(proposalId);
-    if (!proposal) {
-      throw Error(`could not get proposal ${proposalId}`);
-    }
-
-    console.log("proposal", proposal);
-
-    const differentPurchase = await oneShotGetIndexed(
-      this.db,
-      Stores.purchases.fulfillmentUrlIndex,
-      proposal.contractTerms.fulfillment_url,
-    );
-
-    let fulfillmentUrl = proposal.contractTerms.fulfillment_url;
-    let doublePurchaseDetection = false;
-    if (fulfillmentUrl.startsWith("http")) {
-      doublePurchaseDetection = true;
-    }
-
-    if (differentPurchase && doublePurchaseDetection) {
-      // We do this check to prevent merchant B to find out if we bought a
-      // digital product with merchant A by abusing the existing payment
-      // redirect feature.
-      if (
-        differentPurchase.contractTerms.merchant_pub !=
-        proposal.contractTerms.merchant_pub
-      ) {
-        console.warn(
-          "merchant with different public key offered contract with same 
fulfillment URL as an existing purchase",
-        );
-      } else {
-        if (uriResult.sessionId) {
-          await this.submitPay(
-            differentPurchase.contractTermsHash,
-            uriResult.sessionId,
-          );
-        }
-        return {
-          status: "paid",
-          contractTerms: differentPurchase.contractTerms,
-          nextUrl: this.getNextUrl(differentPurchase.contractTerms),
-        };
-      }
-    }
-
-    // First check if we already payed for it.
-    const purchase = await oneShotGet(
-      this.db,
-      Stores.purchases,
-      proposal.contractTermsHash,
-    );
-
-    if (!purchase) {
-      const paymentAmount = 
Amounts.parseOrThrow(proposal.contractTerms.amount);
-      let wireFeeLimit;
-      if (proposal.contractTerms.max_wire_fee) {
-        wireFeeLimit = Amounts.parseOrThrow(
-          proposal.contractTerms.max_wire_fee,
-        );
-      } else {
-        wireFeeLimit = Amounts.getZero(paymentAmount.currency);
-      }
-      // If not already payed, check if we could pay for it.
-      const res = await this.getCoinsForPayment({
-        allowedAuditors: proposal.contractTerms.auditors,
-        allowedExchanges: proposal.contractTerms.exchanges,
-        depositFeeLimit: Amounts.parseOrThrow(proposal.contractTerms.max_fee),
-        paymentAmount,
-        wireFeeAmortization: proposal.contractTerms.wire_fee_amortization || 1,
-        wireFeeLimit,
-        // FIXME: parse this properly
-        wireFeeTime: extractTalerStamp(proposal.contractTerms.timestamp) || {
-          t_ms: 0,
-        },
-        wireMethod: proposal.contractTerms.wire_method,
-      });
-
-      if (!res) {
-        console.log("not confirming payment, insufficient coins");
-        return {
-          status: "insufficient-balance",
-          contractTerms: proposal.contractTerms,
-          proposalId: proposal.proposalId,
-        };
-      }
-
-      // Only create speculative signature if we don't already have one for 
this proposal
-      if (
-        !this.speculativePayData ||
-        (this.speculativePayData &&
-          this.speculativePayData.orderDownloadId !== proposalId)
-      ) {
-        const { exchangeUrl, cds, totalAmount } = res;
-        const payCoinInfo = await this.cryptoApi.signDeposit(
-          proposal.contractTerms,
-          cds,
-          totalAmount,
-        );
-        this.speculativePayData = {
-          exchangeUrl,
-          payCoinInfo,
-          proposal,
-          orderDownloadId: proposalId,
-        };
-        Wallet.enableTracing &&
-          console.log("created speculative pay data for payment");
-      }
-
-      return {
-        status: "payment-possible",
-        contractTerms: proposal.contractTerms,
-        proposalId: proposal.proposalId,
-        totalFees: res.totalFees,
-      };
-    }
-
-    if (uriResult.sessionId) {
-      await this.submitPay(purchase.contractTermsHash, uriResult.sessionId);
-    }
-
-    return {
-      status: "paid",
-      contractTerms: proposal.contractTerms,
-      nextUrl: this.getNextUrl(purchase.contractTerms),
-    };
-  }
-
-  /**
-   * Download a proposal and store it in the database.
-   * Returns an id for it to retrieve it later.
-   *
-   * @param sessionId Current session ID, if the proposal is being
-   *  downloaded in the context of a session ID.
-   */
-  async downloadProposal(url: string, sessionId?: string): Promise<string> {
-    const oldProposal = await oneShotGetIndexed(
-      this.db,
-      Stores.proposals.urlIndex,
-      url,
-    );
-    if (oldProposal) {
-      return oldProposal.proposalId;
-    }
-
-    const { priv, pub } = await this.cryptoApi.createEddsaKeypair();
-    const parsed_url = new URI(url);
-    const urlWithNonce = parsed_url.setQuery({ nonce: pub }).href();
-    console.log("downloading contract from '" + urlWithNonce + "'");
-    let resp;
-    try {
-      resp = await this.http.get(urlWithNonce);
-    } catch (e) {
-      console.log("contract download failed", e);
-      throw e;
-    }
-
-    const proposal = Proposal.checked(resp.responseJson);
-
-    const contractTermsHash = await this.hashContract(proposal.contract_terms);
-
-    const proposalId = encodeCrock(getRandomBytes(32));
-
-    const proposalRecord: ProposalRecord = {
-      contractTerms: proposal.contract_terms,
-      contractTermsHash,
-      merchantSig: proposal.sig,
-      noncePriv: priv,
-      timestamp: getTimestampNow(),
-      url,
-      downloadSessionId: sessionId,
-      proposalId: proposalId,
-      proposalStatus: ProposalStatus.PROPOSED,
-    };
-    await oneShotPut(this.db, Stores.proposals, proposalRecord);
-    this.notifier.notify();
-
-    return proposalId;
-  }
-
-  async refundFailedPay(proposalId: number) {
-    console.log(`refunding failed payment with proposal id ${proposalId}`);
-    const proposal = await oneShotGet(this.db, Stores.proposals, proposalId);
-    if (!proposal) {
-      throw Error(`proposal with id ${proposalId} not found`);
-    }
-
-    const purchase = await oneShotGet(
-      this.db,
-      Stores.purchases,
-      proposal.contractTermsHash,
-    );
-
-    if (!purchase) {
-      throw Error("purchase not found for proposal");
-    }
-
-    if (purchase.finished) {
-      throw Error("can't auto-refund finished purchase");
-    }
-  }
-
-  async submitPay(
-    contractTermsHash: string,
-    sessionId: string | undefined,
-  ): Promise<ConfirmPayResult> {
-    const purchase = await oneShotGet(
-      this.db,
-      Stores.purchases,
-      contractTermsHash,
-    );
-    if (!purchase) {
-      throw Error("Purchase not found: " + contractTermsHash);
-    }
-    if (purchase.abortRequested) {
-      throw Error("not submitting payment for aborted purchase");
-    }
-    let resp;
-    const payReq = { ...purchase.payReq, session_id: sessionId };
-
-    const payUrl = new URI("pay")
-      .absoluteTo(purchase.contractTerms.merchant_base_url)
-      .href();
-
-    try {
-      resp = await this.http.postJson(payUrl, payReq);
-    } catch (e) {
-      // Gives the user the option to retry / abort and refresh
-      console.log("payment failed", e);
-      throw e;
-    }
-    const merchantResp = resp.responseJson;
-    console.log("got success from pay URL");
-
-    const merchantPub = purchase.contractTerms.merchant_pub;
-    const valid: boolean = await this.cryptoApi.isValidPaymentSignature(
-      merchantResp.sig,
-      contractTermsHash,
-      merchantPub,
-    );
-    if (!valid) {
-      console.error("merchant payment signature invalid");
-      // FIXME: properly display error
-      throw Error("merchant payment signature invalid");
-    }
-    purchase.finished = true;
-    const modifiedCoins: CoinRecord[] = [];
-    for (const pc of purchase.payReq.coins) {
-      const c = await oneShotGet(this.db, Stores.coins, pc.coin_pub);
-      if (!c) {
-        console.error("coin not found");
-        throw Error("coin used in payment not found");
-      }
-      c.status = CoinStatus.Dirty;
-      modifiedCoins.push(c);
-    }
-
-    await runWithWriteTransaction(
-      this.db,
-      [Stores.coins, Stores.purchases],
-      async tx => {
-        for (let c of modifiedCoins) {
-          tx.put(Stores.coins, c);
-        }
-        tx.put(Stores.purchases, purchase);
-      },
-    );
-
-    for (const c of purchase.payReq.coins) {
-      this.refresh(c.coin_pub);
-    }
-
-    const nextUrl = this.getNextUrl(purchase.contractTerms);
-    this.cachedNextUrl[purchase.contractTerms.fulfillment_url] = {
-      nextUrl,
-      lastSessionId: sessionId,
-    };
-
-    return { nextUrl };
+    return preparePay(this.ws, talerPayUri);
   }
 
   /**
@@ -1138,218 +347,8 @@ export class Wallet {
     proposalId: string,
     sessionIdOverride: string | undefined,
   ): Promise<ConfirmPayResult> {
-    Wallet.enableTracing &&
-      console.log(
-        `executing confirmPay with proposalId ${proposalId} and 
sessionIdOverride ${sessionIdOverride}`,
-      );
-    const proposal = await oneShotGet(this.db, Stores.proposals, proposalId);
-
-    if (!proposal) {
-      throw Error(`proposal with id ${proposalId} not found`);
-    }
-
-    const sessionId = sessionIdOverride || proposal.downloadSessionId;
-
-    let purchase = await oneShotGet(
-      this.db,
-      Stores.purchases,
-      proposal.contractTermsHash,
-    );
-
-    if (purchase) {
-      return this.submitPay(purchase.contractTermsHash, sessionId);
-    }
-
-    const contractAmount = Amounts.parseOrThrow(proposal.contractTerms.amount);
-
-    let wireFeeLimit;
-    if (!proposal.contractTerms.max_wire_fee) {
-      wireFeeLimit = Amounts.getZero(contractAmount.currency);
-    } else {
-      wireFeeLimit = Amounts.parseOrThrow(proposal.contractTerms.max_wire_fee);
-    }
-
-    const res = await this.getCoinsForPayment({
-      allowedAuditors: proposal.contractTerms.auditors,
-      allowedExchanges: proposal.contractTerms.exchanges,
-      depositFeeLimit: Amounts.parseOrThrow(proposal.contractTerms.max_fee),
-      paymentAmount: Amounts.parseOrThrow(proposal.contractTerms.amount),
-      wireFeeAmortization: proposal.contractTerms.wire_fee_amortization || 1,
-      wireFeeLimit,
-      // FIXME: parse this properly
-      wireFeeTime: extractTalerStamp(proposal.contractTerms.timestamp) || {
-        t_ms: 0,
-      },
-      wireMethod: proposal.contractTerms.wire_method,
-    });
-
-    Wallet.enableTracing && console.log("coin selection result", res);
-
-    if (!res) {
-      // Should not happen, since checkPay should be called first
-      console.log("not confirming payment, insufficient coins");
-      throw Error("insufficient balance");
-    }
-
-    const sd = await this.getSpeculativePayData(proposalId);
-    if (!sd) {
-      const { exchangeUrl, cds, totalAmount } = res;
-      const payCoinInfo = await this.cryptoApi.signDeposit(
-        proposal.contractTerms,
-        cds,
-        totalAmount,
-      );
-      purchase = await this.recordConfirmPay(
-        proposal,
-        payCoinInfo,
-        exchangeUrl,
-      );
-    } else {
-      purchase = await this.recordConfirmPay(
-        sd.proposal,
-        sd.payCoinInfo,
-        sd.exchangeUrl,
-      );
-    }
-
-    return this.submitPay(purchase.contractTermsHash, sessionId);
-  }
-
-  /**
-   * Get the speculative pay data, but only if coins have not changed in 
between.
-   */
-  async getSpeculativePayData(
-    proposalId: string,
-  ): Promise<SpeculativePayData | undefined> {
-    const sp = this.speculativePayData;
-    if (!sp) {
-      return;
-    }
-    if (sp.orderDownloadId !== proposalId) {
-      return;
-    }
-    const coinKeys = sp.payCoinInfo.updatedCoins.map(x => x.coinPub);
-    const coins: CoinRecord[] = [];
-    for (let coinKey of coinKeys) {
-      const cc = await oneShotGet(this.db, Stores.coins, coinKey);
-      if (cc) {
-        coins.push(cc);
-      }
-    }
-    for (let i = 0; i < coins.length; i++) {
-      const specCoin = sp.payCoinInfo.originalCoins[i];
-      const currentCoin = coins[i];
-
-      // Coin does not exist anymore!
-      if (!currentCoin) {
-        return;
-      }
-      if (
-        Amounts.cmp(specCoin.currentAmount, currentCoin.currentAmount) !== 0
-      ) {
-        return;
-      }
-    }
-    return sp;
-  }
-
-  private async processReserveBankStatus(reservePub: string): Promise<void> {
-    let reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
-    switch (reserve?.reserveStatus) {
-      case ReserveRecordStatus.WAIT_CONFIRM_BANK:
-      case ReserveRecordStatus.REGISTERING_BANK:
-        break;
-      default:
-        return;
-    }
-    const bankStatusUrl = reserve.bankWithdrawStatusUrl;
-    if (!bankStatusUrl) {
-      return;
-    }
-
-    let status: WithdrawOperationStatusResponse;
-    try {
-      const statusResp = await this.http.get(bankStatusUrl);
-      status = 
WithdrawOperationStatusResponse.checked(statusResp.responseJson);
-    } catch (e) {
-      throw e;
-    }
-
-    if (status.selection_done) {
-      if (reserve.reserveStatus === ReserveRecordStatus.REGISTERING_BANK) {
-        await this.registerReserveWithBank(reservePub);
-        return await this.processReserveBankStatus(reservePub);
-      }
-    } else {
-      await this.registerReserveWithBank(reservePub);
-      return await this.processReserveBankStatus(reservePub);
-    }
-
-    if (status.transfer_done) {
-      await oneShotMutate(this.db, Stores.reserves, reservePub, r => {
-        switch (r.reserveStatus) {
-          case ReserveRecordStatus.REGISTERING_BANK:
-          case ReserveRecordStatus.WAIT_CONFIRM_BANK:
-            break;
-          default:
-            return;
-        }
-        const now = getTimestampNow();
-        r.timestampConfirmed = now;
-        r.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
-        return r;
-      });
-      await this.processReserveImpl(reservePub);
-    } else {
-      await oneShotMutate(this.db, Stores.reserves, reservePub, r => {
-        switch (r.reserveStatus) {
-          case ReserveRecordStatus.WAIT_CONFIRM_BANK:
-            break;
-          default:
-            return;
-        }
-        r.bankWithdrawConfirmUrl = status.confirm_transfer_url;
-        return r;
-      });
-    }
-  }
-
-  async registerReserveWithBank(reservePub: string) {
-    let reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
-    switch (reserve?.reserveStatus) {
-      case ReserveRecordStatus.WAIT_CONFIRM_BANK:
-      case ReserveRecordStatus.REGISTERING_BANK:
-        break;
-      default:
-        return;
-    }
-    const bankStatusUrl = reserve.bankWithdrawStatusUrl;
-    if (!bankStatusUrl) {
-      return;
-    }
-    console.log("making selection");
-    if (reserve.timestampReserveInfoPosted) {
-      throw Error("bank claims that reserve info selection is not done");
-    }
-    const bankResp = await this.http.postJson(bankStatusUrl, {
-      reserve_pub: reservePub,
-      selected_exchange: reserve.exchangeWire,
-    });
-    console.log("got response", bankResp);
-    await oneShotMutate(this.db, Stores.reserves, reservePub, r => {
-      switch (r.reserveStatus) {
-        case ReserveRecordStatus.REGISTERING_BANK:
-        case ReserveRecordStatus.WAIT_CONFIRM_BANK:
-          break;
-        default:
-          return;
-      }
-      r.timestampReserveInfoPosted = getTimestampNow();
-      r.reserveStatus = ReserveRecordStatus.WAIT_CONFIRM_BANK;
-      return r;
-    });
-    return this.processReserveBankStatus(reservePub);
-  }
+    return confirmPay(this.ws, proposalId, sessionIdOverride);
+  }
 
   /**
    * First fetch information requred to withdraw from the reserve,
@@ -1359,149 +358,7 @@ export class Wallet {
    * state DORMANT.
    */
   async processReserve(reservePub: string): Promise<void> {
-    const p = this.memoProcessReserve.find(reservePub);
-    if (p) {
-      return p;
-    } else {
-      return this.memoProcessReserve.put(
-        reservePub,
-        this.processReserveImpl(reservePub),
-      );
-    }
-  }
-
-  private async processReserveImpl(reservePub: string): Promise<void> {
-    const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
-    if (!reserve) {
-      console.log("not processing reserve: reserve does not exist");
-      return;
-    }
-    logger.trace(
-      `Processing reserve ${reservePub} with status ${reserve.reserveStatus}`,
-    );
-    switch (reserve.reserveStatus) {
-      case ReserveRecordStatus.UNCONFIRMED:
-        // nothing to do
-        break;
-      case ReserveRecordStatus.REGISTERING_BANK:
-        await this.processReserveBankStatus(reservePub);
-        return this.processReserveImpl(reservePub);
-      case ReserveRecordStatus.QUERYING_STATUS:
-        await this.updateReserve(reservePub);
-        return this.processReserveImpl(reservePub);
-      case ReserveRecordStatus.WITHDRAWING:
-        await this.depleteReserve(reservePub);
-        break;
-      case ReserveRecordStatus.DORMANT:
-        // nothing to do
-        break;
-      case ReserveRecordStatus.WAIT_CONFIRM_BANK:
-        await this.processReserveBankStatus(reservePub);
-        break;
-      default:
-        console.warn("unknown reserve record status:", reserve.reserveStatus);
-        assertUnreachable(reserve.reserveStatus);
-        break;
-    }
-  }
-
-  /**
-   * Given a planchet, withdraw a coin from the exchange.
-   */
-  private async processPlanchet(coinPub: string): Promise<void> {
-    logger.trace("process planchet", coinPub);
-    const planchet = await oneShotGet(this.db, Stores.planchets, coinPub);
-    if (!planchet) {
-      console.log("processPlanchet: planchet not found");
-      return;
-    }
-    const exchange = await oneShotGet(
-      this.db,
-      Stores.exchanges,
-      planchet.exchangeBaseUrl,
-    );
-    if (!exchange) {
-      console.error("db inconsistent: exchange for planchet not found");
-      return;
-    }
-
-    const denom = await oneShotGet(this.db, Stores.denominations, [
-      planchet.exchangeBaseUrl,
-      planchet.denomPub,
-    ]);
-
-    if (!denom) {
-      console.error("db inconsistent: denom for planchet not found");
-      return;
-    }
-
-    const wd: any = {};
-    wd.denom_pub_hash = planchet.denomPubHash;
-    wd.reserve_pub = planchet.reservePub;
-    wd.reserve_sig = planchet.withdrawSig;
-    wd.coin_ev = planchet.coinEv;
-    const reqUrl = new URI("reserve/withdraw").absoluteTo(exchange.baseUrl);
-    const resp = await this.http.postJson(reqUrl.href(), wd);
-
-    const r = resp.responseJson;
-
-    const denomSig = await this.cryptoApi.rsaUnblind(
-      r.ev_sig,
-      planchet.blindingKey,
-      planchet.denomPub,
-    );
-
-    const coin: CoinRecord = {
-      blindingKey: planchet.blindingKey,
-      coinPriv: planchet.coinPriv,
-      coinPub: planchet.coinPub,
-      currentAmount: planchet.coinValue,
-      denomPub: planchet.denomPub,
-      denomPubHash: planchet.denomPubHash,
-      denomSig,
-      exchangeBaseUrl: planchet.exchangeBaseUrl,
-      reservePub: planchet.reservePub,
-      status: CoinStatus.Fresh,
-      coinIndex: planchet.coinIndex,
-      withdrawSessionId: planchet.withdrawSessionId,
-    };
-
-    await runWithWriteTransaction(
-      this.db,
-      [Stores.planchets, Stores.coins, Stores.withdrawalSession, 
Stores.reserves],
-      async tx => {
-        const currentPc = await tx.get(Stores.planchets, coin.coinPub);
-        if (!currentPc) {
-          return;
-        }
-        const ws = await tx.get(
-          Stores.withdrawalSession,
-          planchet.withdrawSessionId,
-        );
-        if (!ws) {
-          return;
-        }
-        if (ws.withdrawn[planchet.coinIndex]) {
-          // Already withdrawn
-          return;
-        }
-        ws.withdrawn[planchet.coinIndex] = true;
-        await tx.put(Stores.withdrawalSession, ws);
-        const r = await tx.get(Stores.reserves, planchet.reservePub);
-        if (!r) {
-          return;
-        }
-        r.withdrawCompletedAmount = Amounts.add(
-          r.withdrawCompletedAmount,
-          Amounts.add(denom.value, denom.feeWithdraw).amount,
-        ).amount;
-        tx.put(Stores.reserves, r);
-        await tx.delete(Stores.planchets, coin.coinPub);
-        await tx.add(Stores.coins, coin);
-      },
-    );
-    this.notifier.notify();
-    logger.trace(`withdraw of one coin ${coin.coinPub} finished`);
+    return processReserve(this.ws, reservePub);
   }
 
   /**
@@ -1513,119 +370,7 @@ export class Wallet {
   async createReserve(
     req: CreateReserveRequest,
   ): Promise<CreateReserveResponse> {
-    const keypair = await this.cryptoApi.createEddsaKeypair();
-    const now = getTimestampNow();
-    const canonExchange = canonicalizeBaseUrl(req.exchange);
-
-    let reserveStatus;
-    if (req.bankWithdrawStatusUrl) {
-      reserveStatus = ReserveRecordStatus.REGISTERING_BANK;
-    } else {
-      reserveStatus = ReserveRecordStatus.UNCONFIRMED;
-    }
-
-    const currency = req.amount.currency;
-
-    const reserveRecord: ReserveRecord = {
-      created: now,
-      withdrawAllocatedAmount: Amounts.getZero(currency),
-      withdrawCompletedAmount: Amounts.getZero(currency),
-      withdrawRemainingAmount: Amounts.getZero(currency),
-      exchangeBaseUrl: canonExchange,
-      hasPayback: false,
-      initiallyRequestedAmount: req.amount,
-      reservePriv: keypair.priv,
-      reservePub: keypair.pub,
-      senderWire: req.senderWire,
-      timestampConfirmed: undefined,
-      timestampReserveInfoPosted: undefined,
-      bankWithdrawStatusUrl: req.bankWithdrawStatusUrl,
-      exchangeWire: req.exchangeWire,
-      reserveStatus,
-      lastStatusQuery: undefined,
-    };
-
-    const senderWire = req.senderWire;
-    if (senderWire) {
-      const rec = {
-        paytoUri: senderWire,
-      };
-      await oneShotPut(this.db, Stores.senderWires, rec);
-    }
-
-    const exchangeInfo = await this.updateExchangeFromUrl(req.exchange);
-    const exchangeDetails = exchangeInfo.details;
-    if (!exchangeDetails) {
-      throw Error("exchange not updated");
-    }
-    const { isAudited, isTrusted } = await this.getExchangeTrust(exchangeInfo);
-    let currencyRecord = await oneShotGet(
-      this.db,
-      Stores.currencies,
-      exchangeDetails.currency,
-    );
-    if (!currencyRecord) {
-      currencyRecord = {
-        auditors: [],
-        exchanges: [],
-        fractionalDigits: 2,
-        name: exchangeDetails.currency,
-      };
-    }
-
-    if (!isAudited && !isTrusted) {
-      currencyRecord.exchanges.push({
-        baseUrl: req.exchange,
-        exchangePub: exchangeDetails.masterPublicKey,
-      });
-    }
-
-    const cr: CurrencyRecord = currencyRecord;
-
-    const resp = await runWithWriteTransaction(
-      this.db,
-      [Stores.currencies, Stores.reserves, Stores.bankWithdrawUris],
-      async tx => {
-        // Check if we have already created a reserve for that 
bankWithdrawStatusUrl
-        if (reserveRecord.bankWithdrawStatusUrl) {
-          const bwi = await tx.get(
-            Stores.bankWithdrawUris,
-            reserveRecord.bankWithdrawStatusUrl,
-          );
-          if (bwi) {
-            const otherReserve = await tx.get(Stores.reserves, bwi.reservePub);
-            if (otherReserve) {
-              logger.trace(
-                "returning existing reserve for bankWithdrawStatusUri",
-              );
-              return {
-                exchange: otherReserve.exchangeBaseUrl,
-                reservePub: otherReserve.reservePub,
-              };
-            }
-          }
-          await tx.put(Stores.bankWithdrawUris, {
-            reservePub: reserveRecord.reservePub,
-            talerWithdrawUri: reserveRecord.bankWithdrawStatusUrl,
-          });
-        }
-        await tx.put(Stores.currencies, cr);
-        await tx.put(Stores.reserves, reserveRecord);
-        const r: CreateReserveResponse = {
-          exchange: canonExchange,
-          reservePub: keypair.pub,
-        };
-        return r;
-      },
-    );
-
-    // Asynchronously process the reserve, but return
-    // to the caller already.
-    this.processReserve(resp.reservePub).catch(e => {
-      console.error("Processing reserve failed:", e);
-    });
-
-    return resp;
+    return createReserve(this.ws, req);
   }
 
   /**
@@ -1638,1342 +383,60 @@ export class Wallet {
    * an unconfirmed reserve should be hidden.
    */
   async confirmReserve(req: ConfirmReserveRequest): Promise<void> {
-    const now = getTimestampNow();
-    await oneShotMutate(this.db, Stores.reserves, req.reservePub, reserve => {
-      if (reserve.reserveStatus !== ReserveRecordStatus.UNCONFIRMED) {
-        return;
-      }
-      reserve.timestampConfirmed = now;
-      reserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
-      return reserve;
-    });
-
-    this.notifier.notify();
+    return confirmReserve(this.ws, req);
+  }
 
-    this.processReserve(req.reservePub).catch(e => {
-      console.log("processing reserve failed:", e);
-    });
+  private async processWithdrawSession(
+    withdrawalSessionId: string,
+  ): Promise<void> {
+    return processWithdrawSession(this.ws, withdrawalSessionId);
   }
 
   /**
-   * Withdraw coins from a reserve until it is empty.
-   *
-   * When finished, marks the reserve as depleted by setting
-   * the depleted timestamp.
+   * Check if and how an exchange is trusted and/or audited.
    */
-  private async depleteReserve(reservePub: string): Promise<void> {
-    const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
-    if (!reserve) {
-      return;
-    }
-    if (reserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
-      return;
-    }
-    logger.trace(`depleting reserve ${reservePub}`);
-
-    const withdrawAmount = reserve.withdrawRemainingAmount;
-
-    logger.trace(`getting denom list`);
-
-    const denomsForWithdraw = await this.getVerifiedWithdrawDenomList(
-      reserve.exchangeBaseUrl,
-      withdrawAmount,
-    );
-    logger.trace(`got denom list`);
-    if (denomsForWithdraw.length === 0) {
-      const m = `Unable to withdraw from reserve, no denominations are 
available to withdraw.`;
-      await this.setReserveError(reserve.reservePub, {
-        type: "internal",
-        message: m,
-        details: {},
-      });
-      console.log(m);
-      throw new OperationFailedAndReportedError(m);
-    }
-
-    logger.trace("selected denominations");
-
-    const withdrawalSessionId = encodeCrock(randomBytes(32));
-
-    const withdrawalRecord: WithdrawalSessionRecord = {
-      withdrawSessionId: withdrawalSessionId,
-      reservePub: reserve.reservePub,
-      withdrawalAmount: Amounts.toString(withdrawAmount),
-      startTimestamp: getTimestampNow(),
-      denoms: denomsForWithdraw.map(x => x.denomPub),
-      withdrawn: denomsForWithdraw.map(x => false),
-      planchetCreated: denomsForWithdraw.map(x => false),
-    };
-
-    const totalCoinValue = Amounts.sum(denomsForWithdraw.map(x => x.value))
-      .amount;
-    const totalCoinWithdrawFee = Amounts.sum(
-      denomsForWithdraw.map(x => x.feeWithdraw),
-    ).amount;
-    const totalWithdrawAmount = Amounts.add(
-      totalCoinValue,
-      totalCoinWithdrawFee,
-    ).amount;
-
-    function mutateReserve(r: ReserveRecord): ReserveRecord {
-      const remaining = Amounts.sub(
-        r.withdrawRemainingAmount,
-        totalWithdrawAmount,
-      );
-      if (remaining.saturated) {
-        console.error("can't create planchets, saturated");
-        throw TransactionAbort;
-      }
-      const allocated = Amounts.add(
-        r.withdrawAllocatedAmount,
-        totalWithdrawAmount,
-      );
-      if (allocated.saturated) {
-        console.error("can't create planchets, saturated");
-        throw TransactionAbort;
-      }
-      r.withdrawRemainingAmount = remaining.amount;
-      r.withdrawAllocatedAmount = allocated.amount;
-      r.reserveStatus = ReserveRecordStatus.DORMANT;
-
-      return r;
-    }
-
-    const success = await runWithWriteTransaction(
-      this.db,
-      [Stores.planchets, Stores.withdrawalSession, Stores.reserves],
-      async tx => {
-        const myReserve = await tx.get(Stores.reserves, reservePub);
-        if (!myReserve) {
-          return false;
-        }
-        if (myReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
-          return false;
-        }
-        await tx.mutate(Stores.reserves, reserve.reservePub, mutateReserve);
-        await tx.put(Stores.withdrawalSession, withdrawalRecord);
-        return true;
-      },
-    );
-
-    if (success) {
-      console.log("processing new withdraw session");
-      await this.processWithdrawSession(withdrawalSessionId);
-    } else {
-      console.trace("withdraw session already existed");
-    }
-  }
-
-  private async processWithdrawSession(withdrawalSessionId: string): 
Promise<void> {
-    logger.trace("processing withdraw session", withdrawalSessionId);
-    const ws = await oneShotGet(
-      this.db,
-      Stores.withdrawalSession,
-      withdrawalSessionId,
-    );
-    if (!ws) {
-      logger.trace("withdraw session doesn't exist");
-      return;
-    }
-
-    const ps = ws.denoms.map((d, i) =>
-      this.processWithdrawCoin(withdrawalSessionId, i),
-    );
-    await Promise.all(ps);
-    this.badge.showNotification();
-    return;
+  async getExchangeTrust(
+    exchangeInfo: ExchangeRecord,
+  ): Promise<{ isTrusted: boolean; isAudited: boolean }> {
+    return getExchangeTrust(this.ws, exchangeInfo);
   }
 
-  private async processWithdrawCoin(
-    withdrawalSessionId: string,
-    coinIndex: number,
-  ) {
-    logger.info("starting withdraw for coin");
-    const ws = await oneShotGet(
-      this.db,
-      Stores.withdrawalSession,
-      withdrawalSessionId,
-    );
-    if (!ws) {
-      console.log("ws doesn't exist");
-      return;
-    }
-
-    const coin = await oneShotGetIndexed(
-      this.db,
-      Stores.coins.byWithdrawalWithIdx,
-      [withdrawalSessionId, coinIndex],
-    );
-
-    if (coin) {
-      console.log("coin already exists");
-      return;
-    }
-
-    const pc = await oneShotGetIndexed(
-      this.db,
-      Stores.planchets.byWithdrawalWithIdx,
-      [withdrawalSessionId, coinIndex],
+  async getWithdrawDetailsForUri(
+    talerWithdrawUri: string,
+    maybeSelectedExchange?: string,
+  ): Promise<WithdrawDetails> {
+    return getWithdrawDetailsForUri(
+      this.ws,
+      talerWithdrawUri,
+      maybeSelectedExchange,
     );
-
-    if (pc) {
-      return this.processPlanchet(pc.coinPub);
-    } else {
-      const reserve = await oneShotGet(this.db, Stores.reserves, 
ws.reservePub);
-      if (!reserve) {
-        return;
-      }
-      const denom = await oneShotGet(this.db, Stores.denominations, [
-        reserve.exchangeBaseUrl,
-        ws.denoms[coinIndex],
-      ]);
-      if (!denom) {
-        return;
-      }
-      const r = await this.cryptoApi.createPlanchet(denom, reserve);
-      const newPlanchet: PlanchetRecord = {
-        blindingKey: r.blindingKey,
-        coinEv: r.coinEv,
-        coinIndex,
-        coinPriv: r.coinPriv,
-        coinPub: r.coinPub,
-        coinValue: r.coinValue,
-        denomPub: r.denomPub,
-        denomPubHash: r.denomPubHash,
-        exchangeBaseUrl: r.exchangeBaseUrl,
-        isFromTip: false,
-        reservePub: r.reservePub,
-        withdrawSessionId: withdrawalSessionId,
-        withdrawSig: r.withdrawSig,
-      };
-      await runWithWriteTransaction(this.db, [Stores.planchets, 
Stores.withdrawalSession], async (tx) => {
-        const myWs = await tx.get(Stores.withdrawalSession, 
withdrawalSessionId);
-        if (!myWs) {
-          return;
-        }
-        if (myWs.planchetCreated[coinIndex]) {
-          return;
-        }
-        await tx.put(Stores.planchets, newPlanchet);
-      });
-      await this.processPlanchet(newPlanchet.coinPub);
-    }
   }
 
   /**
-   * Update the information about a reserve that is stored in the wallet
-   * by quering the reserve's exchange.
+   * Update or add exchange DB entry by fetching the /keys and /wire 
information.
+   * Optionally link the reserve entry to the new or existing
+   * exchange entry in then DB.
    */
-  private async updateReserve(reservePub: string): Promise<void> {
-    const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
-    if (!reserve) {
-      throw Error("reserve not in db");
-    }
-
-    if (reserve.timestampConfirmed === undefined) {
-      throw Error("reserve not confirmed yet");
-    }
-
-    if (reserve.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
-      return;
-    }
-
-    const reqUrl = new URI("reserve/status").absoluteTo(
-      reserve.exchangeBaseUrl,
-    );
-    reqUrl.query({ reserve_pub: reservePub });
-    let resp;
-    try {
-      resp = await this.http.get(reqUrl.href());
-    } catch (e) {
-      if (e.response?.status === 404) {
-        return;
-      } else {
-        const m = e.message;
-        this.setReserveError(reservePub, {
-          type: "network",
-          details: {},
-          message: m,
-        });
-        throw new OperationFailedAndReportedError(m);
-      }
-    }
-    const reserveInfo = ReserveStatus.checked(resp.responseJson);
-    const balance = Amounts.parseOrThrow(reserveInfo.balance);
-    await oneShotMutate(this.db, Stores.reserves, reserve.reservePub, r => {
-      if (r.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
-        return;
-      }
-
-      // FIXME: check / compare history!
-      if (!r.lastStatusQuery) {
-        // FIXME: check if this matches initial expectations
-        r.withdrawRemainingAmount = balance;
-      } else {
-        const expectedBalance = Amounts.sub(
-          r.withdrawAllocatedAmount,
-          r.withdrawCompletedAmount,
-        );
-        const cmp = Amounts.cmp(balance, expectedBalance.amount);
-        if (cmp == 0) {
-          // Nothing changed.
-          return;
-        }
-        if (cmp > 0) {
-          const extra = Amounts.sub(balance, expectedBalance.amount).amount;
-          r.withdrawRemainingAmount = Amounts.add(
-            r.withdrawRemainingAmount,
-            extra,
-          ).amount;
-        } else {
-          // We're missing some money.
-        }
-      }
-      r.lastStatusQuery = getTimestampNow();
-      r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
-      return r;
-    });
-    this.notifier.notify();
-  }
-
-  async getPossibleDenoms(
-    exchangeBaseUrl: string,
-  ): Promise<DenominationRecord[]> {
-    return await oneShotIterIndex(
-      this.db,
-      Stores.denominations.exchangeBaseUrlIndex,
-      exchangeBaseUrl,
-    ).filter(d => {
-      return (
-        d.status === DenominationStatus.Unverified ||
-        d.status === DenominationStatus.VerifiedGood
-      );
-    });
+  async updateExchangeFromUrl(
+    baseUrl: string,
+    force: boolean = false,
+  ): Promise<ExchangeRecord> {
+    return updateExchangeFromUrl(this.ws, baseUrl, force);
   }
 
   /**
-   * Compute the smallest withdrawable amount possible, based on verified 
denominations.
-   *
-   * Writes to the DB in order to record the result from verifying
-   * denominations.
+   * Get detailed balance information, sliced by exchange and by currency.
    */
-  async getVerifiedSmallestWithdrawAmount(
-    exchangeBaseUrl: string,
-  ): Promise<AmountJson> {
-    const exchange = await oneShotGet(
-      this.db,
-      Stores.exchanges,
-      exchangeBaseUrl,
-    );
-    if (!exchange) {
-      throw Error(`exchange ${exchangeBaseUrl} not found`);
-    }
-    const exchangeDetails = exchange.details;
-    if (!exchangeDetails) {
-      throw Error(`exchange ${exchangeBaseUrl} details not available`);
-    }
-
-    const possibleDenoms = await this.getPossibleDenoms(exchange.baseUrl);
-
-    possibleDenoms.sort((d1, d2) => {
-      const a1 = Amounts.add(d1.feeWithdraw, d1.value).amount;
-      const a2 = Amounts.add(d2.feeWithdraw, d2.value).amount;
-      return Amounts.cmp(a1, a2);
-    });
+  async getBalances(): Promise<WalletBalance> {
+    return getBalances(this.ws);
+  }
 
-    for (const denom of possibleDenoms) {
-      if (denom.status === DenominationStatus.VerifiedGood) {
-        return Amounts.add(denom.feeWithdraw, denom.value).amount;
-      }
-      const valid = await this.cryptoApi.isValidDenom(
-        denom,
-        exchangeDetails.masterPublicKey,
-      );
-      if (!valid) {
-        denom.status = DenominationStatus.VerifiedBad;
-      } else {
-        denom.status = DenominationStatus.VerifiedGood;
-      }
-      await oneShotPut(this.db, Stores.denominations, denom);
-      if (valid) {
-        return Amounts.add(denom.feeWithdraw, denom.value).amount;
-      }
-    }
-    return Amounts.getZero(exchangeDetails.currency);
+  async refresh(oldCoinPub: string, force: boolean = false): Promise<void> {
+    return refresh(this.ws, oldCoinPub, force);
   }
 
-  /**
-   * Get a list of denominations to withdraw from the given exchange for the
-   * given amount, making sure that all denominations' signatures are verified.
-   *
-   * Writes to the DB in order to record the result from verifying
-   * denominations.
-   */
-  async getVerifiedWithdrawDenomList(
-    exchangeBaseUrl: string,
-    amount: AmountJson,
-  ): Promise<DenominationRecord[]> {
-    const exchange = await oneShotGet(
-      this.db,
-      Stores.exchanges,
-      exchangeBaseUrl,
-    );
-    if (!exchange) {
-      console.log("exchange not found");
-      throw Error(`exchange ${exchangeBaseUrl} not found`);
-    }
-    const exchangeDetails = exchange.details;
-    if (!exchangeDetails) {
-      console.log("exchange details not available");
-      throw Error(`exchange ${exchangeBaseUrl} details not available`);
-    }
-
-    console.log("getting possible denoms");
-
-    const possibleDenoms = await this.getPossibleDenoms(exchange.baseUrl);
-
-    console.log("got possible denoms");
-
-    let allValid = false;
-
-    let selectedDenoms: DenominationRecord[];
-
-    do {
-      allValid = true;
-      const nextPossibleDenoms = [];
-      selectedDenoms = getWithdrawDenomList(amount, possibleDenoms);
-      console.log("got withdraw denom list");
-      for (const denom of selectedDenoms || []) {
-        if (denom.status === DenominationStatus.Unverified) {
-          console.log("checking validity", denom, 
exchangeDetails.masterPublicKey);
-          const valid = await this.cryptoApi.isValidDenom(
-            denom,
-            exchangeDetails.masterPublicKey,
-          );
-          console.log("done checking validity");
-          if (!valid) {
-            denom.status = DenominationStatus.VerifiedBad;
-            allValid = false;
-          } else {
-            denom.status = DenominationStatus.VerifiedGood;
-            nextPossibleDenoms.push(denom);
-          }
-          await oneShotPut(this.db, Stores.denominations, denom);
-        } else {
-          nextPossibleDenoms.push(denom);
-        }
-      }
-    } while (selectedDenoms.length > 0 && !allValid);
-
-    console.log("returning denoms");
-
-    return selectedDenoms;
-  }
-
-  /**
-   * Check if and how an exchange is trusted and/or audited.
-   */
-  async getExchangeTrust(
-    exchangeInfo: ExchangeRecord,
-  ): Promise<{ isTrusted: boolean; isAudited: boolean }> {
-    let isTrusted = false;
-    let isAudited = false;
-    const exchangeDetails = exchangeInfo.details;
-    if (!exchangeDetails) {
-      throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
-    }
-    const currencyRecord = await oneShotGet(
-      this.db,
-      Stores.currencies,
-      exchangeDetails.currency,
-    );
-    if (currencyRecord) {
-      for (const trustedExchange of currencyRecord.exchanges) {
-        if (trustedExchange.exchangePub === exchangeDetails.masterPublicKey) {
-          isTrusted = true;
-          break;
-        }
-      }
-      for (const trustedAuditor of currencyRecord.auditors) {
-        for (const exchangeAuditor of exchangeDetails.auditors) {
-          if (trustedAuditor.auditorPub === exchangeAuditor.auditor_pub) {
-            isAudited = true;
-            break;
-          }
-        }
-      }
-    }
-    return { isTrusted, isAudited };
-  }
-
-  async getWithdrawDetailsForUri(
-    talerWithdrawUri: string,
-    maybeSelectedExchange?: string,
-  ): Promise<WithdrawDetails> {
-    const info = await this.getWithdrawalInfo(talerWithdrawUri);
-    let rci: ReserveCreationInfo | undefined = undefined;
-    if (maybeSelectedExchange) {
-      rci = await this.getWithdrawDetailsForAmount(
-        maybeSelectedExchange,
-        info.amount,
-      );
-    }
-    return {
-      withdrawInfo: info,
-      reserveCreationInfo: rci,
-    };
-  }
-
-  async getWithdrawDetailsForAmount(
-    baseUrl: string,
-    amount: AmountJson,
-  ): Promise<ReserveCreationInfo> {
-    const exchangeInfo = await this.updateExchangeFromUrl(baseUrl);
-    const exchangeDetails = exchangeInfo.details;
-    if (!exchangeDetails) {
-      throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
-    }
-    const exchangeWireInfo = exchangeInfo.wireInfo;
-    if (!exchangeWireInfo) {
-      throw Error(
-        `exchange ${exchangeInfo.baseUrl} wire details not available`,
-      );
-    }
-
-    const selectedDenoms = await this.getVerifiedWithdrawDenomList(
-      baseUrl,
-      amount,
-    );
-    let acc = Amounts.getZero(amount.currency);
-    for (const d of selectedDenoms) {
-      acc = Amounts.add(acc, d.feeWithdraw).amount;
-    }
-    const actualCoinCost = selectedDenoms
-      .map(
-        (d: DenominationRecord) => Amounts.add(d.value, d.feeWithdraw).amount,
-      )
-      .reduce((a, b) => Amounts.add(a, b).amount);
-
-    const exchangeWireAccounts: string[] = [];
-    for (let account of exchangeWireInfo.accounts) {
-      exchangeWireAccounts.push(account.url);
-    }
-
-    const { isTrusted, isAudited } = await this.getExchangeTrust(exchangeInfo);
-
-    let earliestDepositExpiration = selectedDenoms[0].stampExpireDeposit;
-    for (let i = 1; i < selectedDenoms.length; i++) {
-      const expireDeposit = selectedDenoms[i].stampExpireDeposit;
-      if (expireDeposit.t_ms < earliestDepositExpiration.t_ms) {
-        earliestDepositExpiration = expireDeposit;
-      }
-    }
-
-    const possibleDenoms = await oneShotIterIndex(
-      this.db,
-      Stores.denominations.exchangeBaseUrlIndex,
-      baseUrl,
-    ).filter(d => d.isOffered);
-
-    const trustedAuditorPubs = [];
-    const currencyRecord = await oneShotGet(
-      this.db,
-      Stores.currencies,
-      amount.currency,
-    );
-    if (currencyRecord) {
-      trustedAuditorPubs.push(
-        ...currencyRecord.auditors.map(a => a.auditorPub),
-      );
-    }
-
-    let versionMatch;
-    if (exchangeDetails.protocolVersion) {
-      versionMatch = LibtoolVersion.compare(
-        WALLET_PROTOCOL_VERSION,
-        exchangeDetails.protocolVersion,
-      );
-
-      if (
-        versionMatch &&
-        !versionMatch.compatible &&
-        versionMatch.currentCmp === -1
-      ) {
-        console.warn(
-          `wallet version ${WALLET_PROTOCOL_VERSION} might be outdated ` +
-            `(exchange has ${exchangeDetails.protocolVersion}), checking for 
updates`,
-        );
-        if (isFirefox()) {
-          console.log("skipping update check on Firefox");
-        } else {
-          chrome.runtime.requestUpdateCheck((status, details) => {
-            console.log("update check status:", status);
-          });
-        }
-      }
-    }
-
-    const ret: ReserveCreationInfo = {
-      earliestDepositExpiration,
-      exchangeInfo,
-      exchangeWireAccounts,
-      exchangeVersion: exchangeDetails.protocolVersion || "unknown",
-      isAudited,
-      isTrusted,
-      numOfferedDenoms: possibleDenoms.length,
-      overhead: Amounts.sub(amount, actualCoinCost).amount,
-      selectedDenoms,
-      trustedAuditorPubs,
-      versionMatch,
-      walletVersion: WALLET_PROTOCOL_VERSION,
-      wireFees: exchangeWireInfo,
-      withdrawFee: acc,
-    };
-    return ret;
-  }
-
-  async getExchangePaytoUri(
-    exchangeBaseUrl: string,
-    supportedTargetTypes: string[],
-  ): Promise<string> {
-    // We do the update here, since the exchange might not even exist
-    // yet in our database.
-    const exchangeRecord = await this.updateExchangeFromUrl(exchangeBaseUrl);
-    if (!exchangeRecord) {
-      throw Error(`Exchange '${exchangeBaseUrl}' not found.`);
-    }
-    const exchangeWireInfo = exchangeRecord.wireInfo;
-    if (!exchangeWireInfo) {
-      throw Error(`Exchange wire info for '${exchangeBaseUrl}' not found.`);
-    }
-    for (let account of exchangeWireInfo.accounts) {
-      const paytoUri = new URI(account.url);
-      if (supportedTargetTypes.includes(paytoUri.authority())) {
-        return account.url;
-      }
-    }
-    throw Error("no matching exchange account found");
-  }
-
-  /**
-   * Update or add exchange DB entry by fetching the /keys and /wire 
information.
-   * Optionally link the reserve entry to the new or existing
-   * exchange entry in then DB.
-   */
-  async updateExchangeFromUrl(
-    baseUrl: string,
-    force: boolean = false,
-  ): Promise<ExchangeRecord> {
-    const now = getTimestampNow();
-    baseUrl = canonicalizeBaseUrl(baseUrl);
-
-    const r = await oneShotGet(this.db, Stores.exchanges, baseUrl);
-    if (!r) {
-      const newExchangeRecord: ExchangeRecord = {
-        baseUrl: baseUrl,
-        details: undefined,
-        wireInfo: undefined,
-        updateStatus: ExchangeUpdateStatus.FETCH_KEYS,
-        updateStarted: now,
-        updateReason: "initial",
-        timestampAdded: getTimestampNow(),
-      };
-      await oneShotPut(this.db, Stores.exchanges, newExchangeRecord);
-    } else {
-      await runWithWriteTransaction(this.db, [Stores.exchanges], async t => {
-        const rec = await t.get(Stores.exchanges, baseUrl);
-        if (!rec) {
-          return;
-        }
-        if (rec.updateStatus != ExchangeUpdateStatus.FETCH_KEYS && !force) {
-          return;
-        }
-        if (rec.updateStatus != ExchangeUpdateStatus.FETCH_KEYS && force) {
-          rec.updateReason = "forced";
-        }
-        rec.updateStarted = now;
-        rec.updateStatus = ExchangeUpdateStatus.FETCH_KEYS;
-        rec.lastError = undefined;
-        t.put(Stores.exchanges, rec);
-      });
-    }
-
-    await this.updateExchangeWithKeys(baseUrl);
-    await this.updateExchangeWithWireInfo(baseUrl);
-
-    const updatedExchange = await oneShotGet(
-      this.db,
-      Stores.exchanges,
-      baseUrl,
-    );
-
-    if (!updatedExchange) {
-      // This should practically never happen
-      throw Error("exchange not found");
-    }
-    return updatedExchange;
-  }
-
-  private async setExchangeError(
-    baseUrl: string,
-    err: OperationError,
-  ): Promise<void> {
-    const mut = (exchange: ExchangeRecord) => {
-      exchange.lastError = err;
-      return exchange;
-    };
-    await oneShotMutate(this.db, Stores.exchanges, baseUrl, mut);
-  }
-
-  private async setReserveError(
-    reservePub: string,
-    err: OperationError,
-  ): Promise<void> {
-    const mut = (reserve: ReserveRecord) => {
-      reserve.lastError = err;
-      return reserve;
-    };
-    await oneShotMutate(this.db, Stores.reserves, reservePub, mut);
-  }
-
-  /**
-   * Fetch the exchange's /keys and update our database accordingly.
-   *
-   * Exceptions thrown in this method must be caught and reported
-   * in the pending operations.
-   */
-  private async updateExchangeWithKeys(baseUrl: string): Promise<void> {
-    const existingExchangeRecord = await oneShotGet(
-      this.db,
-      Stores.exchanges,
-      baseUrl,
-    );
-
-    if (
-      existingExchangeRecord?.updateStatus != ExchangeUpdateStatus.FETCH_KEYS
-    ) {
-      return;
-    }
-    const keysUrl = new URI("keys")
-      .absoluteTo(baseUrl)
-      .addQuery("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
-    let keysResp;
-    try {
-      keysResp = await this.http.get(keysUrl.href());
-    } catch (e) {
-      const m = `Fetching keys failed: ${e.message}`;
-      await this.setExchangeError(baseUrl, {
-        type: "network",
-        details: {
-          requestUrl: e.config?.url,
-        },
-        message: m,
-      });
-      throw new OperationFailedAndReportedError(m);
-    }
-    let exchangeKeysJson: KeysJson;
-    try {
-      exchangeKeysJson = KeysJson.checked(keysResp.responseJson);
-    } catch (e) {
-      const m = `Parsing /keys response failed: ${e.message}`;
-      await this.setExchangeError(baseUrl, {
-        type: "protocol-violation",
-        details: {},
-        message: m,
-      });
-      throw new OperationFailedAndReportedError(m);
-    }
-
-    const lastUpdateTimestamp = extractTalerStamp(
-      exchangeKeysJson.list_issue_date,
-    );
-    if (!lastUpdateTimestamp) {
-      const m = `Parsing /keys response failed: invalid list_issue_date.`;
-      await this.setExchangeError(baseUrl, {
-        type: "protocol-violation",
-        details: {},
-        message: m,
-      });
-      throw new OperationFailedAndReportedError(m);
-    }
-
-    if (exchangeKeysJson.denoms.length === 0) {
-      const m = "exchange doesn't offer any denominations";
-      await this.setExchangeError(baseUrl, {
-        type: "protocol-violation",
-        details: {},
-        message: m,
-      });
-      throw new OperationFailedAndReportedError(m);
-    }
-
-    const protocolVersion = exchangeKeysJson.version;
-    if (!protocolVersion) {
-      const m = "outdate exchange, no version in /keys response";
-      await this.setExchangeError(baseUrl, {
-        type: "protocol-violation",
-        details: {},
-        message: m,
-      });
-      throw new OperationFailedAndReportedError(m);
-    }
-
-    const currency = Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value)
-      .currency;
-
-    const newDenominations = await Promise.all(
-      exchangeKeysJson.denoms.map(d =>
-        this.denominationRecordFromKeys(baseUrl, d),
-      ),
-    );
-
-    await runWithWriteTransaction(
-      this.db,
-      [Stores.exchanges, Stores.denominations],
-      async tx => {
-        const r = await tx.get(Stores.exchanges, baseUrl);
-        if (!r) {
-          console.warn(`exchange ${baseUrl} no longer present`);
-          return;
-        }
-        if (r.details) {
-          // FIXME: We need to do some consistency checks!
-        }
-        r.details = {
-          auditors: exchangeKeysJson.auditors,
-          currency: currency,
-          lastUpdateTime: lastUpdateTimestamp,
-          masterPublicKey: exchangeKeysJson.master_public_key,
-          protocolVersion: protocolVersion,
-        };
-        r.updateStatus = ExchangeUpdateStatus.FETCH_WIRE;
-        r.lastError = undefined;
-        await tx.put(Stores.exchanges, r);
-
-        for (const newDenom of newDenominations) {
-          const oldDenom = await tx.get(Stores.denominations, [
-            baseUrl,
-            newDenom.denomPub,
-          ]);
-          if (oldDenom) {
-            // FIXME: Do consistency check
-          } else {
-            await tx.put(Stores.denominations, newDenom);
-          }
-        }
-      },
-    );
-  }
-
-  /**
-   * Fetch wire information for an exchange and store it in the database.
-   *
-   * @param exchangeBaseUrl Exchange base URL, assumed to be already 
normalized.
-   */
-  private async updateExchangeWithWireInfo(exchangeBaseUrl: string) {
-    const exchange = await this.findExchange(exchangeBaseUrl);
-    if (!exchange) {
-      return;
-    }
-    if (exchange.updateStatus != ExchangeUpdateStatus.FETCH_WIRE) {
-      return;
-    }
-    const reqUrl = new URI("wire")
-      .absoluteTo(exchangeBaseUrl)
-      .addQuery("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
-    const resp = await this.http.get(reqUrl.href());
-
-    const wiJson = resp.responseJson;
-    if (!wiJson) {
-      throw Error("/wire response malformed");
-    }
-    const wireInfo = ExchangeWireJson.checked(wiJson);
-    const feesForType: { [wireMethod: string]: WireFee[] } = {};
-    for (const wireMethod of Object.keys(wireInfo.fees)) {
-      const feeList: WireFee[] = [];
-      for (const x of wireInfo.fees[wireMethod]) {
-        const startStamp = extractTalerStamp(x.start_date);
-        if (!startStamp) {
-          throw Error("wrong date format");
-        }
-        const endStamp = extractTalerStamp(x.end_date);
-        if (!endStamp) {
-          throw Error("wrong date format");
-        }
-        feeList.push({
-          closingFee: Amounts.parseOrThrow(x.closing_fee),
-          endStamp,
-          sig: x.sig,
-          startStamp,
-          wireFee: Amounts.parseOrThrow(x.wire_fee),
-        });
-      }
-      feesForType[wireMethod] = feeList;
-    }
-
-    await runWithWriteTransaction(this.db, [Stores.exchanges], async tx => {
-      const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
-      if (!r) {
-        return;
-      }
-      if (r.updateStatus != ExchangeUpdateStatus.FETCH_WIRE) {
-        return;
-      }
-      r.wireInfo = {
-        accounts: wireInfo.accounts,
-        feesForType: feesForType,
-      };
-      r.updateStatus = ExchangeUpdateStatus.FINISHED;
-      r.lastError = undefined;
-      await tx.put(Stores.exchanges, r);
-    });
-  }
-
-  /**
-   * Get detailed balance information, sliced by exchange and by currency.
-   */
-  async getBalances(): Promise<WalletBalance> {
-    /**
-     * Add amount to a balance field, both for
-     * the slicing by exchange and currency.
-     */
-    function addTo(
-      balance: WalletBalance,
-      field: keyof WalletBalanceEntry,
-      amount: AmountJson,
-      exchange: string,
-    ): void {
-      const z = Amounts.getZero(amount.currency);
-      const balanceIdentity = {
-        available: z,
-        paybackAmount: z,
-        pendingIncoming: z,
-        pendingPayment: z,
-        pendingIncomingDirty: z,
-        pendingIncomingRefresh: z,
-        pendingIncomingWithdraw: z,
-      };
-      let entryCurr = balance.byCurrency[amount.currency];
-      if (!entryCurr) {
-        balance.byCurrency[amount.currency] = entryCurr = {
-          ...balanceIdentity,
-        };
-      }
-      let entryEx = balance.byExchange[exchange];
-      if (!entryEx) {
-        balance.byExchange[exchange] = entryEx = { ...balanceIdentity };
-      }
-      entryCurr[field] = Amounts.add(entryCurr[field], amount).amount;
-      entryEx[field] = Amounts.add(entryEx[field], amount).amount;
-    }
-
-    const balanceStore = {
-      byCurrency: {},
-      byExchange: {},
-    };
-
-    await runWithWriteTransaction(
-      this.db,
-      [Stores.coins, Stores.refresh, Stores.reserves, Stores.purchases],
-      async tx => {
-        await tx.iter(Stores.coins).forEach(c => {
-          if (c.suspended) {
-            return;
-          }
-          if (c.status === CoinStatus.Fresh) {
-            addTo(
-              balanceStore,
-              "available",
-              c.currentAmount,
-              c.exchangeBaseUrl,
-            );
-          }
-          if (c.status === CoinStatus.Dirty) {
-            addTo(
-              balanceStore,
-              "pendingIncoming",
-              c.currentAmount,
-              c.exchangeBaseUrl,
-            );
-            addTo(
-              balanceStore,
-              "pendingIncomingDirty",
-              c.currentAmount,
-              c.exchangeBaseUrl,
-            );
-          }
-        });
-        await tx.iter(Stores.refresh).forEach(r => {
-          // Don't count finished refreshes, since the refresh already resulted
-          // in coins being added to the wallet.
-          if (r.finished) {
-            return;
-          }
-          addTo(
-            balanceStore,
-            "pendingIncoming",
-            r.valueOutput,
-            r.exchangeBaseUrl,
-          );
-          addTo(
-            balanceStore,
-            "pendingIncomingRefresh",
-            r.valueOutput,
-            r.exchangeBaseUrl,
-          );
-        });
-
-        await tx.iter(Stores.purchases).forEach(t => {
-          if (t.finished) {
-            return;
-          }
-          for (const c of t.payReq.coins) {
-            addTo(
-              balanceStore,
-              "pendingPayment",
-              Amounts.parseOrThrow(c.contribution),
-              c.exchange_url,
-            );
-          }
-        });
-      },
-    );
-
-    Wallet.enableTracing && console.log("computed balances:", balanceStore);
-    return balanceStore;
-  }
-
-  async refresh(oldCoinPub: string, force: boolean = false): Promise<void> {
-    const coin = await oneShotGet(this.db, Stores.coins, oldCoinPub);
-    if (!coin) {
-      console.warn("can't refresh, coin not in database");
-      return;
-    }
-    switch (coin.status) {
-      case CoinStatus.Dirty:
-        break;
-      case CoinStatus.Dormant:
-        return;
-      case CoinStatus.Fresh:
-        if (!force) {
-          return;
-        }
-        break;
-    }
-
-    const exchange = await this.updateExchangeFromUrl(coin.exchangeBaseUrl);
-    if (!exchange) {
-      throw Error("db inconsistent: exchange of coin not found");
-    }
-
-    const oldDenom = await oneShotGet(this.db, Stores.denominations, [
-      exchange.baseUrl,
-      coin.denomPub,
-    ]);
-
-    if (!oldDenom) {
-      throw Error("db inconsistent: denomination for coin not found");
-    }
-
-    const availableDenoms: DenominationRecord[] = await oneShotIterIndex(
-      this.db,
-      Stores.denominations.exchangeBaseUrlIndex,
-      exchange.baseUrl,
-    ).toArray();
-
-    const availableAmount = Amounts.sub(coin.currentAmount, 
oldDenom.feeRefresh)
-      .amount;
-
-    const newCoinDenoms = getWithdrawDenomList(
-      availableAmount,
-      availableDenoms,
-    );
-
-    if (newCoinDenoms.length === 0) {
-      logger.trace(
-        `not refreshing, available amount ${amountToPretty(
-          availableAmount,
-        )} too small`,
-      );
-      await oneShotMutate(this.db, Stores.coins, oldCoinPub, x => {
-        if (x.status != coin.status) {
-          // Concurrent modification?
-          return;
-        }
-        x.status = CoinStatus.Dormant;
-        return x;
-      });
-      this.notifier.notify();
-      return;
-    }
-
-    const refreshSession: RefreshSessionRecord = await 
this.cryptoApi.createRefreshSession(
-      exchange.baseUrl,
-      3,
-      coin,
-      newCoinDenoms,
-      oldDenom.feeRefresh,
-    );
-
-    function mutateCoin(c: CoinRecord): CoinRecord {
-      const r = Amounts.sub(c.currentAmount, refreshSession.valueWithFee);
-      if (r.saturated) {
-        // Something else must have written the coin value
-        throw TransactionAbort;
-      }
-      c.currentAmount = r.amount;
-      c.status = CoinStatus.Dormant;
-      return c;
-    }
-
-    // Store refresh session and subtract refreshed amount from
-    // coin in the same transaction.
-    await runWithWriteTransaction(
-      this.db,
-      [Stores.refresh, Stores.coins],
-      async tx => {
-        await tx.put(Stores.refresh, refreshSession);
-        await tx.mutate(Stores.coins, coin.coinPub, mutateCoin);
-      },
-    );
-    logger.info(`created refresh session ${refreshSession.refreshSessionId}`);
-    this.notifier.notify();
-
-    await this.processRefreshSession(refreshSession.refreshSessionId);
-  }
-
-  async processRefreshSession(refreshSessionId: string) {
-    const refreshSession = await oneShotGet(
-      this.db,
-      Stores.refresh,
-      refreshSessionId,
-    );
-    if (!refreshSession) {
-      return;
-    }
-    if (refreshSession.finished) {
-      return;
-    }
-    if (typeof refreshSession.norevealIndex !== "number") {
-      await this.refreshMelt(refreshSession.refreshSessionId);
-    }
-    await this.refreshReveal(refreshSession.refreshSessionId);
-    logger.trace("refresh finished");
-  }
-
-  async refreshMelt(refreshSessionId: string): Promise<void> {
-    const refreshSession = await oneShotGet(
-      this.db,
-      Stores.refresh,
-      refreshSessionId,
-    );
-    if (!refreshSession) {
-      return;
-    }
-    if (refreshSession.norevealIndex !== undefined) {
-      return;
-    }
-
-    const coin = await oneShotGet(
-      this.db,
-      Stores.coins,
-      refreshSession.meltCoinPub,
-    );
-
-    if (!coin) {
-      console.error("can't melt coin, it does not exist");
-      return;
-    }
-
-    const reqUrl = new URI("refresh/melt").absoluteTo(
-      refreshSession.exchangeBaseUrl,
-    );
-    const meltReq = {
-      coin_pub: coin.coinPub,
-      confirm_sig: refreshSession.confirmSig,
-      denom_pub_hash: coin.denomPubHash,
-      denom_sig: coin.denomSig,
-      rc: refreshSession.hash,
-      value_with_fee: refreshSession.valueWithFee,
-    };
-    Wallet.enableTracing && console.log("melt request:", meltReq);
-    const resp = await this.http.postJson(reqUrl.href(), meltReq);
-
-    Wallet.enableTracing && console.log("melt response:", resp.responseJson);
-
-    if (resp.status !== 200) {
-      console.error(resp.responseJson);
-      throw Error("refresh failed");
-    }
-
-    const respJson = resp.responseJson;
-
-    const norevealIndex = respJson.noreveal_index;
-
-    if (typeof norevealIndex !== "number") {
-      throw Error("invalid response");
-    }
-
-    refreshSession.norevealIndex = norevealIndex;
-
-    await oneShotMutate(this.db, Stores.refresh, refreshSessionId, rs => {
-      if (rs.norevealIndex !== undefined) {
-        return;
-      }
-      if (rs.finished) {
-        return;
-      }
-      rs.norevealIndex = norevealIndex;
-      return rs;
-    });
-
-    this.notifier.notify();
-  }
-
-  private async refreshReveal(refreshSessionId: string): Promise<void> {
-    const refreshSession = await oneShotGet(
-      this.db,
-      Stores.refresh,
-      refreshSessionId,
-    );
-    if (!refreshSession) {
-      return;
-    }
-    const norevealIndex = refreshSession.norevealIndex;
-    if (norevealIndex === undefined) {
-      throw Error("can't reveal without melting first");
-    }
-    const privs = Array.from(refreshSession.transferPrivs);
-    privs.splice(norevealIndex, 1);
-
-    const planchets = refreshSession.planchetsForGammas[norevealIndex];
-    if (!planchets) {
-      throw Error("refresh index error");
-    }
-
-    const meltCoinRecord = await oneShotGet(
-      this.db,
-      Stores.coins,
-      refreshSession.meltCoinPub,
-    );
-    if (!meltCoinRecord) {
-      throw Error("inconsistent database");
-    }
-
-    const evs = planchets.map((x: RefreshPlanchetRecord) => x.coinEv);
-
-    const linkSigs: string[] = [];
-    for (let i = 0; i < refreshSession.newDenoms.length; i++) {
-      const linkSig = await this.cryptoApi.signCoinLink(
-        meltCoinRecord.coinPriv,
-        refreshSession.newDenomHashes[i],
-        refreshSession.meltCoinPub,
-        refreshSession.transferPubs[norevealIndex],
-        planchets[i].coinEv,
-      );
-      linkSigs.push(linkSig);
-    }
-
-    const req = {
-      coin_evs: evs,
-      new_denoms_h: refreshSession.newDenomHashes,
-      rc: refreshSession.hash,
-      transfer_privs: privs,
-      transfer_pub: refreshSession.transferPubs[norevealIndex],
-      link_sigs: linkSigs,
-    };
-
-    const reqUrl = new URI("refresh/reveal").absoluteTo(
-      refreshSession.exchangeBaseUrl,
-    );
-    Wallet.enableTracing && console.log("reveal request:", req);
-
-    let resp;
-    try {
-      resp = await this.http.postJson(reqUrl.href(), req);
-    } catch (e) {
-      console.error("got error during /refresh/reveal request");
-      console.error(e);
-      return;
-    }
-
-    Wallet.enableTracing && console.log("session:", refreshSession);
-    Wallet.enableTracing && console.log("reveal response:", resp);
-
-    if (resp.status !== 200) {
-      console.error("error: /refresh/reveal returned status " + resp.status);
-      return;
-    }
-
-    const respJson = resp.responseJson;
-
-    if (!respJson.ev_sigs || !Array.isArray(respJson.ev_sigs)) {
-      console.error("/refresh/reveal did not contain ev_sigs");
-      return;
-    }
-
-    const exchange = await this.findExchange(refreshSession.exchangeBaseUrl);
-    if (!exchange) {
-      console.error(`exchange ${refreshSession.exchangeBaseUrl} not found`);
-      return;
-    }
-
-    const coins: CoinRecord[] = [];
-
-    for (let i = 0; i < respJson.ev_sigs.length; i++) {
-      const denom = await oneShotGet(this.db, Stores.denominations, [
-        refreshSession.exchangeBaseUrl,
-        refreshSession.newDenoms[i],
-      ]);
-      if (!denom) {
-        console.error("denom not found");
-        continue;
-      }
-      const pc =
-        refreshSession.planchetsForGammas[refreshSession.norevealIndex!][i];
-      const denomSig = await this.cryptoApi.rsaUnblind(
-        respJson.ev_sigs[i].ev_sig,
-        pc.blindingKey,
-        denom.denomPub,
-      );
-      const coin: CoinRecord = {
-        blindingKey: pc.blindingKey,
-        coinPriv: pc.privateKey,
-        coinPub: pc.publicKey,
-        currentAmount: denom.value,
-        denomPub: denom.denomPub,
-        denomPubHash: denom.denomPubHash,
-        denomSig,
-        exchangeBaseUrl: refreshSession.exchangeBaseUrl,
-        reservePub: undefined,
-        status: CoinStatus.Fresh,
-        coinIndex: -1,
-        withdrawSessionId: "",
-      };
-
-      coins.push(coin);
-    }
-
-    refreshSession.finished = true;
-
-    await runWithWriteTransaction(
-      this.db,
-      [Stores.coins, Stores.refresh],
-      async tx => {
-        const rs = await tx.get(Stores.refresh, refreshSessionId);
-        if (!rs) {
-          return;
-        }
-        if (rs.finished) {
-          return;
-        }
-        for (let coin of coins) {
-          await tx.put(Stores.coins, coin);
-        }
-        await tx.put(Stores.refresh, refreshSession);
-      },
-    );
-    this.notifier.notify();
+  async processRefreshSession(refreshSessionId: string) {
+    return processRefreshSession(this.ws, refreshSessionId);
   }
 
   async findExchange(
@@ -2988,324 +451,11 @@ export class Wallet {
   async getHistory(
     historyQuery?: HistoryQuery,
   ): Promise<{ history: HistoryEvent[] }> {
-    const history: HistoryEvent[] = [];
-
-    // FIXME: do pagination instead of generating the full history
-
-    // We uniquely identify history rows via their timestamp.
-    // This works as timestamps are guaranteed to be monotonically
-    // increasing even
-
-    const proposals = await oneShotIter(this.db, Stores.proposals).toArray();
-    for (const p of proposals) {
-      history.push({
-        detail: {
-          contractTermsHash: p.contractTermsHash,
-          merchantName: p.contractTerms.merchant.name,
-        },
-        timestamp: p.timestamp,
-        type: "claim-order",
-        explicit: false,
-      });
-    }
-
-    const withdrawals = await oneShotIter(
-      this.db,
-      Stores.withdrawalSession,
-    ).toArray();
-    for (const w of withdrawals) {
-      history.push({
-        detail: {
-          withdrawalAmount: w.withdrawalAmount,
-        },
-        timestamp: w.startTimestamp,
-        type: "withdraw",
-        explicit: false,
-      });
-    }
-
-    const purchases = await oneShotIter(this.db, Stores.purchases).toArray();
-    for (const p of purchases) {
-      history.push({
-        detail: {
-          amount: p.contractTerms.amount,
-          contractTermsHash: p.contractTermsHash,
-          fulfillmentUrl: p.contractTerms.fulfillment_url,
-          merchantName: p.contractTerms.merchant.name,
-        },
-        timestamp: p.timestamp,
-        type: "pay",
-        explicit: false,
-      });
-      if (p.timestamp_refund) {
-        const contractAmount = Amounts.parseOrThrow(p.contractTerms.amount);
-        const amountsPending = Object.keys(p.refundsPending).map(x =>
-          Amounts.parseOrThrow(p.refundsPending[x].refund_amount),
-        );
-        const amountsDone = Object.keys(p.refundsDone).map(x =>
-          Amounts.parseOrThrow(p.refundsDone[x].refund_amount),
-        );
-        const amounts: AmountJson[] = amountsPending.concat(amountsDone);
-        const amount = Amounts.add(
-          Amounts.getZero(contractAmount.currency),
-          ...amounts,
-        ).amount;
-
-        history.push({
-          detail: {
-            contractTermsHash: p.contractTermsHash,
-            fulfillmentUrl: p.contractTerms.fulfillment_url,
-            merchantName: p.contractTerms.merchant.name,
-            refundAmount: amount,
-          },
-          timestamp: p.timestamp_refund,
-          type: "refund",
-          explicit: false,
-        });
-      }
-    }
-
-    const reserves = await oneShotIter(this.db, Stores.reserves).toArray();
-
-    for (const r of reserves) {
-      const reserveType = r.bankWithdrawStatusUrl ? "taler-bank" : "manual";
-      history.push({
-        detail: {
-          exchangeBaseUrl: r.exchangeBaseUrl,
-          requestedAmount: Amounts.toString(r.initiallyRequestedAmount),
-          reservePub: r.reservePub,
-          reserveType,
-          bankWithdrawStatusUrl: r.bankWithdrawStatusUrl,
-        },
-        timestamp: r.created,
-        type: "reserve-created",
-        explicit: false,
-      });
-      if (r.timestampConfirmed) {
-        history.push({
-          detail: {
-            exchangeBaseUrl: r.exchangeBaseUrl,
-            requestedAmount: Amounts.toString(r.initiallyRequestedAmount),
-            reservePub: r.reservePub,
-            reserveType,
-            bankWithdrawStatusUrl: r.bankWithdrawStatusUrl,
-          },
-          timestamp: r.created,
-          type: "reserve-confirmed",
-          explicit: false,
-        });
-      }
-    }
-
-    const tips: TipRecord[] = await oneShotIter(this.db, 
Stores.tips).toArray();
-    for (const tip of tips) {
-      history.push({
-        detail: {
-          accepted: tip.accepted,
-          amount: tip.amount,
-          merchantDomain: tip.merchantDomain,
-          tipId: tip.tipId,
-        },
-        timestamp: tip.timestamp,
-        explicit: false,
-        type: "tip",
-      });
-    }
-
-    await oneShotIter(this.db, Stores.exchanges).forEach(exchange => {
-      history.push({
-        type: "exchange-added",
-        explicit: false,
-        timestamp: exchange.timestampAdded,
-        detail: {
-          exchangeBaseUrl: exchange.baseUrl,
-        },
-      });
-    });
-
-    history.sort((h1, h2) => Math.sign(h1.timestamp.t_ms - h2.timestamp.t_ms));
-
-    return { history };
+    return getHistory(this.ws, historyQuery);
   }
 
   async getPendingOperations(): Promise<PendingOperationsResponse> {
-    const pendingOperations: PendingOperationInfo[] = [];
-    const exchanges = await this.getExchanges();
-    for (let e of exchanges) {
-      switch (e.updateStatus) {
-        case ExchangeUpdateStatus.FINISHED:
-          if (e.lastError) {
-            pendingOperations.push({
-              type: "bug",
-              message:
-                "Exchange record is in FINISHED state but has lastError set",
-              details: {
-                exchangeBaseUrl: e.baseUrl,
-              },
-            });
-          }
-          if (!e.details) {
-            pendingOperations.push({
-              type: "bug",
-              message:
-                "Exchange record does not have details, but no update in 
progress.",
-              details: {
-                exchangeBaseUrl: e.baseUrl,
-              },
-            });
-          }
-          if (!e.wireInfo) {
-            pendingOperations.push({
-              type: "bug",
-              message:
-                "Exchange record does not have wire info, but no update in 
progress.",
-              details: {
-                exchangeBaseUrl: e.baseUrl,
-              },
-            });
-          }
-          break;
-        case ExchangeUpdateStatus.FETCH_KEYS:
-          pendingOperations.push({
-            type: "exchange-update",
-            stage: "fetch-keys",
-            exchangeBaseUrl: e.baseUrl,
-            lastError: e.lastError,
-            reason: e.updateReason || "unknown",
-          });
-          break;
-        case ExchangeUpdateStatus.FETCH_WIRE:
-          pendingOperations.push({
-            type: "exchange-update",
-            stage: "fetch-wire",
-            exchangeBaseUrl: e.baseUrl,
-            lastError: e.lastError,
-            reason: e.updateReason || "unknown",
-          });
-          break;
-        default:
-          pendingOperations.push({
-            type: "bug",
-            message: "Unknown exchangeUpdateStatus",
-            details: {
-              exchangeBaseUrl: e.baseUrl,
-              exchangeUpdateStatus: e.updateStatus,
-            },
-          });
-          break;
-      }
-    }
-    await oneShotIter(this.db, Stores.reserves).forEach(reserve => {
-      const reserveType = reserve.bankWithdrawStatusUrl
-        ? "taler-bank"
-        : "manual";
-      switch (reserve.reserveStatus) {
-        case ReserveRecordStatus.DORMANT:
-          // nothing to report as pending
-          break;
-        case ReserveRecordStatus.WITHDRAWING:
-        case ReserveRecordStatus.UNCONFIRMED:
-        case ReserveRecordStatus.QUERYING_STATUS:
-        case ReserveRecordStatus.REGISTERING_BANK:
-          pendingOperations.push({
-            type: "reserve",
-            stage: reserve.reserveStatus,
-            timestampCreated: reserve.created,
-            reserveType,
-            reservePub: reserve.reservePub,
-          });
-          break;
-        case ReserveRecordStatus.WAIT_CONFIRM_BANK:
-          pendingOperations.push({
-            type: "reserve",
-            stage: reserve.reserveStatus,
-            timestampCreated: reserve.created,
-            reserveType,
-            reservePub: reserve.reservePub,
-            bankWithdrawConfirmUrl: reserve.bankWithdrawConfirmUrl,
-          });
-          break;
-        default:
-          pendingOperations.push({
-            type: "bug",
-            message: "Unknown reserve record status",
-            details: {
-              reservePub: reserve.reservePub,
-              reserveStatus: reserve.reserveStatus,
-            },
-          });
-          break;
-      }
-    });
-
-    await oneShotIter(this.db, Stores.refresh).forEach(r => {
-      if (r.finished) {
-        return;
-      }
-      let refreshStatus: string;
-      if (r.norevealIndex === undefined) {
-        refreshStatus = "melt";
-      } else {
-        refreshStatus = "reveal";
-      }
-
-      pendingOperations.push({
-        type: "refresh",
-        oldCoinPub: r.meltCoinPub,
-        refreshStatus,
-        refreshOutputSize: r.newDenoms.length,
-        refreshSessionId: r.refreshSessionId,
-      });
-    });
-
-    await oneShotIter(this.db, Stores.planchets).forEach(pc => {
-      pendingOperations.push({
-        type: "planchet",
-        coinPub: pc.coinPub,
-        reservePub: pc.reservePub,
-      });
-    });
-
-    await oneShotIter(this.db, Stores.coins).forEach(coin => {
-      if (coin.status == CoinStatus.Dirty) {
-        pendingOperations.push({
-          type: "dirty-coin",
-          coinPub: coin.coinPub,
-        });
-      }
-    });
-
-    await oneShotIter(this.db, Stores.withdrawalSession).forEach(ws => {
-      const numCoinsWithdrawn = ws.withdrawn.reduce(
-        (a, x) => a + (x ? 1 : 0),
-        0,
-      );
-      const numCoinsTotal = ws.withdrawn.length;
-      if (numCoinsWithdrawn < numCoinsTotal) {
-        pendingOperations.push({
-          type: "withdraw",
-          numCoinsTotal,
-          numCoinsWithdrawn,
-          reservePub: ws.reservePub,
-          withdrawSessionId: ws.withdrawSessionId,
-        });
-      }
-    });
-
-    await oneShotIter(this.db, Stores.proposals).forEach(proposal => {
-      if (proposal.proposalStatus == ProposalStatus.PROPOSED) {
-        pendingOperations.push({
-          type: "proposal",
-          merchantBaseUrl: proposal.contractTerms.merchant_base_url,
-          proposalId: proposal.proposalId,
-          proposalTimestamp: proposal.timestamp,
-        });
-      }
-    });
-
-    return {
-      pendingOperations,
-    };
+    return getPendingOperations(this.ws);
   }
 
   async getDenoms(exchangeUrl: string): Promise<DenominationRecord[]> {
@@ -3331,7 +481,7 @@ export class Wallet {
   }
 
   async updateCurrency(currencyRecord: CurrencyRecord): Promise<void> {
-    Wallet.enableTracing && console.log("updating currency to", 
currencyRecord);
+    logger.trace("updating currency to", currencyRecord);
     await oneShotPut(this.db, Stores.currencies, currencyRecord);
     this.notifier.notify();
   }
@@ -3352,107 +502,8 @@ export class Wallet {
     return await oneShotIter(this.db, Stores.coins).toArray();
   }
 
-  async getPlanchets(exchangeBaseUrl: string): Promise<PlanchetRecord[]> {
-    return await oneShotIter(this.db, Stores.planchets).filter(
-      c => c.exchangeBaseUrl === exchangeBaseUrl,
-    );
-  }
-
-  private async hashContract(contract: ContractTerms): Promise<string> {
-    return this.cryptoApi.hashString(canonicalJson(contract));
-  }
-
   async payback(coinPub: string): Promise<void> {
-    let coin = await oneShotGet(this.db, Stores.coins, coinPub);
-    if (!coin) {
-      throw Error(`Coin ${coinPub} not found, can't request payback`);
-    }
-    const reservePub = coin.reservePub;
-    if (!reservePub) {
-      throw Error(`Can't request payback for a refreshed coin`);
-    }
-    const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
-    if (!reserve) {
-      throw Error(`Reserve of coin ${coinPub} not found`);
-    }
-    switch (coin.status) {
-      case CoinStatus.Dormant:
-        throw Error(`Can't do payback for coin ${coinPub} since it's dormant`);
-    }
-    coin.status = CoinStatus.Dormant;
-    // Even if we didn't get the payback yet, we suspend withdrawal, since
-    // technically we might update reserve status before we get the response
-    // from the reserve for the payback request.
-    reserve.hasPayback = true;
-    await runWithWriteTransaction(
-      this.db,
-      [Stores.coins, Stores.reserves],
-      async tx => {
-        await tx.put(Stores.coins, coin!!);
-        await tx.put(Stores.reserves, reserve);
-      },
-    );
-    this.notifier.notify();
-
-    const paybackRequest = await this.cryptoApi.createPaybackRequest(coin);
-    const reqUrl = new URI("payback").absoluteTo(coin.exchangeBaseUrl);
-    const resp = await this.http.postJson(reqUrl.href(), paybackRequest);
-    if (resp.status !== 200) {
-      throw Error();
-    }
-    const paybackConfirmation = PaybackConfirmation.checked(resp.responseJson);
-    if (paybackConfirmation.reserve_pub !== coin.reservePub) {
-      throw Error(`Coin's reserve doesn't match reserve on payback`);
-    }
-    coin = await oneShotGet(this.db, Stores.coins, coinPub);
-    if (!coin) {
-      throw Error(`Coin ${coinPub} not found, can't confirm payback`);
-    }
-    coin.status = CoinStatus.Dormant;
-    await oneShotPut(this.db, Stores.coins, coin);
-    this.notifier.notify();
-    await this.updateReserve(reservePub!);
-  }
-
-  private async denominationRecordFromKeys(
-    exchangeBaseUrl: string,
-    denomIn: Denomination,
-  ): Promise<DenominationRecord> {
-    const denomPubHash = await this.cryptoApi.hashDenomPub(denomIn.denom_pub);
-    const d: DenominationRecord = {
-      denomPub: denomIn.denom_pub,
-      denomPubHash,
-      exchangeBaseUrl,
-      feeDeposit: Amounts.parseOrThrow(denomIn.fee_deposit),
-      feeRefresh: Amounts.parseOrThrow(denomIn.fee_refresh),
-      feeRefund: Amounts.parseOrThrow(denomIn.fee_refund),
-      feeWithdraw: Amounts.parseOrThrow(denomIn.fee_withdraw),
-      isOffered: true,
-      masterSig: denomIn.master_sig,
-      stampExpireDeposit: extractTalerStampOrThrow(
-        denomIn.stamp_expire_deposit,
-      ),
-      stampExpireLegal: extractTalerStampOrThrow(denomIn.stamp_expire_legal),
-      stampExpireWithdraw: extractTalerStampOrThrow(
-        denomIn.stamp_expire_withdraw,
-      ),
-      stampStart: extractTalerStampOrThrow(denomIn.stamp_start),
-      status: DenominationStatus.Unverified,
-      value: Amounts.parseOrThrow(denomIn.value),
-    };
-    return d;
-  }
-
-  async withdrawPaybackReserve(reservePub: string): Promise<void> {
-    const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
-    if (!reserve) {
-      throw Error(`Reserve ${reservePub} does not exist`);
-    }
-    reserve.hasPayback = false;
-    await oneShotPut(this.db, Stores.reserves, reserve);
-    this.depleteReserve(reserve.reservePub).catch(e => {
-      console.error("Error depleting reserve after payback", e);
-    });
+    return payback(this.ws, coinPub);
   }
 
   async getPaybackReserves(): Promise<ReserveRecord[]> {
@@ -3481,7 +532,6 @@ export class Wallet {
       Object.keys(wi.feesForType).map(k => s.add(k));
     });
 
-    Wallet.enableTracing && console.log(m);
     const exchangeWireTypes: { [url: string]: string[] } = {};
     Object.keys(m).map(e => {
       exchangeWireTypes[e] = Array.from(m[e]);
@@ -3504,202 +554,7 @@ export class Wallet {
    * Trigger paying coins back into the user's account.
    */
   async returnCoins(req: ReturnCoinsRequest): Promise<void> {
-    Wallet.enableTracing && console.log("got returnCoins request", req);
-    const wireType = (req.senderWire as any).type;
-    Wallet.enableTracing && console.log("wireType", wireType);
-    if (!wireType || typeof wireType !== "string") {
-      console.error(`wire type must be a non-empty string, not ${wireType}`);
-      return;
-    }
-    const stampSecNow = Math.floor(new Date().getTime() / 1000);
-    const exchange = await this.findExchange(req.exchange);
-    if (!exchange) {
-      console.error(`Exchange ${req.exchange} not known to the wallet`);
-      return;
-    }
-    const exchangeDetails = exchange.details;
-    if (!exchangeDetails) {
-      throw Error("exchange information needs to be updated first.");
-    }
-    Wallet.enableTracing && console.log("selecting coins for return:", req);
-    const cds = await this.getCoinsForReturn(req.exchange, req.amount);
-    Wallet.enableTracing && console.log(cds);
-
-    if (!cds) {
-      throw Error("coin return impossible, can't select coins");
-    }
-
-    const { priv, pub } = await this.cryptoApi.createEddsaKeypair();
-
-    const wireHash = await this.cryptoApi.hashString(
-      canonicalJson(req.senderWire),
-    );
-
-    const contractTerms: ContractTerms = {
-      H_wire: wireHash,
-      amount: Amounts.toString(req.amount),
-      auditors: [],
-      exchanges: [
-        { master_pub: exchangeDetails.masterPublicKey, url: exchange.baseUrl },
-      ],
-      extra: {},
-      fulfillment_url: "",
-      locations: [],
-      max_fee: Amounts.toString(req.amount),
-      merchant: {},
-      merchant_pub: pub,
-      order_id: "none",
-      pay_deadline: `/Date(${stampSecNow + 30 * 5})/`,
-      wire_transfer_deadline: `/Date(${stampSecNow + 60 * 5})/`,
-      merchant_base_url: "taler://return-to-account",
-      products: [],
-      refund_deadline: `/Date(${stampSecNow + 60 * 5})/`,
-      timestamp: `/Date(${stampSecNow})/`,
-      wire_method: wireType,
-    };
-
-    const contractTermsHash = await this.cryptoApi.hashString(
-      canonicalJson(contractTerms),
-    );
-
-    const payCoinInfo = await this.cryptoApi.signDeposit(
-      contractTerms,
-      cds,
-      Amounts.parseOrThrow(contractTerms.amount),
-    );
-
-    Wallet.enableTracing && console.log("pci", payCoinInfo);
-
-    const coins = payCoinInfo.sigs.map(s => ({ coinPaySig: s }));
-
-    const coinsReturnRecord: CoinsReturnRecord = {
-      coins,
-      contractTerms,
-      contractTermsHash,
-      exchange: exchange.baseUrl,
-      merchantPriv: priv,
-      wire: req.senderWire,
-    };
-
-    await runWithWriteTransaction(
-      this.db,
-      [Stores.coinsReturns, Stores.coins],
-      async tx => {
-        await tx.put(Stores.coinsReturns, coinsReturnRecord);
-        for (let c of payCoinInfo.updatedCoins) {
-          await tx.put(Stores.coins, c);
-        }
-      },
-    );
-    this.badge.showNotification();
-    this.notifier.notify();
-
-    this.depositReturnedCoins(coinsReturnRecord);
-  }
-
-  async depositReturnedCoins(
-    coinsReturnRecord: CoinsReturnRecord,
-  ): Promise<void> {
-    for (const c of coinsReturnRecord.coins) {
-      if (c.depositedSig) {
-        continue;
-      }
-      const req = {
-        H_wire: coinsReturnRecord.contractTerms.H_wire,
-        coin_pub: c.coinPaySig.coin_pub,
-        coin_sig: c.coinPaySig.coin_sig,
-        contribution: c.coinPaySig.contribution,
-        denom_pub: c.coinPaySig.denom_pub,
-        h_contract_terms: coinsReturnRecord.contractTermsHash,
-        merchant_pub: coinsReturnRecord.contractTerms.merchant_pub,
-        pay_deadline: coinsReturnRecord.contractTerms.pay_deadline,
-        refund_deadline: coinsReturnRecord.contractTerms.refund_deadline,
-        timestamp: coinsReturnRecord.contractTerms.timestamp,
-        ub_sig: c.coinPaySig.ub_sig,
-        wire: coinsReturnRecord.wire,
-        wire_transfer_deadline: coinsReturnRecord.contractTerms.pay_deadline,
-      };
-      Wallet.enableTracing && console.log("req", req);
-      const reqUrl = new URI("deposit").absoluteTo(coinsReturnRecord.exchange);
-      const resp = await this.http.postJson(reqUrl.href(), req);
-      if (resp.status !== 200) {
-        console.error("deposit failed due to status code", resp);
-        continue;
-      }
-      const respJson = resp.responseJson;
-      if (respJson.status !== "DEPOSIT_OK") {
-        console.error("deposit failed", resp);
-        continue;
-      }
-
-      if (!respJson.sig) {
-        console.error("invalid 'sig' field", resp);
-        continue;
-      }
-
-      // FIXME: verify signature
-
-      // For every successful deposit, we replace the old record with an 
updated one
-      const currentCrr = await oneShotGet(
-        this.db,
-        Stores.coinsReturns,
-        coinsReturnRecord.contractTermsHash,
-      );
-      if (!currentCrr) {
-        console.error("database inconsistent");
-        continue;
-      }
-      for (const nc of currentCrr.coins) {
-        if (nc.coinPaySig.coin_pub === c.coinPaySig.coin_pub) {
-          nc.depositedSig = respJson.sig;
-        }
-      }
-      await oneShotPut(this.db, Stores.coinsReturns, currentCrr);
-      this.notifier.notify();
-    }
-  }
-
-  private async acceptRefundResponse(
-    refundResponse: MerchantRefundResponse,
-  ): Promise<string> {
-    const refundPermissions = refundResponse.refund_permissions;
-
-    if (!refundPermissions.length) {
-      console.warn("got empty refund list");
-      throw Error("empty refund");
-    }
-
-    /**
-     * Add refund to purchase if not already added.
-     */
-    function f(t: PurchaseRecord | undefined): PurchaseRecord | undefined {
-      if (!t) {
-        console.error("purchase not found, not adding refunds");
-        return;
-      }
-
-      t.timestamp_refund = getTimestampNow();
-
-      for (const perm of refundPermissions) {
-        if (
-          !t.refundsPending[perm.merchant_sig] &&
-          !t.refundsDone[perm.merchant_sig]
-        ) {
-          t.refundsPending[perm.merchant_sig] = perm;
-        }
-      }
-      return t;
-    }
-
-    const hc = refundResponse.h_contract_terms;
-
-    // Add the refund permissions to the purchase within a DB transaction
-    await oneShotMutate(this.db, Stores.purchases, hc, f);
-    this.notifier.notify();
-
-    await this.submitRefunds(hc);
-
-    return hc;
+    return returnCoins(this.ws, req);
   }
 
   /**
@@ -3707,112 +562,7 @@ export class Wallet {
    * that was involved in the refund.
    */
   async applyRefund(talerRefundUri: string): Promise<string> {
-    const parseResult = parseRefundUri(talerRefundUri);
-
-    if (!parseResult) {
-      throw Error("invalid refund URI");
-    }
-
-    const refundUrl = parseResult.refundUrl;
-
-    Wallet.enableTracing && console.log("processing refund");
-    let resp;
-    try {
-      resp = await this.http.get(refundUrl);
-    } catch (e) {
-      console.error("error downloading refund permission", e);
-      throw e;
-    }
-
-    const refundResponse = MerchantRefundResponse.checked(resp.responseJson);
-    return this.acceptRefundResponse(refundResponse);
-  }
-
-  private async submitRefunds(contractTermsHash: string): Promise<void> {
-    const purchase = await oneShotGet(
-      this.db,
-      Stores.purchases,
-      contractTermsHash,
-    );
-    if (!purchase) {
-      console.error(
-        "not submitting refunds, contract terms not found:",
-        contractTermsHash,
-      );
-      return;
-    }
-    const pendingKeys = Object.keys(purchase.refundsPending);
-    if (pendingKeys.length === 0) {
-      return;
-    }
-    for (const pk of pendingKeys) {
-      const perm = purchase.refundsPending[pk];
-      const req: RefundRequest = {
-        coin_pub: perm.coin_pub,
-        h_contract_terms: purchase.contractTermsHash,
-        merchant_pub: purchase.contractTerms.merchant_pub,
-        merchant_sig: perm.merchant_sig,
-        refund_amount: perm.refund_amount,
-        refund_fee: perm.refund_fee,
-        rtransaction_id: perm.rtransaction_id,
-      };
-      console.log("sending refund permission", perm);
-      // FIXME: not correct once we support multiple exchanges per payment
-      const exchangeUrl = purchase.payReq.coins[0].exchange_url;
-      const reqUrl = new URI("refund").absoluteTo(exchangeUrl);
-      const resp = await this.http.postJson(reqUrl.href(), req);
-      if (resp.status !== 200) {
-        console.error("refund failed", resp);
-        continue;
-      }
-
-      // Transactionally mark successful refunds as done
-      const transformPurchase = (
-        t: PurchaseRecord | undefined,
-      ): PurchaseRecord | undefined => {
-        if (!t) {
-          console.warn("purchase not found, not updating refund");
-          return;
-        }
-        if (t.refundsPending[pk]) {
-          t.refundsDone[pk] = t.refundsPending[pk];
-          delete t.refundsPending[pk];
-        }
-        return t;
-      };
-      const transformCoin = (
-        c: CoinRecord | undefined,
-      ): CoinRecord | undefined => {
-        if (!c) {
-          console.warn("coin not found, can't apply refund");
-          return;
-        }
-        const refundAmount = Amounts.parseOrThrow(perm.refund_amount);
-        const refundFee = Amounts.parseOrThrow(perm.refund_fee);
-        c.status = CoinStatus.Dirty;
-        c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
-        c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
-
-        return c;
-      };
-
-      await runWithWriteTransaction(
-        this.db,
-        [Stores.purchases, Stores.coins],
-        async tx => {
-          await tx.mutate(
-            Stores.purchases,
-            contractTermsHash,
-            transformPurchase,
-          );
-          await tx.mutate(Stores.coins, perm.coin_pub, transformCoin);
-        },
-      );
-      this.refresh(perm.coin_pub);
-    }
-
-    this.badge.showNotification();
-    this.notifier.notify();
+    return applyRefund(this.ws, talerRefundUri);
   }
 
   async getPurchase(
@@ -3824,277 +574,19 @@ export class Wallet {
   async getFullRefundFees(
     refundPermissions: MerchantRefundPermission[],
   ): Promise<AmountJson> {
-    if (refundPermissions.length === 0) {
-      throw Error("no refunds given");
-    }
-    const coin0 = await oneShotGet(
-      this.db,
-      Stores.coins,
-      refundPermissions[0].coin_pub,
-    );
-    if (!coin0) {
-      throw Error("coin not found");
-    }
-    let feeAcc = Amounts.getZero(
-      Amounts.parseOrThrow(refundPermissions[0].refund_amount).currency,
-    );
-
-    const denoms = await oneShotIterIndex(
-      this.db,
-      Stores.denominations.exchangeBaseUrlIndex,
-      coin0.exchangeBaseUrl,
-    ).toArray();
-
-    for (const rp of refundPermissions) {
-      const coin = await oneShotGet(this.db, Stores.coins, rp.coin_pub);
-      if (!coin) {
-        throw Error("coin not found");
-      }
-      const denom = await oneShotGet(this.db, Stores.denominations, [
-        coin0.exchangeBaseUrl,
-        coin.denomPub,
-      ]);
-      if (!denom) {
-        throw Error(`denom not found (${coin.denomPub})`);
-      }
-      // FIXME:  this assumes that the refund already happened.
-      // When it hasn't, the refresh cost is inaccurate.  To fix this,
-      // we need introduce a flag to tell if a coin was refunded or
-      // refreshed normally (and what about incremental refunds?)
-      const refundAmount = Amounts.parseOrThrow(rp.refund_amount);
-      const refundFee = Amounts.parseOrThrow(rp.refund_fee);
-      const refreshCost = getTotalRefreshCost(
-        denoms,
-        denom,
-        Amounts.sub(refundAmount, refundFee).amount,
-      );
-      feeAcc = Amounts.add(feeAcc, refreshCost, refundFee).amount;
-    }
-    return feeAcc;
+    return getFullRefundFees(this.ws, refundPermissions);
   }
 
   async acceptTip(talerTipUri: string): Promise<void> {
-    const { tipId, merchantOrigin } = await this.getTipStatus(talerTipUri);
-    let tipRecord = await oneShotGet(this.db, Stores.tips, [
-      tipId,
-      merchantOrigin,
-    ]);
-    if (!tipRecord) {
-      throw Error("tip not in database");
-    }
-
-    tipRecord.accepted = true;
-    await oneShotPut(this.db, Stores.tips, tipRecord);
-
-    if (tipRecord.pickedUp) {
-      console.log("tip already picked up");
-      return;
-    }
-    await this.updateExchangeFromUrl(tipRecord.exchangeUrl);
-    const denomsForWithdraw = await this.getVerifiedWithdrawDenomList(
-      tipRecord.exchangeUrl,
-      tipRecord.amount,
-    );
-
-    if (!tipRecord.planchets) {
-      const planchets = await Promise.all(
-        denomsForWithdraw.map(d => this.cryptoApi.createTipPlanchet(d)),
-      );
-      const coinPubs: string[] = planchets.map(x => x.coinPub);
-
-      await oneShotMutate(this.db, Stores.tips, [tipId, merchantOrigin], r => {
-        if (!r.planchets) {
-          r.planchets = planchets;
-          r.coinPubs = coinPubs;
-        }
-        return r;
-      });
-
-      this.notifier.notify();
-    }
-
-    tipRecord = await oneShotGet(this.db, Stores.tips, [tipId, 
merchantOrigin]);
-    if (!tipRecord) {
-      throw Error("tip not in database");
-    }
-
-    if (!tipRecord.planchets) {
-      throw Error("invariant violated");
-    }
-
-    console.log("got planchets for tip!");
-
-    // Planchets in the form that the merchant expects
-    const planchetsDetail: TipPlanchetDetail[] = tipRecord.planchets.map(p => 
({
-      coin_ev: p.coinEv,
-      denom_pub_hash: p.denomPubHash,
-    }));
-
-    let merchantResp;
-
-    try {
-      const req = { planchets: planchetsDetail, tip_id: tipId };
-      merchantResp = await this.http.postJson(tipRecord.pickupUrl, req);
-      console.log("got merchant resp:", merchantResp);
-    } catch (e) {
-      console.log("tipping failed", e);
-      throw e;
-    }
-
-    const response = TipResponse.checked(merchantResp.responseJson);
-
-    if (response.reserve_sigs.length !== tipRecord.planchets.length) {
-      throw Error("number of tip responses does not match requested 
planchets");
-    }
-
-    for (let i = 0; i < tipRecord.planchets.length; i++) {
-      const tipPlanchet = tipRecord.planchets[i];
-      const planchet: PlanchetRecord = {
-        blindingKey: tipPlanchet.blindingKey,
-        coinEv: tipPlanchet.coinEv,
-        coinPriv: tipPlanchet.coinPriv,
-        coinPub: tipPlanchet.coinPub,
-        coinValue: tipPlanchet.coinValue,
-        denomPub: tipPlanchet.denomPub,
-        denomPubHash: tipPlanchet.denomPubHash,
-        exchangeBaseUrl: tipRecord.exchangeUrl,
-        isFromTip: true,
-        reservePub: response.reserve_pub,
-        withdrawSig: response.reserve_sigs[i].reserve_sig,
-        coinIndex: -1,
-        withdrawSessionId: "",
-      };
-      await oneShotPut(this.db, Stores.planchets, planchet);
-      await this.processPlanchet(planchet.coinPub);
-    }
-
-    tipRecord.pickedUp = true;
-
-    await oneShotPut(this.db, Stores.tips, tipRecord);
-
-    this.notifier.notify();
-    this.badge.showNotification();
-    return;
+    return acceptTip(this.ws, talerTipUri);
   }
 
   async getTipStatus(talerTipUri: string): Promise<TipStatus> {
-    const res = parseTipUri(talerTipUri);
-    if (!res) {
-      throw Error("invalid taler://tip URI");
-    }
-
-    const tipStatusUrl = new URI(res.tipPickupUrl).href();
-    console.log("checking tip status from", tipStatusUrl);
-    const merchantResp = await this.http.get(tipStatusUrl);
-    console.log("resp:", merchantResp.responseJson);
-    const tipPickupStatus = TipPickupGetResponse.checked(
-      merchantResp.responseJson,
-    );
-
-    console.log("status", tipPickupStatus);
-
-    let amount = Amounts.parseOrThrow(tipPickupStatus.amount);
-
-    let tipRecord = await oneShotGet(this.db, Stores.tips, [
-      res.tipId,
-      res.merchantOrigin,
-    ]);
-
-    if (!tipRecord) {
-      const withdrawDetails = await this.getWithdrawDetailsForAmount(
-        tipPickupStatus.exchange_url,
-        amount,
-      );
-
-      tipRecord = {
-        accepted: false,
-        amount,
-        coinPubs: [],
-        deadline: getTalerStampSec(tipPickupStatus.stamp_expire)!,
-        exchangeUrl: tipPickupStatus.exchange_url,
-        merchantDomain: res.merchantOrigin,
-        nextUrl: undefined,
-        pickedUp: false,
-        planchets: undefined,
-        response: undefined,
-        timestamp: getTimestampNow(),
-        tipId: res.tipId,
-        pickupUrl: res.tipPickupUrl,
-        totalFees: Amounts.add(
-          withdrawDetails.overhead,
-          withdrawDetails.withdrawFee,
-        ).amount,
-      };
-      await oneShotPut(this.db, Stores.tips, tipRecord);
-    }
-
-    const tipStatus: TipStatus = {
-      accepted: !!tipRecord && tipRecord.accepted,
-      amount: Amounts.parseOrThrow(tipPickupStatus.amount),
-      amountLeft: Amounts.parseOrThrow(tipPickupStatus.amount_left),
-      exchangeUrl: tipPickupStatus.exchange_url,
-      nextUrl: tipPickupStatus.extra.next_url,
-      merchantOrigin: res.merchantOrigin,
-      tipId: res.tipId,
-      expirationTimestamp: getTalerStampSec(tipPickupStatus.stamp_expire)!,
-      timestamp: getTalerStampSec(tipPickupStatus.stamp_created)!,
-      totalFees: tipRecord.totalFees,
-    };
-
-    return tipStatus;
+    return getTipStatus(this.ws, talerTipUri);
   }
 
   async abortFailedPayment(contractTermsHash: string): Promise<void> {
-    const purchase = await oneShotGet(
-      this.db,
-      Stores.purchases,
-      contractTermsHash,
-    );
-    if (!purchase) {
-      throw Error("Purchase not found, unable to abort with refund");
-    }
-    if (purchase.finished) {
-      throw Error("Purchase already finished, not aborting");
-    }
-    if (purchase.abortDone) {
-      console.warn("abort requested on already aborted purchase");
-      return;
-    }
-
-    purchase.abortRequested = true;
-
-    // From now on, we can't retry payment anymore,
-    // so mark this in the DB in case the /pay abort
-    // does not complete on the first try.
-    await oneShotPut(this.db, Stores.purchases, purchase);
-
-    let resp;
-
-    const abortReq = { ...purchase.payReq, mode: "abort-refund" };
-
-    const payUrl = new URI("pay")
-      .absoluteTo(purchase.contractTerms.merchant_base_url)
-      .href();
-
-    try {
-      resp = await this.http.postJson(payUrl, abortReq);
-    } catch (e) {
-      // Gives the user the option to retry / abort and refresh
-      console.log("aborting payment failed", e);
-      throw e;
-    }
-
-    const refundResponse = MerchantRefundResponse.checked(resp.responseJson);
-    await this.acceptRefundResponse(refundResponse);
-
-    await runWithWriteTransaction(this.db, [Stores.purchases], async tx => {
-      const p = await tx.get(Stores.purchases, purchase.contractTermsHash);
-      if (!p) {
-        return;
-      }
-      p.abortDone = true;
-      await tx.put(Stores.purchases, p);
-    });
+    return abortFailedPayment(this.ws, contractTermsHash);
   }
 
   public async handleNotifyReserve() {
@@ -4102,7 +594,7 @@ export class Wallet {
     for (const r of reserves) {
       if (r.reserveStatus === ReserveRecordStatus.WAIT_CONFIRM_BANK) {
         try {
-          this.processReserveBankStatus(r.reservePub);
+          this.processReserve(r.reservePub);
         } catch (e) {
           console.error(e);
         }
@@ -4128,49 +620,14 @@ export class Wallet {
   async getWithdrawalInfo(
     talerWithdrawUri: string,
   ): Promise<DownloadedWithdrawInfo> {
-    const uriResult = parseWithdrawUri(talerWithdrawUri);
-    if (!uriResult) {
-      throw Error("can't parse URL");
-    }
-    const resp = await this.http.get(uriResult.statusUrl);
-    console.log("resp:", resp.responseJson);
-    const status = WithdrawOperationStatusResponse.checked(resp.responseJson);
-    return {
-      amount: Amounts.parseOrThrow(status.amount),
-      confirmTransferUrl: status.confirm_transfer_url,
-      extractedStatusUrl: uriResult.statusUrl,
-      selectionDone: status.selection_done,
-      senderWire: status.sender_wire,
-      suggestedExchange: status.suggested_exchange,
-      transferDone: status.transfer_done,
-      wireTypes: status.wire_types,
-    };
+    return getWithdrawalInfo(this.ws, talerWithdrawUri);
   }
 
   async acceptWithdrawal(
     talerWithdrawUri: string,
     selectedExchange: string,
   ): Promise<AcceptWithdrawalResponse> {
-    const withdrawInfo = await this.getWithdrawalInfo(talerWithdrawUri);
-    const exchangeWire = await this.getExchangePaytoUri(
-      selectedExchange,
-      withdrawInfo.wireTypes,
-    );
-    const reserve = await this.createReserve({
-      amount: withdrawInfo.amount,
-      bankWithdrawStatusUrl: withdrawInfo.extractedStatusUrl,
-      exchange: selectedExchange,
-      senderWire: withdrawInfo.senderWire,
-      exchangeWire: exchangeWire,
-    });
-    // We do this here, as the reserve should be registered before we return,
-    // so that we can redirect the user to the bank's status page.
-    await this.processReserveBankStatus(reserve.reservePub);
-    console.log("acceptWithdrawal: returning");
-    return {
-      reservePub: reserve.reservePub,
-      confirmTransferUrl: withdrawInfo.confirmTransferUrl,
-    };
+    return acceptWithdrawal(this.ws, talerWithdrawUri, selectedExchange);
   }
 
   async getPurchaseDetails(hc: string): Promise<PurchaseDetails> {
diff --git a/src/walletTypes.ts b/src/walletTypes.ts
index 45560694..5736282e 100644
--- a/src/walletTypes.ts
+++ b/src/walletTypes.ts
@@ -25,16 +25,17 @@
 /**
  * Imports.
  */
-import { Checkable } from "./checkable";
-import * as LibtoolVersion from "./libtoolVersion";
+import { Checkable } from "./util/checkable";
+import * as LibtoolVersion from "./util/libtoolVersion";
 
-import { AmountJson } from "./amounts";
+import { AmountJson } from "./util/amounts";
 
 import {
   CoinRecord,
   DenominationRecord,
   ExchangeRecord,
   ExchangeWireInfo,
+  WithdrawalSource,
 } from "./dbTypes";
 import { CoinPaySig, ContractTerms, PayReq } from "./talerTypes";
 
@@ -413,6 +414,7 @@ export interface TipStatus {
   nextUrl: string;
   exchangeUrl: string;
   tipId: string;
+  merchantTipId: string;
   merchantOrigin: string;
   expirationTimestamp: number;
   timestamp: number;
@@ -523,7 +525,7 @@ export interface WalletDiagnostics {
 
 export interface PendingWithdrawOperation {
   type: "withdraw";
-  reservePub: string;
+  source: WithdrawalSource,
   withdrawSessionId: string;
   numCoinsWithdrawn: number;
   numCoinsTotal: number;
@@ -576,13 +578,6 @@ export interface PendingRefreshOperation {
   refreshOutputSize: number;
 }
 
-export interface PendingPlanchetOperation {
-  type: "planchet";
-  coinPub: string;
-  reservePub: string;
-  lastError?: OperationError;
-}
-
 export interface PendingDirtyCoinOperation {
   type: "dirty-coin";
   coinPub: string;
@@ -595,14 +590,21 @@ export interface PendingProposalOperation {
   proposalId: string;
 }
 
+export interface PendingTipOperation {
+  type: "tip";
+  tipId: string;
+  merchantBaseUrl: string;
+  merchantTipId: string;
+}
+
 export type PendingOperationInfo =
   | PendingWithdrawOperation
   | PendingReserveOperation
   | PendingBugOperation
-  | PendingPlanchetOperation
   | PendingDirtyCoinOperation
   | PendingExchangeUpdateOperation
   | PendingRefreshOperation
+  | PendingTipOperation
   | PendingProposalOperation;
 
 export interface PendingOperationsResponse {
@@ -642,7 +644,6 @@ export function getTimestampNow(): Timestamp {
   };
 }
 
-
 export interface PlanchetCreationResult {
   coinPub: string;
   coinPriv: string;
@@ -652,6 +653,13 @@ export interface PlanchetCreationResult {
   blindingKey: string;
   withdrawSig: string;
   coinEv: string;
-  exchangeBaseUrl: string;
   coinValue: AmountJson;
+}
+
+export interface PlanchetCreationRequest {
+  value: AmountJson;
+  feeWithdraw: AmountJson;
+  denomPub: string;
+  reservePub: string;
+  reservePriv: string;
 }
\ No newline at end of file
diff --git a/src/webex/messages.ts b/src/webex/messages.ts
index e321e5ac..cf409b44 100644
--- a/src/webex/messages.ts
+++ b/src/webex/messages.ts
@@ -21,7 +21,7 @@
 // Messages are already documented in wxApi.
 /* tslint:disable:completed-docs */
 
-import { AmountJson } from "../amounts";
+import { AmountJson } from "../util/amounts";
 import * as dbTypes from "../dbTypes";
 import * as talerTypes from "../talerTypes";
 import * as walletTypes from "../walletTypes";
@@ -113,10 +113,6 @@ export interface MessageMap {
     request: { reservePub: string };
     response: dbTypes.ReserveRecord[];
   };
-  "get-planchets": {
-    request: { exchangeBaseUrl: string };
-    response: dbTypes.PlanchetRecord[];
-  };
   "get-denoms": {
     request: { exchangeBaseUrl: string };
     response: dbTypes.DenominationRecord[];
@@ -153,14 +149,6 @@ export interface MessageMap {
     request: {};
     response: void;
   };
-  "download-proposal": {
-    request: { url: string };
-    response: number;
-  };
-  "submit-pay": {
-    request: { contractTermsHash: string; sessionId: string | undefined };
-    response: walletTypes.ConfirmPayResult;
-  };
   "accept-refund": {
     request: { refundUrl: string };
     response: string;
diff --git a/src/webex/notify.ts b/src/webex/notify.ts
index 4e53c3e1..61a96cb1 100644
--- a/src/webex/notify.ts
+++ b/src/webex/notify.ts
@@ -24,8 +24,6 @@
 /**
  * Imports.
  */
-import URI = require("urijs");
-
 import wxApi = require("./wxApi");
 
 declare var cloneInto: any;
@@ -180,25 +178,19 @@ function registerHandlers() {
   });
 
   addHandler("taler-create-reserve", (msg: any) => {
-    const params = {
-      amount: JSON.stringify(msg.amount),
-      bank_url: document.location.href,
-      callback_url: new URI(msg.callback_url) 
.absoluteTo(document.location.href),
-      suggested_exchange_url: msg.suggested_exchange_url,
-      wt_types: JSON.stringify(msg.wt_types),
-    };
-    const uri = new 
URI(chrome.extension.getURL("/src/webex/pages/confirm-create-reserve.html"));
-    const redirectUrl = uri.query(params).href();
-    window.location.href = redirectUrl;
+    const uri = new 
URL(chrome.extension.getURL("/src/webex/pages/confirm-create-reserve.html"));
+    uri.searchParams.set("amount", JSON.stringify(msg.amount));
+    uri.searchParams.set("bank_url", document.location.href);
+    uri.searchParams.set("callback_url", new URL(msg.callback_url, 
document.location.href).href);
+    uri.searchParams.set("suggested_exchange_url", msg.suggested_exchange_url);
+    uri.searchParams.set("wt_types", JSON.stringify(msg.wt_types));
+    window.location.href = uri.href;
   });
 
   addHandler("taler-add-auditor", (msg: any) => {
-    const params = {
-      req: JSON.stringify(msg),
-    };
-    const uri = new 
URI(chrome.extension.getURL("/src/webex/pages/add-auditor.html"));
-    const redirectUrl = uri.query(params).href();
-    window.location.href = redirectUrl;
+    const uri = new 
URL(chrome.extension.getURL("/src/webex/pages/add-auditor.html"));
+    uri.searchParams.set("req", JSON.stringify(msg))
+    window.location.href = uri.href;
   });
 
   addHandler("taler-confirm-reserve", async (msg: any, sendResponse: any) => {
diff --git a/src/webex/pages/add-auditor.tsx b/src/webex/pages/add-auditor.tsx
index 7e3e0632..766db9c5 100644
--- a/src/webex/pages/add-auditor.tsx
+++ b/src/webex/pages/add-auditor.tsx
@@ -23,7 +23,6 @@
 import { CurrencyRecord } from "../../dbTypes";
 import { getCurrencies, updateCurrency } from "../wxApi";
 import React, { useState } from "react";
-import URI = require("urijs");
 import { registerMountPage } from "../renderHtml";
 
 interface ConfirmAuditorProps {
@@ -118,14 +117,24 @@ function ConfirmAuditor(props: ConfirmAuditorProps) {
 
 
 registerMountPage(() => {
-  const walletPageUrl = new URI(document.location.href);
-  const query: any = JSON.parse(
-    (URI.parseQuery(walletPageUrl.query()) as any).req,
-  );
-  const url = query.url;
-  const currency: string = query.currency;
-  const auditorPub: string = query.auditorPub;
-  const expirationStamp = Number.parseInt(query.expirationStamp);
+  const walletPageUrl = new URL(document.location.href);
+  const url = walletPageUrl.searchParams.get("url");
+  if (!url) {
+    throw Error("missign parameter (url)");
+  }
+  const currency = walletPageUrl.searchParams.get("currency");
+  if (!currency) {
+    throw Error("missing parameter (currency)");
+  }
+  const auditorPub = walletPageUrl.searchParams.get("auditorPub");
+  if (!auditorPub) {
+    throw Error("missing parameter (auditorPub)");
+  }
+  const auditorStampStr = walletPageUrl.searchParams.get("expirationStamp");
+  if (!auditorStampStr) {
+    throw Error("missing parameter (auditorStampStr)");
+  }
+  const expirationStamp = Number.parseInt(auditorStampStr);
   const args = { url, currency, auditorPub, expirationStamp };
   return <ConfirmAuditor {...args}/>;
 });
diff --git a/src/webex/pages/pay.tsx b/src/webex/pages/pay.tsx
index 7f2a174b..cff2f946 100644
--- a/src/webex/pages/pay.tsx
+++ b/src/webex/pages/pay.tsx
@@ -30,9 +30,8 @@ import { renderAmount, ProgressButton, registerMountPage } 
from "../renderHtml";
 import * as wxApi from "../wxApi";
 
 import React, { useState, useEffect } from "react";
-import URI = require("urijs");
 
-import * as Amounts from "../../amounts";
+import * as Amounts from "../../util/amounts";
 
 function TalerPayDialog({ talerPayUri }: { talerPayUri: string }) {
   const [payStatus, setPayStatus] = useState<PreparePayResult | undefined>();
@@ -164,10 +163,10 @@ function TalerPayDialog({ talerPayUri }: { talerPayUri: 
string }) {
 }
 
 registerMountPage(() => {
-  const url = new URI(document.location.href);
-  const query: any = URI.parseQuery(url.query());
-
-  let talerPayUri = query.talerPayUri;
-
+  const url = new URL(document.location.href);
+  const talerPayUri = url.searchParams.get("talerPayUri");
+  if (!talerPayUri) {
+    throw Error("invalid parameter");
+  }
   return <TalerPayDialog talerPayUri={talerPayUri} />;
 });
diff --git a/src/webex/pages/popup.tsx b/src/webex/pages/popup.tsx
index 78b7374b..27d5dddb 100644
--- a/src/webex/pages/popup.tsx
+++ b/src/webex/pages/popup.tsx
@@ -26,8 +26,8 @@
  */
 import * as i18n from "../../i18n";
 
-import { AmountJson } from "../../amounts";
-import * as Amounts from "../../amounts";
+import { AmountJson } from "../../util/amounts";
+import * as Amounts from "../../util/amounts";
 
 import {
   HistoryEvent,
@@ -44,9 +44,6 @@ import {
 import * as wxApi from "../wxApi";
 
 import * as React from "react";
-import * as ReactDOM from "react-dom";
-
-import URI = require("urijs");
 
 function onUpdateNotification(f: () => void): () => void {
   const port = chrome.runtime.connect({ name: "notifications" });
@@ -339,7 +336,7 @@ function formatHistoryItem(historyItem: HistoryEvent) {
         </i18n.Translate>
       );
     case "confirm-reserve": {
-      const exchange = new URI(d.exchangeBaseUrl).host();
+      const exchange = new URL(d.exchangeBaseUrl).host;
       const pub = abbrev(d.reservePub);
       return (
         <i18n.Translate wrap="p">
@@ -359,7 +356,7 @@ function formatHistoryItem(historyItem: HistoryEvent) {
     }
     case "depleted-reserve": {
       const exchange = d.exchangeBaseUrl
-        ? new URI(d.exchangeBaseUrl).host()
+        ? new URL(d.exchangeBaseUrl).host
         : "??";
       const amount = renderAmount(d.requestedAmount);
       const pub = abbrev(d.reservePub);
@@ -396,11 +393,10 @@ function formatHistoryItem(historyItem: HistoryEvent) {
       );
     }
     case "tip": {
-      const tipPageUrl = new URI(
-        chrome.extension.getURL("/src/webex/pages/tip.html"),
-      );
-      const params = { tip_id: d.tipId, merchant_domain: d.merchantDomain };
-      const url = tipPageUrl.query(params).href();
+      const tipPageUrl = new 
URL(chrome.extension.getURL("/src/webex/pages/tip.html"));
+      tipPageUrl.searchParams.set("tip_id", d.tipId);
+      tipPageUrl.searchParams.set("merchant_domain", d.merchantDomain);
+      const url = tipPageUrl.href;
       const tipLink = <a href={url} onClick={openTab(url)}>{i18n.str`tip`}</a>;
       // i18n: Tip
       return (
diff --git a/src/webex/pages/refund.tsx b/src/webex/pages/refund.tsx
index 79cadcdc..5196c9ea 100644
--- a/src/webex/pages/refund.tsx
+++ b/src/webex/pages/refund.tsx
@@ -22,7 +22,6 @@
 
 import React, { useEffect, useState } from "react";
 import ReactDOM from "react-dom";
-import URI = require("urijs");
 
 import * as wxApi from "../wxApi";
 import { PurchaseDetails } from "../../walletTypes";
@@ -76,8 +75,7 @@ function RefundStatusView(props: { talerRefundUri: string }) {
 }
 
 async function main() {
-  const url = new URI(document.location.href);
-  const query: any = URI.parseQuery(url.query());
+  const url = new URL(document.location.href);
 
   const container = document.getElementById("container");
   if (!container) {
@@ -85,7 +83,7 @@ async function main() {
     return;
   }
 
-  const talerRefundUri = query.talerRefundUri;
+  const talerRefundUri = url.searchParams.get("talerRefundUri");
   if (!talerRefundUri) {
     console.error("taler refund URI requred");
     return;
diff --git a/src/webex/pages/return-coins.tsx b/src/webex/pages/return-coins.tsx
index b5d53c31..be65b412 100644
--- a/src/webex/pages/return-coins.tsx
+++ b/src/webex/pages/return-coins.tsx
@@ -25,8 +25,8 @@
  * Imports.
  */
 
-import { AmountJson } from "../../amounts";
-import * as Amounts from "../../amounts";
+import { AmountJson } from "../../util/amounts";
+import * as Amounts from "../../util/amounts";
 
 import {
   SenderWireInfos,
@@ -35,7 +35,7 @@ import {
 
 import * as i18n from "../../i18n";
 
-import * as wire from "../../wire";
+import * as wire from "../../util/wire";
 
 import {
   getBalance,
diff --git a/src/webex/pages/tip.tsx b/src/webex/pages/tip.tsx
index 148b8203..ac904cf0 100644
--- a/src/webex/pages/tip.tsx
+++ b/src/webex/pages/tip.tsx
@@ -23,7 +23,6 @@
 
 import * as React from "react";
 import * as ReactDOM from "react-dom";
-import URI = require("urijs");
 
 import * as i18n from "../../i18n";
 
@@ -31,7 +30,7 @@ import { acceptTip, getReserveCreationInfo, getTipStatus } 
from "../wxApi";
 
 import { WithdrawDetailView, renderAmount, ProgressButton } from 
"../renderHtml";
 
-import * as Amounts from "../../amounts";
+import * as Amounts from "../../util/amounts";
 import { useState, useEffect } from "react";
 import { TipStatus } from "../../walletTypes";
 
@@ -68,7 +67,7 @@ function TipDisplay(props: { talerTipUri: string }) {
 
   const accept = async () => {
     setLoading(true);
-    await acceptTip(props.talerTipUri);
+    await acceptTip(tipStatus.tipId);
     setFinished(true);
   };
 
@@ -101,9 +100,8 @@ function TipDisplay(props: { talerTipUri: string }) {
 
 async function main() {
   try {
-    const url = new URI(document.location.href);
-    const query: any = URI.parseQuery(url.query());
-    const talerTipUri = query.talerTipUri;
+    const url = new URL(document.location.href);
+    const talerTipUri = url.searchParams.get("talerTipUri");
     if (typeof talerTipUri !== "string") {
       throw Error("talerTipUri must be a string");
     }
diff --git a/src/webex/pages/withdraw.tsx b/src/webex/pages/withdraw.tsx
index 39b27f2d..6b7152dc 100644
--- a/src/webex/pages/withdraw.tsx
+++ b/src/webex/pages/withdraw.tsx
@@ -32,7 +32,6 @@ import { WithdrawDetailView, renderAmount } from 
"../renderHtml";
 
 import React, { useState, useEffect } from "react";
 import * as ReactDOM from "react-dom";
-import URI = require("urijs");
 import { getWithdrawDetails, acceptWithdrawal } from "../wxApi";
 
 function NewExchangeSelection(props: { talerWithdrawUri: string }) {
@@ -199,9 +198,8 @@ function NewExchangeSelection(props: { talerWithdrawUri: 
string }) {
 
 async function main() {
   try {
-    const url = new URI(document.location.href);
-    const query: any = URI.parseQuery(url.query());
-    let talerWithdrawUri = query.talerWithdrawUri;
+    const url = new URL(document.location.href);
+    const talerWithdrawUri = url.searchParams.get("talerWithdrawUri");
     if (!talerWithdrawUri) {
       throw Error("withdraw URI required");
     }
diff --git a/src/webex/renderHtml.tsx b/src/webex/renderHtml.tsx
index 42bcdbab..945719b6 100644
--- a/src/webex/renderHtml.tsx
+++ b/src/webex/renderHtml.tsx
@@ -23,8 +23,8 @@
 /**
  * Imports.
  */
-import { AmountJson } from "../amounts";
-import * as Amounts from "../amounts";
+import { AmountJson } from "../util/amounts";
+import * as Amounts from "../util/amounts";
 import { DenominationRecord } from "../dbTypes";
 import { ReserveCreationInfo } from "../walletTypes";
 import * as moment from "moment";
diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts
index a8b35ed3..ea26cd2e 100644
--- a/src/webex/wxApi.ts
+++ b/src/webex/wxApi.ts
@@ -22,7 +22,7 @@
 /**
  * Imports.
  */
-import { AmountJson } from "../amounts";
+import { AmountJson } from "../util/amounts";
 import {
   CoinRecord,
   CurrencyRecord,
@@ -173,14 +173,6 @@ export function getCoins(exchangeBaseUrl: string): 
Promise<CoinRecord[]> {
 }
 
 
-/**
- * Get all planchets withdrawn from the given exchange.
- */
-export function getPlanchets(exchangeBaseUrl: string): 
Promise<PlanchetRecord[]> {
-  return callBackend("get-planchets", { exchangeBaseUrl });
-}
-
-
 /**
  * Get all denoms offered by the given exchange.
  */
@@ -211,13 +203,6 @@ export function confirmPay(proposalId: string, sessionId: 
string | undefined): P
   return callBackend("confirm-pay", { proposalId, sessionId });
 }
 
-/**
- * Replay paying for a purchase.
- */
-export function submitPay(contractTermsHash: string, sessionId: string | 
undefined): Promise<ConfirmPayResult> {
-  return callBackend("submit-pay", { contractTermsHash, sessionId });
-}
-
 
 /**
  * Mark a reserve as confirmed.
@@ -302,14 +287,6 @@ export function clearNotification(): Promise<void> {
   return callBackend("clear-notification", { });
 }
 
-
-/**
- * Download a contract.
- */
-export function downloadProposal(url: string): Promise<number> {
-  return callBackend("download-proposal", { url });
-}
-
 /**
  * Download a refund and accept it.
  */
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index 78c86a97..2d7f963e 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -23,8 +23,8 @@
 /**
  * Imports.
  */
-import { BrowserHttpLib } from "../http";
-import { AmountJson } from "../amounts";
+import { BrowserHttpLib } from "../util/http";
+import { AmountJson } from "../util/amounts";
 import {
   ConfirmReserveRequest,
   CreateReserveRequest,
@@ -39,11 +39,10 @@ import { openTalerDb, exportDb, importDb, deleteDb } from 
"../db";
 import { ChromeBadge } from "./chromeBadge";
 import { MessageType } from "./messages";
 import * as wxApi from "./wxApi";
-import URI = require("urijs");
 import Port = chrome.runtime.Port;
 import MessageSender = chrome.runtime.MessageSender;
 import { BrowserCryptoWorkerFactory } from "../crypto/cryptoApi";
-import { OpenedPromise, openPromise } from "../promiseUtils";
+import { OpenedPromise, openPromise } from "../util/promiseUtils";
 
 const NeedsWallet = Symbol("NeedsWallet");
 
@@ -122,15 +121,6 @@ async function handleMessage(
       }
       return needsWallet().confirmPay(detail.proposalId, detail.sessionId);
     }
-    case "submit-pay": {
-      if (typeof detail.contractTermsHash !== "string") {
-        throw Error("contractTermsHash must be a string");
-      }
-      return needsWallet().submitPay(
-        detail.contractTermsHash,
-        detail.sessionId,
-      );
-    }
     case "exchange-info": {
       if (!detail.baseUrl) {
         return Promise.resolve({ error: "bad url" });
@@ -170,7 +160,7 @@ async function handleMessage(
       if (typeof detail.reservePub !== "string") {
         return Promise.reject(Error("reservePub missing"));
       }
-      return needsWallet().withdrawPaybackReserve(detail.reservePub);
+      throw Error("not implemented");
     }
     case "get-coins": {
       if (typeof detail.exchangeBaseUrl !== "string") {
@@ -178,12 +168,6 @@ async function handleMessage(
       }
       return needsWallet().getCoinsForExchange(detail.exchangeBaseUrl);
     }
-    case "get-planchets": {
-      if (typeof detail.exchangeBaseUrl !== "string") {
-        return Promise.reject(Error("exchangBaseUrl missing"));
-      }
-      return needsWallet().getPlanchets(detail.exchangeBaseUrl);
-    }
     case "get-denoms": {
       if (typeof detail.exchangeBaseUrl !== "string") {
         return Promise.reject(Error("exchangBaseUrl missing"));
@@ -244,9 +228,6 @@ async function handleMessage(
     case "clear-notification": {
       return needsWallet().clearNotification();
     }
-    case "download-proposal": {
-      return needsWallet().downloadProposal(detail.url);
-    }
     case "abort-failed-payment": {
       if (!detail.contractTermsHash) {
         throw Error("contracTermsHash not given");
@@ -404,18 +385,19 @@ function makeSyncWalletRedirect(
   oldUrl: string,
   params?: { [name: string]: string | undefined },
 ): object {
-  const innerUrl = new URI(chrome.extension.getURL("/src/webex/pages/" + url));
+  const innerUrl = new URL(chrome.extension.getURL("/src/webex/pages/" + url));
   if (params) {
     for (const key in params) {
-      if (params[key]) {
-        innerUrl.addSearch(key, params[key]);
+      const p = params[key];
+      if (p) {
+        innerUrl.searchParams.set(key, p);
       }
     }
   }
-  const outerUrl = new URI(
+  const outerUrl = new URL(
     chrome.extension.getURL("/src/webex/pages/redirect.html"),
   );
-  outerUrl.addSearch("url", innerUrl);
+  outerUrl.searchParams.set("url", innerUrl.href);
   if (isFirefox()) {
     // Some platforms don't support the sync redirect (yet), so fall back to
     // async redirect after a timeout.
@@ -423,12 +405,12 @@ function makeSyncWalletRedirect(
       await waitMs(150);
       const tab = await getTab(tabId);
       if (tab.url === oldUrl) {
-        chrome.tabs.update(tabId, { url: outerUrl.href() });
+        chrome.tabs.update(tabId, { url: outerUrl.href });
       }
     };
     doit();
   }
-  return { redirectUrl: outerUrl.href() };
+  return { redirectUrl: outerUrl.href };
 }
 
 /**
@@ -549,29 +531,29 @@ export async function wxMain() {
       if (!tab.url || !tab.id) {
         continue;
       }
-      const uri = new URI(tab.url);
-      if (uri.protocol() !== "http" && uri.protocol() !== "https") {
+      const uri = new URL(tab.url);
+      if (uri.protocol !== "http:" && uri.protocol !== "https:") {
         continue;
       }
       console.log(
         "injecting into existing tab",
         tab.id,
         "with url",
-        uri.href(),
+        uri.href,
         "protocol",
-        uri.protocol(),
+        uri.protocol,
       );
       injectScript(
         tab.id,
         { file: "/dist/contentScript-bundle.js", runAt: "document_start" },
-        uri.href(),
+        uri.href,
       );
       const code = `
         if (("taler" in window) || 
document.documentElement.getAttribute("data-taler-nojs")) {
           document.dispatchEvent(new Event("taler-probe-result"));
         }
       `;
-      injectScript(tab.id, { code, runAt: "document_start" }, uri.href());
+      injectScript(tab.id, { code, runAt: "document_start" }, uri.href);
     }
   });
 
@@ -603,8 +585,8 @@ export async function wxMain() {
         if (!tab.url || !tab.id) {
           return;
         }
-        const uri = new URI(tab.url);
-        if (!(uri.protocol() === "http" || uri.protocol() === "https")) {
+        const uri = new URL(tab.url);
+        if (!(uri.protocol === "http:" || uri.protocol === "https:")) {
           return;
         }
         const code = `
@@ -612,7 +594,7 @@ export async function wxMain() {
             document.dispatchEvent(new Event("taler-probe-result"));
           }
         `;
-        injectScript(tab.id!, { code, runAt: "document_start" }, uri.href());
+        injectScript(tab.id!, { code, runAt: "document_start" }, uri.href);
       });
     };
 
diff --git a/tsconfig.json b/tsconfig.json
index 8df94764..75214637 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -23,9 +23,7 @@
     "esModuleInterop": true
   },
   "files": [
-    "src/amounts.ts",
     "src/android/index.ts",
-    "src/checkable.ts",
     "src/crypto/browserWorkerEntry.ts",
     "src/crypto/cryptoApi.ts",
     "src/crypto/cryptoImplementation.ts",
@@ -46,22 +44,42 @@
     "src/headless/integrationtest.ts",
     "src/headless/merchant.ts",
     "src/headless/taler-wallet-cli.ts",
-    "src/helpers-test.ts",
-    "src/helpers.ts",
-    "src/http.ts",
     "src/i18n.tsx",
     "src/i18n/strings.ts",
     "src/index.ts",
-    "src/libtoolVersion-test.ts",
-    "src/libtoolVersion.ts",
-    "src/logging.ts",
-    "src/promiseUtils.ts",
-    "src/query.ts",
     "src/talerTypes.ts",
-    "src/taleruri-test.ts",
-    "src/taleruri.ts",
-    "src/timer.ts",
     "src/types-test.ts",
+    "src/util/amounts.ts",
+    "src/util/assertUnreachable.ts",
+    "src/util/asyncMemo.ts",
+    "src/util/checkable.ts",
+    "src/util/helpers-test.ts",
+    "src/util/helpers.ts",
+    "src/util/http.ts",
+    "src/util/libtoolVersion-test.ts",
+    "src/util/libtoolVersion.ts",
+    "src/util/logging.ts",
+    "src/util/payto-test.ts",
+    "src/util/payto.ts",
+    "src/util/promiseUtils.ts",
+    "src/util/query.ts",
+    "src/util/taleruri-test.ts",
+    "src/util/taleruri.ts",
+    "src/util/timer.ts",
+    "src/util/wire.ts",
+    "src/wallet-impl/balance.ts",
+    "src/wallet-impl/exchanges.ts",
+    "src/wallet-impl/history.ts",
+    "src/wallet-impl/pay.ts",
+    "src/wallet-impl/payback.ts",
+    "src/wallet-impl/pending.ts",
+    "src/wallet-impl/refresh.ts",
+    "src/wallet-impl/refund.ts",
+    "src/wallet-impl/reserves.ts",
+    "src/wallet-impl/return.ts",
+    "src/wallet-impl/state.ts",
+    "src/wallet-impl/tip.ts",
+    "src/wallet-impl/withdraw.ts",
     "src/wallet-test.ts",
     "src/wallet.ts",
     "src/walletTypes.ts",
@@ -86,7 +104,6 @@
     "src/webex/pages/withdraw.tsx",
     "src/webex/renderHtml.tsx",
     "src/webex/wxApi.ts",
-    "src/webex/wxBackend.ts",
-    "src/wire.ts"
+    "src/webex/wxBackend.ts"
   ]
 }
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 22bcd5fd..a28bb07c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -394,11 +394,6 @@
     "@types/prop-types" "*"
     csstype "^2.2.0"
 
-"@types/urijs@^1.19.3":
-  version "1.19.4"
-  resolved 
"https://registry.yarnpkg.com/@types/urijs/-/urijs-1.19.4.tgz#29c4a694d4842d7f95e359a26223fc1865f1ab13";
-  integrity 
sha512-uHUvuLfy4YkRHL4UH8J8oRsINhdEHd9ymag7KJZVT94CjAmY1njoUzhazJsZjwfy+IpWKQKGVyXCwzhZvg73Fg==
-
 "@webassemblyjs/ast@1.8.5":
   version "1.8.5"
   resolved 
"https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359";
@@ -7084,11 +7079,6 @@ uri-js@^4.2.2:
   dependencies:
     punycode "^2.1.0"
 
-urijs@^1.18.10:
-  version "1.19.2"
-  resolved 
"https://registry.yarnpkg.com/urijs/-/urijs-1.19.2.tgz#f9be09f00c4c5134b7cb3cf475c1dd394526265a";
-  integrity 
sha512-s/UIq9ap4JPZ7H1EB5ULo/aOUbWqfDi7FKzMC2Nz+0Si8GiT1rIEaprt8hy3Vy2Ex2aJPpOQv4P4DuOZ+K1c6w==
-
 urix@^0.1.0:
   version "0.1.0"
   resolved 
"https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72";

-- 
To stop receiving notification emails like this one, please contact
address@hidden.



reply via email to

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