gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 02/02: wallet robustness WIP


From: gnunet
Subject: [taler-wallet-core] 02/02: wallet robustness WIP
Date: Sat, 30 Nov 2019 00:36:31 +0100

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

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

commit aaf7e1338d6cdb1b4e01ad318938b3eaea2f922b
Author: Florian Dold <address@hidden>
AuthorDate: Sat Nov 30 00:36:20 2019 +0100

    wallet robustness WIP
---
 gulpfile.js                        |   4 +-
 package.json                       |   2 +-
 src/crypto/cryptoApi.ts            |  16 +-
 src/crypto/cryptoImplementation.ts |  25 +-
 src/crypto/primitives/kdf.ts       |   2 +-
 src/crypto/talerCrypto.ts          |   3 +
 src/dbTypes.ts                     | 164 +++++--
 src/headless/bank.ts               |  31 ++
 src/headless/clk.ts                |  15 +-
 src/headless/helpers.ts            |  18 +-
 src/headless/merchant.ts           |  64 ++-
 src/headless/taler-wallet-cli.ts   | 183 ++++++--
 src/wallet-test.ts                 |   2 +
 src/wallet.ts                      | 877 +++++++++++++++++++++++++------------
 src/walletTypes.ts                 |  49 ++-
 src/webex/messages.ts              |   6 +-
 src/webex/pages/payback.tsx        |   2 +-
 src/webex/wxApi.ts                 |  10 +-
 src/webex/wxBackend.ts             |  21 +-
 yarn.lock                          |   8 +-
 20 files changed, 1073 insertions(+), 429 deletions(-)

diff --git a/gulpfile.js b/gulpfile.js
index 698944b2..dbdb33cc 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -60,8 +60,6 @@ const paths = {
   dist: [
     "dist/*-bundle.js",
     "dist/*-bundle.js.map",
-    "emscripten/taler-emscripten-lib.js",
-    "emscripten/taler-emscripten-lib.wasm",
     "img/icon.png",
     "img/logo.png",
     "src/webex/**/*.{js,css,html}",
@@ -149,7 +147,7 @@ function dist_prod() {
 }
 
 function compile_prod(callback) {
-  let config = require("./webpack.config.js")({ prod: true });
+  let config = require("./webpack.config.js")({ mode: "production" });
   webpack(config, function(err, stats) {
     if (err) {
       throw new gutil.PluginError("webpack", err);
diff --git a/package.json b/package.json
index df470324..fcff69c9 100644
--- a/package.json
+++ b/package.json
@@ -64,7 +64,7 @@
     "@types/urijs": "^1.19.3",
     "axios": "^0.19.0",
     "big-integer": "^1.6.48",
-    "idb-bridge": "^0.0.14",
+    "idb-bridge": "^0.0.15",
     "qrcode-generator": "^1.4.3",
     "source-map-support": "^0.5.12",
     "urijs": "^1.18.10"
diff --git a/src/crypto/cryptoApi.ts b/src/crypto/cryptoApi.ts
index 46fe2576..b5eae9be 100644
--- a/src/crypto/cryptoApi.ts
+++ b/src/crypto/cryptoApi.ts
@@ -27,7 +27,7 @@ import { AmountJson } from "../amounts";
 import {
   CoinRecord,
   DenominationRecord,
-  PreCoinRecord,
+  PlanchetRecord,
   RefreshSessionRecord,
   ReserveRecord,
   TipPlanchet,
@@ -38,7 +38,7 @@ import { CryptoWorker } from "./cryptoWorker";
 
 import { ContractTerms, PaybackRequest } from "../talerTypes";
 
-import { BenchmarkResult, CoinWithDenom, PayCoinInfo } from "../walletTypes";
+import { BenchmarkResult, CoinWithDenom, PayCoinInfo, PlanchetCreationResult } 
from "../walletTypes";
 
 import * as timer from "../timer";
 
@@ -173,6 +173,7 @@ export class CryptoApi {
    */
   wake(ws: WorkerState, work: WorkItem): void {
     if (this.stopped) {
+      console.log("cryptoApi is stopped");
       CryptoApi.enableTracing && console.log("not waking, as cryptoApi is 
stopped");
       return;
     }
@@ -299,7 +300,6 @@ export class CryptoApi {
     priority: number,
     ...args: any[]
   ): Promise<T> {
-    CryptoApi.enableTracing && console.log("cryptoApi: doRpc called");
     const p: Promise<T> = new Promise<T>((resolve, reject) => {
       const rpcId = this.nextRpcId++;
       const workItem: WorkItem = {
@@ -332,16 +332,14 @@ export class CryptoApi {
       throw Error("assertion failed");
     });
 
-    return p.then((r: T) => {
-      return r;
-    });
+    return p;
   }
 
-  createPreCoin(
+  createPlanchet(
     denom: DenominationRecord,
     reserve: ReserveRecord,
-  ): Promise<PreCoinRecord> {
-    return this.doRpc<PreCoinRecord>("createPreCoin", 1, denom, reserve);
+  ): Promise<PlanchetCreationResult> {
+    return this.doRpc<PlanchetCreationResult>("createPlanchet", 1, denom, 
reserve);
   }
 
   createTipPlanchet(denom: DenominationRecord): Promise<TipPlanchet> {
diff --git a/src/crypto/cryptoImplementation.ts 
b/src/crypto/cryptoImplementation.ts
index 9ffdec70..7cddf903 100644
--- a/src/crypto/cryptoImplementation.ts
+++ b/src/crypto/cryptoImplementation.ts
@@ -28,8 +28,7 @@ import {
   CoinRecord,
   CoinStatus,
   DenominationRecord,
-  PreCoinRecord,
-  RefreshPreCoinRecord,
+  RefreshPlanchetRecord,
   RefreshSessionRecord,
   ReserveRecord,
   TipPlanchet,
@@ -42,6 +41,7 @@ import {
   CoinWithDenom,
   PayCoinInfo,
   Timestamp,
+  PlanchetCreationResult,
 } from "../walletTypes";
 import { canonicalJson, getTalerStampSec } from "../helpers";
 import { AmountJson } from "../amounts";
@@ -154,10 +154,10 @@ export class CryptoImplementation {
    * Create a pre-coin of the given denomination to be withdrawn from then 
given
    * reserve.
    */
-  createPreCoin(
+  createPlanchet(
     denom: DenominationRecord,
     reserve: ReserveRecord,
-  ): PreCoinRecord {
+  ): PlanchetCreationResult {
     const reservePub = decodeCrock(reserve.reservePub);
     const reservePriv = decodeCrock(reserve.reservePriv);
     const denomPub = decodeCrock(denom.denomPub);
@@ -179,7 +179,7 @@ export class CryptoImplementation {
 
     const sig = eddsaSign(withdrawRequest, reservePriv);
 
-    const preCoin: PreCoinRecord = {
+    const planchet: PlanchetCreationResult = {
       blindingKey: encodeCrock(blindingFactor),
       coinEv: encodeCrock(ev),
       coinPriv: encodeCrock(coinKeyPair.eddsaPriv),
@@ -188,11 +188,10 @@ export class CryptoImplementation {
       denomPub: encodeCrock(denomPub),
       denomPubHash: encodeCrock(denomPubHash),
       exchangeBaseUrl: reserve.exchangeBaseUrl,
-      isFromTip: false,
       reservePub: encodeCrock(reservePub),
       withdrawSig: encodeCrock(sig),
     };
-    return preCoin;
+    return planchet;
   }
 
   /**
@@ -424,7 +423,7 @@ export class CryptoImplementation {
     const transferPubs: string[] = [];
     const transferPrivs: string[] = [];
 
-    const preCoinsForGammas: RefreshPreCoinRecord[][] = [];
+    const planchetsForGammas: RefreshPlanchetRecord[][] = [];
 
     for (let i = 0; i < kappa; i++) {
       const transferKeyPair = createEcdheKeyPair();
@@ -442,7 +441,7 @@ export class CryptoImplementation {
     sessionHc.update(amountToBuffer(valueWithFee));
 
     for (let i = 0; i < kappa; i++) {
-      const preCoins: RefreshPreCoinRecord[] = [];
+      const planchets: RefreshPlanchetRecord[] = [];
       for (let j = 0; j < newCoinDenoms.length; j++) {
         const transferPriv = decodeCrock(transferPrivs[i]);
         const oldCoinPub = decodeCrock(meltCoin.coinPub);
@@ -456,16 +455,16 @@ export class CryptoImplementation {
         const pubHash = hash(coinPub);
         const denomPub = decodeCrock(newCoinDenoms[j].denomPub);
         const ev = rsaBlind(pubHash, blindingFactor, denomPub);
-        const preCoin: RefreshPreCoinRecord = {
+        const planchet: RefreshPlanchetRecord = {
           blindingKey: encodeCrock(blindingFactor),
           coinEv: encodeCrock(ev),
           privateKey: encodeCrock(coinPriv),
           publicKey: encodeCrock(coinPub),
         };
-        preCoins.push(preCoin);
+        planchets.push(planchet);
         sessionHc.update(ev);
       }
-      preCoinsForGammas.push(preCoins);
+      planchetsForGammas.push(planchets);
     }
 
     const sessionHash = sessionHc.finish();
@@ -496,7 +495,7 @@ export class CryptoImplementation {
       newDenomHashes: newCoinDenoms.map(d => d.denomPubHash),
       newDenoms: newCoinDenoms.map(d => d.denomPub),
       norevealIndex: undefined,
-      preCoinsForGammas,
+      planchetsForGammas: planchetsForGammas,
       transferPrivs,
       transferPubs,
       valueOutput,
diff --git a/src/crypto/primitives/kdf.ts b/src/crypto/primitives/kdf.ts
index 08296307..e1baed40 100644
--- a/src/crypto/primitives/kdf.ts
+++ b/src/crypto/primitives/kdf.ts
@@ -88,5 +88,5 @@ export function kdf(
     output.set(chunk, i * 32);
   }
 
-  return output;
+  return output.slice(0, outputLength);
 }
diff --git a/src/crypto/talerCrypto.ts b/src/crypto/talerCrypto.ts
index b754b0c5..317b1af5 100644
--- a/src/crypto/talerCrypto.ts
+++ b/src/crypto/talerCrypto.ts
@@ -237,6 +237,9 @@ function rsaFullDomainHash(hm: Uint8Array, rsaPub: RsaPub): 
bigint.BigInteger {
 function rsaPubDecode(rsaPub: Uint8Array): RsaPub {
   const modulusLength = (rsaPub[0] << 8) | rsaPub[1];
   const exponentLength = (rsaPub[2] << 8) | rsaPub[3];
+  if (4 + exponentLength + modulusLength != rsaPub.length) {
+    throw Error("invalid RSA public key (format wrong)");
+  }
   const modulus = rsaPub.slice(4, 4 + modulusLength);
   const exponent = rsaPub.slice(
     4 + modulusLength,
diff --git a/src/dbTypes.ts b/src/dbTypes.ts
index bb4f5dbd..8dba28ed 100644
--- a/src/dbTypes.ts
+++ b/src/dbTypes.ts
@@ -57,6 +57,13 @@ export enum ReserveRecordStatus {
    */
   REGISTERING_BANK = "registering-bank",
 
+  /**
+   * We've registered reserve's information with the bank
+   * and are now waiting for the user to confirm the withdraw
+   * with the bank (typically 2nd factor auth).
+   */
+  WAIT_CONFIRM_BANK = "wait-confirm-bank",
+
   /**
    * Querying reserve status with the exchange.
    */
@@ -117,22 +124,26 @@ export interface ReserveRecord {
   timestampConfirmed: Timestamp | undefined;
 
   /**
-   * Current amount left in the reserve
+   * Amount that's still available for withdrawing
+   * from this reserve.
    */
-  currentAmount: AmountJson | null;
+  withdrawRemainingAmount: AmountJson;
 
   /**
-   * Amount requested when the reserve was created.
-   * When a reserve is re-used (rare!)  the current_amount can
-   * be higher than the requested_amount
+   * Amount allocated for withdrawing.
+   * The corresponding withdraw operation may or may not
+   * have been completed yet.
    */
-  requestedAmount: AmountJson;
+  withdrawAllocatedAmount: AmountJson;
+
+  withdrawCompletedAmount: AmountJson;
 
   /**
-   * What's the current amount that sits
-   * in precoins?
+   * Amount requested when the reserve was created.
+   * When a reserve is re-used (rare!)  the current_amount can
+   * be higher than the requested_amount
    */
-  precoinAmount: AmountJson;
+  initiallyRequestedAmount: AmountJson;
 
   /**
    * We got some payback to this reserve.  We'll cease to automatically
@@ -154,8 +165,19 @@ export interface ReserveRecord {
 
   bankWithdrawStatusUrl?: string;
 
+  /**
+   * URL that the bank gave us to redirect the customer
+   * to in order to confirm a withdrawal.
+   */
+  bankWithdrawConfirmUrl?: string;
+
   reserveStatus: ReserveRecordStatus;
 
+  /**
+   * Time of the last successful status query.
+   */
+  lastStatusQuery: Timestamp | undefined;
+
   lastError?: OperationError;
 }
 
@@ -421,7 +443,16 @@ export interface ExchangeRecord {
 /**
  * A coin that isn't yet signed by an exchange.
  */
-export interface PreCoinRecord {
+export interface PlanchetRecord {
+  withdrawSessionId: string;
+  /**
+   * Index of the coin in the withdrawal session.
+   */
+  coinIndex: number;
+
+  /**
+   * Public key of the coin.
+   */
   coinPub: string;
   coinPriv: string;
   reservePub: string;
@@ -443,7 +474,7 @@ export interface PreCoinRecord {
 /**
  * Planchet for a coin during refrehs.
  */
-export interface RefreshPreCoinRecord {
+export interface RefreshPlanchetRecord {
   /**
    * Public key for the coin.
    */
@@ -485,6 +516,16 @@ export enum CoinStatus {
  * of the wallet database.
  */
 export interface CoinRecord {
+  /**
+   * Withdraw session ID, or "" (empty string) if withdrawn via refresh.
+   */
+  withdrawSessionId: string;
+
+  /**
+   * Index of the coin in the withdrawal session.
+   */
+  coinIndex: number;
+
   /**
    * Public key of the coin.
    */
@@ -546,11 +587,17 @@ export interface CoinRecord {
   status: CoinStatus;
 }
 
+export enum ProposalStatus {
+  PROPOSED = "proposed",
+  ACCEPTED = "accepted",
+  REJECTED = "rejected",
+}
+
 /**
- * Proposal record, stored in the wallet's database.
+ * Record for a downloaded order, stored in the wallet's database.
  */
 @Checkable.Class()
-export class ProposalDownloadRecord {
+export class ProposalRecord {
   /**
    * URL where the proposal was downloaded.
    */
@@ -576,10 +623,10 @@ export class ProposalDownloadRecord {
   contractTermsHash: string;
 
   /**
-   * Serial ID when the offer is stored in the wallet DB.
+   * Unique ID when the order is stored in the wallet DB.
    */
-  @Checkable.Optional(Checkable.Number())
-  id?: number;
+  @Checkable.String()
+  proposalId: string;
 
   /**
    * Timestamp (in ms) of when the record
@@ -594,6 +641,9 @@ export class ProposalDownloadRecord {
   @Checkable.String()
   noncePriv: string;
 
+  @Checkable.String()
+  proposalStatus: ProposalStatus;
+
   /**
    * Session ID we got when downloading the contract.
    */
@@ -604,7 +654,7 @@ export class ProposalDownloadRecord {
    * Verify that a value matches the schema of this class and convert it into a
    * member.
    */
-  static checked: (obj: any) => ProposalDownloadRecord;
+  static checked: (obj: any) => ProposalRecord;
 }
 
 /**
@@ -717,9 +767,9 @@ export interface RefreshSessionRecord {
   newDenoms: string[];
 
   /**
-   * Precoins for each cut-and-choose instance.
+   * Planchets for each cut-and-choose instance.
    */
-  preCoinsForGammas: RefreshPreCoinRecord[][];
+  planchetsForGammas: RefreshPlanchetRecord[][];
 
   /**
    * The transfer keys, kappa of them.
@@ -933,7 +983,9 @@ export interface CoinsReturnRecord {
   wire: any;
 }
 
-export interface WithdrawalRecord {
+export interface WithdrawalSessionRecord {
+  withdrawSessionId: string;
+
   /**
    * Reserve that we're withdrawing from.
    */
@@ -956,9 +1008,29 @@ export interface WithdrawalRecord {
    */
   withdrawalAmount: string;
 
-  numCoinsTotal: number;
+  denoms: string[];
+
+  /**
+   * 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 {
+  /**
+   * The withdraw URI we got from the bank.
+   */
+  talerWithdrawUri: string;
 
-  numCoinsWithdrawn: number;
+  /**
+   * Reserve that was created for the withdraw URI.
+   */
+  reservePub: string;
 }
 
 /* tslint:disable:completed-docs */
@@ -967,7 +1039,7 @@ export interface WithdrawalRecord {
  * The stores and indices for the wallet database.
  */
 export namespace Stores {
-  class ExchangeStore extends Store<ExchangeRecord> {
+  class ExchangesStore extends Store<ExchangeRecord> {
     constructor() {
       super("exchanges", { keyPath: "baseUrl" });
     }
@@ -988,16 +1060,18 @@ export namespace Stores {
       "denomPubIndex",
       "denomPub",
     );
+    byWithdrawalWithIdx = new Index<any, CoinRecord>(
+      this,
+      "planchetsByWithdrawalWithIdxIndex",
+      ["withdrawSessionId", "coinIndex"],
+    );
   }
 
-  class ProposalsStore extends Store<ProposalDownloadRecord> {
+  class ProposalsStore extends Store<ProposalRecord> {
     constructor() {
-      super("proposals", {
-        autoIncrement: true,
-        keyPath: "id",
-      });
+      super("proposals", { keyPath: "proposalId" });
     }
-    urlIndex = new Index<string, ProposalDownloadRecord>(
+    urlIndex = new Index<string, ProposalRecord>(
       this,
       "urlIndex",
       "url",
@@ -1084,28 +1158,39 @@ export namespace Stores {
     }
   }
 
-  class WithdrawalsStore extends Store<WithdrawalRecord> {
+  class WithdrawalSessionsStore extends Store<WithdrawalSessionRecord> {
     constructor() {
-      super("withdrawals", { keyPath: "id", autoIncrement: true });
+      super("withdrawals", { keyPath: "withdrawSessionId" });
     }
-    byReservePub = new Index<string, WithdrawalRecord>(
+    byReservePub = new Index<string, WithdrawalSessionRecord>(
       this,
       "withdrawalsReservePubIndex",
       "reservePub",
     );
   }
 
-  class PreCoinsStore extends Store<PreCoinRecord> {
+  class BankWithdrawUrisStore extends Store<BankWithdrawUriRecord> {
     constructor() {
-      super("precoins", {
+      super("bankWithdrawUris", { keyPath: "talerWithdrawUri" });
+    }
+  }
+
+  class PlanchetsStore extends Store<PlanchetRecord> {
+    constructor() {
+      super("planchets", {
         keyPath: "coinPub",
       });
     }
-    byReservePub = new Index<string, PreCoinRecord>(
+    byReservePub = new Index<string, PlanchetRecord>(
       this,
-      "precoinsReservePubIndex",
+      "planchetsReservePubIndex",
       "reservePub",
     );
+    byWithdrawalWithIdx = new Index<any, PlanchetRecord>(
+      this,
+      "planchetsByWithdrawalWithIdxIndex",
+      ["withdrawSessionId", "coinIndex"],
+    );
   }
 
   export const coins = new CoinsStore();
@@ -1115,8 +1200,8 @@ export namespace Stores {
   export const config = new ConfigStore();
   export const currencies = new CurrenciesStore();
   export const denominations = new DenominationsStore();
-  export const exchanges = new ExchangeStore();
-  export const precoins = new PreCoinsStore();
+  export const exchanges = new ExchangesStore();
+  export const planchets = new PlanchetsStore();
   export const proposals = new ProposalsStore();
   export const refresh = new Store<RefreshSessionRecord>("refresh", {
     keyPath: "refreshSessionId",
@@ -1125,7 +1210,8 @@ export namespace Stores {
   export const purchases = new PurchasesStore();
   export const tips = new TipsStore();
   export const senderWires = new SenderWiresStore();
-  export const withdrawals = new WithdrawalsStore();
+  export const withdrawalSession = new WithdrawalSessionsStore();
+  export const bankWithdrawUris = new BankWithdrawUrisStore();
 }
 
 /* tslint:enable:completed-docs */
diff --git a/src/headless/bank.ts b/src/headless/bank.ts
index f3502100..36f61a71 100644
--- a/src/headless/bank.ts
+++ b/src/headless/bank.ts
@@ -45,6 +45,37 @@ function makeId(length: number): string {
 export class Bank {
   constructor(private bankBaseUrl: string) {}
 
+  async generateWithdrawUri(bankUser: BankUser, amount: string): 
Promise<string> {
+    const body = {
+      amount,
+    };
+
+    const reqUrl = new URI("api/withdraw-headless-uri")
+    .absoluteTo(this.bankBaseUrl)
+    .href();
+
+    const resp = await Axios({
+      method: "post",
+      url: reqUrl,
+      data: body,
+      responseType: "json",
+      headers: {
+        "X-Taler-Bank-Username": bankUser.username,
+        "X-Taler-Bank-Password": bankUser.password,
+      },
+    });
+
+    if (resp.status != 200) {
+      throw Error("failed to create bank reserve");
+    }
+
+    const withdrawUri = resp.data["taler_withdraw_uri"];
+    if (!withdrawUri) {
+      throw Error("Bank's response did not include withdraw URI");
+    }
+    return withdrawUri;
+  }
+
   async createReserve(
     bankUser: BankUser,
     amount: string,
diff --git a/src/headless/clk.ts b/src/headless/clk.ts
index 4a568dc1..828eb24c 100644
--- a/src/headless/clk.ts
+++ b/src/headless/clk.ts
@@ -29,6 +29,7 @@ export let STRING: Converter<string> = new 
Converter<string>();
 export interface OptionArgs<T> {
   help?: string;
   default?: T;
+  onPresentHandler?: (v: T) => void;
 }
 
 export interface ArgumentArgs<T> {
@@ -269,9 +270,6 @@ export class CommandGroup<GN extends keyof any, TG> {
   }
 
   printHelp(progName: string, parents: CommandGroup<any, any>[]) {
-    const chain: CommandGroup<any, any>[] = Array.prototype.concat(parents, [
-      this,
-    ]);
     let usageSpec = "";
     for (let p of parents) {
       usageSpec += (p.name ?? progName) + " ";
@@ -352,6 +350,7 @@ export class CommandGroup<GN extends keyof any, TG> {
               process.exit(-1);
               throw Error("not reached");
             }
+            foundOptions[d.name] = true;
             myArgs[d.name] = true;
           } else {
             if (r.value === undefined) {
@@ -380,6 +379,7 @@ export class CommandGroup<GN extends keyof any, TG> {
             }
             if (opt.isFlag) {
               myArgs[opt.name] = true;
+              foundOptions[opt.name] = true;
             } else {
               if (si == optShort.length - 1) {
                 if (i === unparsedArgs.length - 1) {
@@ -449,6 +449,13 @@ export class CommandGroup<GN extends keyof any, TG> {
       }
     }
 
+    for (let option of this.options) {
+      const ph = option.args.onPresentHandler;
+      if (ph && foundOptions[option.name]) {
+        ph(myArgs[option.name]);
+      }
+    }
+
     if (parsedArgs[this.argKey].help) {
       this.printHelp(progname, parents);
       process.exit(-1);
@@ -546,7 +553,7 @@ export class Program<PN extends keyof any, T> {
     name: N,
     flagspec: string[],
     args: OptionArgs<boolean> = {},
-  ): Program<N, T & SubRecord<PN, N, boolean>> {
+  ): Program<PN, T & SubRecord<PN, N, boolean>> {
     this.mainCommand.flag(name, flagspec, args);
     return this as any;
   }
diff --git a/src/headless/helpers.ts b/src/headless/helpers.ts
index a38ef1db..9faf24da 100644
--- a/src/headless/helpers.ts
+++ b/src/headless/helpers.ts
@@ -34,35 +34,30 @@ import { Bank } from "./bank";
 
 import fs = require("fs");
 import { NodeCryptoWorkerFactory } from "../crypto/nodeProcessWorker";
+import { Logger } from "../logging";
+
+const logger = new Logger("helpers.ts");
 
-const enableTracing = false;
 
 class ConsoleBadge implements Badge {
   startBusy(): void {
-    enableTracing && console.log("NOTIFICATION: busy");
   }
   stopBusy(): void {
-    enableTracing && console.log("NOTIFICATION: busy end");
   }
   showNotification(): void {
-    enableTracing && console.log("NOTIFICATION: show");
   }
   clearNotification(): void {
-    enableTracing && console.log("NOTIFICATION: cleared");
   }
 }
 
 export class NodeHttpLib implements HttpRequestLibrary {
   async get(url: string): Promise<import("../http").HttpResponse> {
-    enableTracing && console.log("making GET request to", url);
     try {
       const resp = await Axios({
         method: "get",
         url: url,
         responseType: "json",
       });
-      enableTracing && console.log("got response", resp.data);
-      enableTracing && console.log("resp type", typeof resp.data);
       return {
         responseJson: resp.data,
         status: resp.status,
@@ -76,7 +71,6 @@ export class NodeHttpLib implements HttpRequestLibrary {
     url: string,
     body: any,
   ): Promise<import("../http").HttpResponse> {
-    enableTracing && console.log("making POST request to", url);
     try {
       const resp = await Axios({
         method: "post",
@@ -84,8 +78,6 @@ export class NodeHttpLib implements HttpRequestLibrary {
         responseType: "json",
         data: body,
       });
-      enableTracing && console.log("got response", resp.data);
-      enableTracing && console.log("resp type", typeof resp.data);
       return {
         responseJson: resp.data,
         status: resp.status,
@@ -149,7 +141,6 @@ export async function getDefaultNodeWallet(
     }
 
     myBackend.afterCommitCallback = async () => {
-      console.log("DATABASE COMMITTED");
       // Allow caller to stop persisting the wallet.
       if (args.persistentStoragePath === undefined) {
         return;
@@ -219,7 +210,7 @@ export async function withdrawTestBalance(
 
   const bankUser = await bank.registerRandomUser();
 
-  console.log("bank user", bankUser);
+  logger.trace(`Registered bank user ${JSON.stringify(bankUser)}`)
 
   const exchangePaytoUri = await myWallet.getExchangePaytoUri(
     exchangeBaseUrl,
@@ -234,6 +225,5 @@ export async function withdrawTestBalance(
   );
 
   await myWallet.confirmReserve({ reservePub: reserveResponse.reservePub });
-
   await myWallet.runUntilReserveDepleted(reservePub);
 }
diff --git a/src/headless/merchant.ts b/src/headless/merchant.ts
index 889eb2d6..423e3d09 100644
--- a/src/headless/merchant.ts
+++ b/src/headless/merchant.ts
@@ -19,9 +19,9 @@
  * Used mostly for integration tests.
  */
 
- /**
-  * Imports.
-  */
+/**
+ * Imports.
+ */
 import axios from "axios";
 import { CheckPaymentResponse } from "../talerTypes";
 import URI = require("urijs");
@@ -30,10 +30,60 @@ import URI = require("urijs");
  * Connection to the *internal* merchant backend.
  */
 export class MerchantBackendConnection {
-  constructor(
-    public merchantBaseUrl: string,
-    public apiKey: string,
-  ) {}
+  async refund(
+    orderId: string,
+    reason: string,
+    refundAmount: string,
+  ): Promise<void> {
+    const reqUrl = new URI("refund").absoluteTo(this.merchantBaseUrl).href();
+    const refundReq = {
+      order_id: orderId,
+      reason,
+      refund: refundAmount,
+    };
+    const resp = await axios({
+      method: "post",
+      url: reqUrl,
+      data: refundReq,
+      responseType: "json",
+      headers: {
+        Authorization: `ApiKey ${this.apiKey}`,
+      },
+    });
+    if (resp.status != 200) {
+      throw Error("failed to do refund");
+    }
+    console.log("response", resp.data);
+    const refundUri = resp.data.taler_refund_uri;
+    if (!refundUri) {
+      throw Error("no refund URI in response");
+    }
+    return refundUri;
+  }
+
+  constructor(public merchantBaseUrl: string, public apiKey: string) {}
+
+  async authorizeTip(amount: string, justification: string) {
+    const reqUrl = new 
URI("tip-authorize").absoluteTo(this.merchantBaseUrl).href();
+    const tipReq = {
+      amount,
+      justification,
+    };
+    const resp = await axios({
+      method: "post",
+      url: reqUrl,
+      data: tipReq,
+      responseType: "json",
+      headers: {
+        Authorization: `ApiKey ${this.apiKey}`,
+      },
+    });
+    const tipUri = resp.data.taler_tip_uri;
+    if (!tipUri) {
+      throw Error("response does not contain tip URI");
+    }
+    return tipUri;
+  }
 
   async createOrder(
     amount: string,
diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
index 90c04dd9..cb2ff055 100644
--- a/src/headless/taler-wallet-cli.ts
+++ b/src/headless/taler-wallet-cli.ts
@@ -26,11 +26,16 @@ import { BridgeIDBFactory, MemoryBackend } from 
"idb-bridge";
 import { Logger } from "../logging";
 import * as Amounts from "../amounts";
 import { decodeCrock } from "../crypto/talerCrypto";
+import { Bank } from "./bank";
 
 const logger = new Logger("taler-wallet-cli.ts");
 
 const walletDbPath = os.homedir + "/" + ".talerwalletdb.json";
 
+function assertUnreachable(x: never): never {
+  throw new Error("Didn't expect to get here");
+}
+
 async function doPay(
   wallet: Wallet,
   payUrl: string,
@@ -78,7 +83,7 @@ async function doPay(
   }
 
   if (pay) {
-    const payRes = await wallet.confirmPay(result.proposalId!, undefined);
+    const payRes = await wallet.confirmPay(result.proposalId, undefined);
     console.log("paid!");
   } else {
     console.log("not paying");
@@ -93,6 +98,12 @@ function applyVerbose(verbose: boolean) {
   }
 }
 
+function printVersion() {
+  const info = require("../../../package.json");
+  console.log(`${info.version}`);
+  process.exit(0);
+}
+
 const walletCli = clk
   .program("wallet", {
     help: "Command line interface for the GNU Taler wallet.",
@@ -101,6 +112,9 @@ const walletCli = clk
     help:
       "Inhibit running certain operations, useful for debugging and testing.",
   })
+  .flag("version", ["-v", "--version"], {
+    onPresentHandler: printVersion,
+  })
   .flag("verbose", ["-V", "--verbose"], {
     help: "Enable verbose output.",
   });
@@ -133,12 +147,21 @@ async function withWallet<T>(
 }
 
 walletCli
-  .subcommand("", "balance", { help: "Show wallet balance." })
+  .subcommand("balance", "balance", { help: "Show wallet balance." })
+  .flag("json", ["--json"], {
+    help: "Show raw JSON.",
+  })
   .action(async args => {
-    console.log("balance command called");
     await withWallet(args, async wallet => {
       const balance = await wallet.getBalances();
-      console.log(JSON.stringify(balance, undefined, 2));
+      if (args.balance.json) {
+        console.log(JSON.stringify(balance, undefined, 2));
+      } else {
+        const currencies = Object.keys(balance.byCurrency).sort();
+        for (const c of currencies) {
+          console.log(Amounts.toString(balance.byCurrency[c].available));
+        }
+      }
     });
   });
 
@@ -205,15 +228,8 @@ walletCli
           process.exit(1);
           return;
         }
-        const { confirmTransferUrl } = await wallet.acceptWithdrawal(
-          uri,
-          selectedExchange,
-        );
-        if (confirmTransferUrl) {
-          console.log("please confirm the transfer at", confirmTransferUrl);
-        }
-      } else {
-        console.error("unrecognized URI");
+        const res = await wallet.acceptWithdrawal(uri, selectedExchange);
+        await wallet.processReserve(res.reservePub);
       }
     });
   });
@@ -258,13 +274,39 @@ const advancedCli = walletCli.subcommand("advancedArgs", 
"advanced", {
 
 advancedCli
   .subcommand("decode", "decode", {
-    help: "Decode base32-crockford",
+    help: "Decode base32-crockford.",
   })
   .action(args => {
-    const enc = fs.readFileSync(0, 'utf8');
-    fs.writeFileSync(1, decodeCrock(enc.trim()))
+    const enc = fs.readFileSync(0, "utf8");
+    fs.writeFileSync(1, decodeCrock(enc.trim()));
   });
 
+advancedCli
+  .subcommand("payPrepare", "pay-prepare", {
+    help: "Claim an order but don't pay yet.",
+  })
+  .requiredArgument("url", clk.STRING)
+  .action(async args => {
+    await withWallet(args, async wallet => {
+      const res = await wallet.preparePay(args.payPrepare.url);
+      switch (res.status) {
+        case "error":
+          console.log("error:", res.error);
+          break;
+        case "insufficient-balance":
+          console.log("insufficient balance");
+          break;
+        case "paid":
+          console.log("already paid");
+          break;
+        case "payment-possible":
+          console.log("payment possible");
+          break;
+        default:
+          assertUnreachable(res);
+      }
+    });
+  });
 
 advancedCli
   .subcommand("refresh", "force-refresh", {
@@ -288,7 +330,9 @@ advancedCli
         console.log(`coin ${coin.coinPub}`);
         console.log(` status ${coin.status}`);
         console.log(` exchange ${coin.exchangeBaseUrl}`);
-        console.log(` remaining amount 
${Amounts.toString(coin.currentAmount)}`);
+        console.log(
+          ` remaining amount ${Amounts.toString(coin.currentAmount)}`,
+        );
       }
     });
   });
@@ -324,12 +368,11 @@ testCli
       return;
     }
     console.log("taler pay URI:", talerPayUri);
-    await withWallet(args, async (wallet) => {
+    await withWallet(args, async wallet => {
       await doPay(wallet, talerPayUri, { alwaysYes: true });
     });
   });
 
-
 testCli
   .subcommand("integrationtestCmd", "integrationtest", {
     help: "Run integration test with bank, exchange and merchant.",
@@ -377,7 +420,74 @@ testCli
   });
 
 testCli
-  .subcommand("testMerchantQrcodeCmd", "test-merchant-qrcode")
+  .subcommand("genTipUri", "gen-tip-uri", {
+    help: "Generate a taler://tip URI.",
+  })
+  .requiredOption("amount", ["-a", "--amount"], clk.STRING, {
+    default: "TESTKUDOS:10",
+  })
+  .action(async args => {
+    const merchantBackend = new MerchantBackendConnection(
+      "https://backend.test.taler.net/";,
+      "sandbox",
+    );
+    const tipUri = await merchantBackend.authorizeTip("TESTKUDOS:10", "test");
+    console.log(tipUri);
+  });
+
+testCli
+  .subcommand("genRefundUri", "gen-refund-uri", {
+    help: "Generate a taler://refund URI.",
+  })
+  .requiredOption("amount", ["-a", "--amount"], clk.STRING, {
+    default: "TESTKUDOS:5",
+  })
+  .requiredOption("refundAmount", ["-r", "--refund"], clk.STRING, {
+    default: "TESTKUDOS:3",
+  })
+  .requiredOption("summary", ["-s", "--summary"], clk.STRING, {
+    default: "Test Payment (for refund)",
+  })
+  .action(async args => {
+    const cmdArgs = args.genRefundUri;
+    const merchantBackend = new MerchantBackendConnection(
+      "https://backend.test.taler.net/";,
+      "sandbox",
+    );
+    const orderResp = await merchantBackend.createOrder(
+      cmdArgs.amount,
+      cmdArgs.summary,
+      "",
+    );
+    console.log("created new order with order ID", orderResp.orderId);
+    const checkPayResp = await merchantBackend.checkPayment(orderResp.orderId);
+    const talerPayUri = checkPayResp.taler_pay_uri;
+    if (!talerPayUri) {
+      console.error("fatal: no taler pay URI received from backend");
+      process.exit(1);
+      return;
+    }
+    await withWallet(args, async wallet => {
+      await doPay(wallet, talerPayUri, { alwaysYes: true });
+    });
+    const refundUri = await merchantBackend.refund(
+      orderResp.orderId,
+      "test refund",
+      cmdArgs.refundAmount,
+    );
+    console.log(refundUri);
+  });
+
+testCli
+  .subcommand("genPayUri", "gen-pay-uri", {
+    help: "Generate a taler://pay URI.",
+  })
+  .flag("qrcode", ["--qr"], {
+    help: "Show a QR code with the taler://pay URI",
+  })
+  .flag("wait", ["--wait"], {
+    help: "Wait until payment has completed",
+  })
   .requiredOption("amount", ["-a", "--amount"], clk.STRING, {
     default: "TESTKUDOS:1",
   })
@@ -385,8 +495,7 @@ testCli
     default: "Test Payment",
   })
   .action(async args => {
-    const cmdArgs = args.testMerchantQrcodeCmd;
-    applyVerbose(args.wallet.verbose);
+    const cmdArgs = args.genPayUri;
     console.log("creating order");
     const merchantBackend = new MerchantBackendConnection(
       "https://backend.test.taler.net/";,
@@ -399,7 +508,6 @@ testCli
     );
     console.log("created new order with order ID", orderResp.orderId);
     const checkPayResp = await merchantBackend.checkPayment(orderResp.orderId);
-    const qrcode = qrcodeGenerator(0, "M");
     const talerPayUri = checkPayResp.taler_pay_uri;
     if (!talerPayUri) {
       console.error("fatal: no taler pay URI received from backend");
@@ -407,18 +515,23 @@ testCli
       return;
     }
     console.log("taler pay URI:", talerPayUri);
-    qrcode.addData(talerPayUri);
-    qrcode.make();
-    console.log(qrcode.createASCII());
-    console.log("waiting for payment ...");
-    while (1) {
-      await asyncSleep(500);
-      const checkPayResp2 = await merchantBackend.checkPayment(
-        orderResp.orderId,
-      );
-      if (checkPayResp2.paid) {
-        console.log("payment successfully received!");
-        break;
+    if (cmdArgs.qrcode) {
+      const qrcode = qrcodeGenerator(0, "M");
+      qrcode.addData(talerPayUri);
+      qrcode.make();
+      console.log(qrcode.createASCII());
+    }
+    if (cmdArgs.wait) {
+      console.log("waiting for payment ...");
+      while (1) {
+        await asyncSleep(500);
+        const checkPayResp2 = await merchantBackend.checkPayment(
+          orderResp.orderId,
+        );
+        if (checkPayResp2.paid) {
+          console.log("payment successfully received!");
+          break;
+        }
       }
     }
   });
diff --git a/src/wallet-test.ts b/src/wallet-test.ts
index 86ddb5e7..fef11ae5 100644
--- a/src/wallet-test.ts
+++ b/src/wallet-test.ts
@@ -47,6 +47,8 @@ function fakeCwd(current: string, value: string, feeDeposit: 
string): types.Coin
       denomSig: "(mock)",
       exchangeBaseUrl: "(mock)",
       reservePub: "(mock)",
+      coinIndex: -1,
+      withdrawSessionId: "",
       status: dbTypes.CoinStatus.Fresh,
     },
     denom: {
diff --git a/src/wallet.ts b/src/wallet.ts
index f1d7be5e..8fe8d367 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -1,6 +1,6 @@
 /*
  This file is part of TALER
- (C) 2015 GNUnet e.V.
+ (C) 2015-2019 GNUnet e.V.
 
  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
@@ -58,19 +58,19 @@ import {
   DenominationRecord,
   DenominationStatus,
   ExchangeRecord,
-  PreCoinRecord,
-  ProposalDownloadRecord,
+  PlanchetRecord,
+  ProposalRecord,
   PurchaseRecord,
-  RefreshPreCoinRecord,
+  RefreshPlanchetRecord,
   RefreshSessionRecord,
   ReserveRecord,
   Stores,
   TipRecord,
   WireFee,
-  WithdrawalRecord,
-  ExchangeDetails,
+  WithdrawalSessionRecord,
   ExchangeUpdateStatus,
   ReserveRecordStatus,
+  ProposalStatus,
 } from "./dbTypes";
 import {
   Auditor,
@@ -128,14 +128,15 @@ import {
   parseTipUri,
   parseRefundUri,
 } from "./taleruri";
-import { isFirefox } from "./webex/compat";
 import { Logger } from "./logging";
+import { randomBytes } from "./crypto/primitives/nacl-fast";
+import { encodeCrock, getRandomBytes } from "./crypto/talerCrypto";
 
 interface SpeculativePayData {
   payCoinInfo: PayCoinInfo;
   exchangeUrl: string;
-  proposalId: number;
-  proposal: ProposalDownloadRecord;
+  orderDownloadId: string;
+  proposal: ProposalRecord;
 }
 
 /**
@@ -166,7 +167,7 @@ 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;
+  const stillOkay = d.stampExpireWithdraw.t_ms + 60 * 1000 > now.t_ms;
   return started && stillOkay;
 }
 
@@ -175,6 +176,10 @@ interface SelectPayCoinsResult {
   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.
@@ -353,6 +358,43 @@ 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.
  */
@@ -369,6 +411,8 @@ export class Wallet {
   private speculativePayData: SpeculativePayData | undefined;
   private cachedNextUrl: { [fulfillmentUrl: string]: NextUrlResult } = {};
 
+  private memoProcessReserve = new AsyncOpMemo<void>();
+
   constructor(
     db: IDBDatabase,
     http: HttpRequestLibrary,
@@ -384,32 +428,51 @@ export class Wallet {
   }
 
   /**
-   * Process pending operations.
+   * Execute one operation based on the pending operation info record.
    */
-  public async runPending(): Promise<void> {
-    // FIXME:  maybe prioritize pending operations by their urgency?
-    const exchangeBaseUrlList = await oneShotIter(
-      this.db,
-      Stores.exchanges,
-    ).map(x => x.baseUrl);
-
-    for (let exchangeBaseUrl of exchangeBaseUrlList) {
-      await this.updateExchangeFromUrl(exchangeBaseUrl);
-    }
-
-    const reservesPubList = await oneShotIter(this.db, Stores.reserves).map(
-      x => x.reservePub,
-    );
-
-    for (let reservePub of reservesPubList) {
-      await this.processReserve(reservePub);
+  async processOnePendingOperation(
+    pending: PendingOperationInfo,
+  ): Promise<void> {
+    switch (pending.type) {
+      case "bug":
+        return;
+      case "dirty-coin":
+        await this.refresh(pending.coinPub);
+        break;
+      case "exchange-update":
+        await this.updateExchangeFromUrl(pending.exchangeBaseUrl);
+        break;
+      case "planchet":
+        await this.processPlanchet(pending.coinPub);
+        break;
+      case "refresh":
+        await this.processRefreshSession(pending.refreshSessionId);
+        break;
+      case "reserve":
+        await this.processReserve(pending.reservePub);
+        break;
+      case "withdraw":
+        await this.processWithdrawSession(pending.withdrawSessionId);
+        break;
+      case "proposal":
+        // Nothing to do, user needs to accept/reject
+        break;
+      default:
+        assertUnreachable(pending);
     }
+  }
 
-    const refreshSessionList = await oneShotIter(this.db, Stores.refresh).map(
-      x => x.refreshSessionId,
-    );
-    for (let rs of refreshSessionList) {
-      await this.processRefreshSession(rs);
+  /**
+   * Process pending operations.
+   */
+  public async runPending(): Promise<void> {
+    const pendingOpsResponse = await this.getPendingOperations();
+    for (const p of pendingOpsResponse.pendingOperations) {
+      try {
+        await this.processOnePendingOperation(p);
+      } catch (e) {
+        console.error(e);
+      }
     }
   }
 
@@ -427,29 +490,23 @@ export class Wallet {
    */
   public async runUntilReserveDepleted(reservePub: string) {
     while (true) {
-      let reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
-      if (!reserve) {
-        throw Error("Reserve does not exist.");
-      }
-      if (reserve.lastError !== undefined) {
-        throw Error("Reserve error: " + reserve.lastError.message);
-      }
-      if (reserve.reserveStatus === ReserveRecordStatus.UNCONFIRMED) {
-        throw Error("Reserve is not confirmed.");
-      }
-      if (reserve.reserveStatus === ReserveRecordStatus.DORMANT) {
-        // Check if all withdraws are done!
-        const precoins = await oneShotIterIndex(
-          this.db,
-          Stores.precoins.byReservePub,
-          reservePub,
-        ).toArray();
-        for (const pc of precoins) {
-          await this.processPreCoin(pc.coinPub);
+      const r = await this.getPendingOperations();
+      const allPending = r.pendingOperations;
+      const relevantPending = allPending.filter(x => {
+        switch (x.type) {
+          case "planchet":
+          case "reserve":
+            return x.reservePub === reservePub;
+          default:
+            return false;
         }
-        break;
+      });
+      if (relevantPending.length === 0) {
+        return;
+      }
+      for (const p of relevantPending) {
+        await this.processOnePendingOperation(p);
       }
-      await this.processReserve(reservePub);
     }
   }
 
@@ -478,18 +535,6 @@ export class Wallet {
     );
   }
 
-  async updateExchanges(): Promise<void> {
-    const exchangeUrls = await oneShotIter(this.db, Stores.exchanges).map(
-      e => e.baseUrl,
-    );
-
-    for (const url of exchangeUrls) {
-      this.updateExchangeFromUrl(url).catch(e => {
-        console.error("updating exchange failed", e);
-      });
-    }
-  }
-
   private async getCoinsForReturn(
     exchangeBaseUrl: string,
     amount: AmountJson,
@@ -554,8 +599,6 @@ export class Wallet {
       cds.push({ coin, denom });
     }
 
-    console.log("coin return:  selecting from possible coins", { cds, amount 
});
-
     const res = selectPayCoins(denoms, cds, amount, amount);
     if (res) {
       return res.cds;
@@ -711,7 +754,7 @@ export class Wallet {
    * pay for a proposal in the wallet's database.
    */
   private async recordConfirmPay(
-    proposal: ProposalDownloadRecord,
+    proposal: ProposalRecord,
     payCoinInfo: PayCoinInfo,
     chosenExchange: string,
   ): Promise<PurchaseRecord> {
@@ -774,7 +817,7 @@ export class Wallet {
       };
     }
 
-    let proposalId: number;
+    let proposalId: string;
     try {
       proposalId = await this.downloadProposal(
         uriResult.downloadUrl,
@@ -788,7 +831,7 @@ export class Wallet {
     }
     const proposal = await this.getProposal(proposalId);
     if (!proposal) {
-      throw Error("could not get proposal");
+      throw Error(`could not get proposal ${proposalId}`);
     }
 
     console.log("proposal", proposal);
@@ -868,7 +911,7 @@ export class Wallet {
         return {
           status: "insufficient-balance",
           contractTerms: proposal.contractTerms,
-          proposalId: proposal.id!,
+          proposalId: proposal.proposalId,
         };
       }
 
@@ -876,7 +919,7 @@ export class Wallet {
       if (
         !this.speculativePayData ||
         (this.speculativePayData &&
-          this.speculativePayData.proposalId !== proposalId)
+          this.speculativePayData.orderDownloadId !== proposalId)
       ) {
         const { exchangeUrl, cds, totalAmount } = res;
         const payCoinInfo = await this.cryptoApi.signDeposit(
@@ -888,7 +931,7 @@ export class Wallet {
           exchangeUrl,
           payCoinInfo,
           proposal,
-          proposalId,
+          orderDownloadId: proposalId,
         };
         Wallet.enableTracing &&
           console.log("created speculative pay data for payment");
@@ -897,7 +940,7 @@ export class Wallet {
       return {
         status: "payment-possible",
         contractTerms: proposal.contractTerms,
-        proposalId: proposal.id!,
+        proposalId: proposal.proposalId,
         totalFees: res.totalFees,
       };
     }
@@ -920,14 +963,14 @@ export class Wallet {
    * @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<number> {
+  async downloadProposal(url: string, sessionId?: string): Promise<string> {
     const oldProposal = await oneShotGetIndexed(
       this.db,
       Stores.proposals.urlIndex,
       url,
     );
     if (oldProposal) {
-      return oldProposal.id!;
+      return oldProposal.proposalId;
     }
 
     const { priv, pub } = await this.cryptoApi.createEddsaKeypair();
@@ -946,7 +989,9 @@ export class Wallet {
 
     const contractTermsHash = await this.hashContract(proposal.contract_terms);
 
-    const proposalRecord: ProposalDownloadRecord = {
+    const proposalId = encodeCrock(getRandomBytes(32));
+
+    const proposalRecord: ProposalRecord = {
       contractTerms: proposal.contract_terms,
       contractTermsHash,
       merchantSig: proposal.sig,
@@ -954,14 +999,13 @@ export class Wallet {
       timestamp: getTimestampNow(),
       url,
       downloadSessionId: sessionId,
+      proposalId: proposalId,
+      proposalStatus: ProposalStatus.PROPOSED,
     };
-
-    const id = await oneShotPut(this.db, Stores.proposals, proposalRecord);
+    await oneShotPut(this.db, Stores.proposals, proposalRecord);
     this.notifier.notify();
-    if (typeof id !== "number") {
-      throw Error("db schema wrong");
-    }
-    return id;
+
+    return proposalId;
   }
 
   async refundFailedPay(proposalId: number) {
@@ -1091,7 +1135,7 @@ export class Wallet {
    * Add a contract to the wallet and sign coins, and send them.
    */
   async confirmPay(
-    proposalId: number,
+    proposalId: string,
     sessionIdOverride: string | undefined,
   ): Promise<ConfirmPayResult> {
     Wallet.enableTracing &&
@@ -1175,13 +1219,13 @@ export class Wallet {
    * Get the speculative pay data, but only if coins have not changed in 
between.
    */
   async getSpeculativePayData(
-    proposalId: number,
+    proposalId: string,
   ): Promise<SpeculativePayData | undefined> {
     const sp = this.speculativePayData;
     if (!sp) {
       return;
     }
-    if (sp.proposalId !== proposalId) {
+    if (sp.orderDownloadId !== proposalId) {
       return;
     }
     const coinKeys = sp.payCoinInfo.updatedCoins.map(x => x.coinPub);
@@ -1209,58 +1253,104 @@ export class Wallet {
     return sp;
   }
 
-  /**
-   * Send reserve details to the bank.
-   */
-  private async sendReserveInfoToBank(reservePub: string) {
-    const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
-    if (!reserve) {
-      throw Error("reserve not in db");
-    }
-
-    if (reserve.reserveStatus != ReserveRecordStatus.REGISTERING_BANK) {
-      return;
+  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) {
-      throw Error("no bank withdraw status URL available.");
+      return;
     }
 
-    const now = getTimestampNow();
-    let status;
+    let status: WithdrawOperationStatusResponse;
     try {
       const statusResp = await this.http.get(bankStatusUrl);
       status = 
WithdrawOperationStatusResponse.checked(statusResp.responseJson);
     } catch (e) {
-      console.log("bank error response", 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;
       });
-    } else if (reserve.timestampReserveInfoPosted === undefined) {
-      try {
-        if (!status.selection_done) {
-          const bankResp = await this.http.postJson(bankStatusUrl, {
-            reserve_pub: reservePub,
-            selected_exchange: reserve.exchangeWire,
-          });
-        }
-      } catch (e) {
-        console.log("bank error response", e);
-        throw e;
-      }
+      await this.processReserveImpl(reservePub);
+    } else {
       await oneShotMutate(this.db, Stores.reserves, reservePub, r => {
-        r.timestampReserveInfoPosted = now;
+        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);
+  }
+
   /**
    * First fetch information requred to withdraw from the reserve,
    * then deplete the reserve, withdrawing coins until it is empty.
@@ -1269,6 +1359,18 @@ 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");
@@ -1282,19 +1384,23 @@ export class Wallet {
         // nothing to do
         break;
       case ReserveRecordStatus.REGISTERING_BANK:
-        await this.sendReserveInfoToBank(reservePub);
-        return this.processReserve(reservePub);
+        await this.processReserveBankStatus(reservePub);
+        return this.processReserveImpl(reservePub);
       case ReserveRecordStatus.QUERYING_STATUS:
         await this.updateReserve(reservePub);
-        return this.processReserve(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;
     }
   }
@@ -1302,38 +1408,38 @@ export class Wallet {
   /**
    * Given a planchet, withdraw a coin from the exchange.
    */
-  private async processPreCoin(preCoinPub: string): Promise<void> {
-    console.log("processPreCoin", preCoinPub);
-    const preCoin = await oneShotGet(this.db, Stores.precoins, preCoinPub);
-    if (!preCoin) {
-      console.log("processPreCoin: preCoinPub not found");
+  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,
-      preCoin.exchangeBaseUrl,
+      planchet.exchangeBaseUrl,
     );
     if (!exchange) {
-      console.error("db inconsistent: exchange for precoin not found");
+      console.error("db inconsistent: exchange for planchet not found");
       return;
     }
 
     const denom = await oneShotGet(this.db, Stores.denominations, [
-      preCoin.exchangeBaseUrl,
-      preCoin.denomPub,
+      planchet.exchangeBaseUrl,
+      planchet.denomPub,
     ]);
 
     if (!denom) {
-      console.error("db inconsistent: denom for precoin not found");
+      console.error("db inconsistent: denom for planchet not found");
       return;
     }
 
     const wd: any = {};
-    wd.denom_pub_hash = preCoin.denomPubHash;
-    wd.reserve_pub = preCoin.reservePub;
-    wd.reserve_sig = preCoin.withdrawSig;
-    wd.coin_ev = preCoin.coinEv;
+    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);
 
@@ -1341,51 +1447,60 @@ export class Wallet {
 
     const denomSig = await this.cryptoApi.rsaUnblind(
       r.ev_sig,
-      preCoin.blindingKey,
-      preCoin.denomPub,
+      planchet.blindingKey,
+      planchet.denomPub,
     );
 
     const coin: CoinRecord = {
-      blindingKey: preCoin.blindingKey,
-      coinPriv: preCoin.coinPriv,
-      coinPub: preCoin.coinPub,
-      currentAmount: preCoin.coinValue,
-      denomPub: preCoin.denomPub,
-      denomPubHash: preCoin.denomPubHash,
+      blindingKey: planchet.blindingKey,
+      coinPriv: planchet.coinPriv,
+      coinPub: planchet.coinPub,
+      currentAmount: planchet.coinValue,
+      denomPub: planchet.denomPub,
+      denomPubHash: planchet.denomPubHash,
       denomSig,
-      exchangeBaseUrl: preCoin.exchangeBaseUrl,
-      reservePub: preCoin.reservePub,
+      exchangeBaseUrl: planchet.exchangeBaseUrl,
+      reservePub: planchet.reservePub,
       status: CoinStatus.Fresh,
-    };
-
-    const mutateReserve = (r: ReserveRecord) => {
-      const x = Amounts.sub(
-        r.precoinAmount,
-        preCoin.coinValue,
-        denom.feeWithdraw,
-      );
-      if (x.saturated) {
-        // FIXME!!!!
-        console.error("database inconsistent");
-        throw TransactionAbort;
-      }
-      r.precoinAmount = x.amount;
-      return r;
+      coinIndex: planchet.coinIndex,
+      withdrawSessionId: planchet.withdrawSessionId,
     };
 
     await runWithWriteTransaction(
       this.db,
-      [Stores.reserves, Stores.precoins, Stores.coins],
+      [Stores.planchets, Stores.coins, Stores.withdrawalSession, 
Stores.reserves],
       async tx => {
-        const currentPc = await tx.get(Stores.precoins, coin.coinPub);
+        const currentPc = await tx.get(Stores.planchets, coin.coinPub);
         if (!currentPc) {
           return;
         }
-        await tx.mutate(Stores.reserves, preCoin.reservePub, mutateReserve);
-        await tx.delete(Stores.precoins, coin.coinPub);
+        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`);
   }
 
@@ -1409,13 +1524,16 @@ export class Wallet {
       reserveStatus = ReserveRecordStatus.UNCONFIRMED;
     }
 
+    const currency = req.amount.currency;
+
     const reserveRecord: ReserveRecord = {
       created: now,
-      currentAmount: null,
+      withdrawAllocatedAmount: Amounts.getZero(currency),
+      withdrawCompletedAmount: Amounts.getZero(currency),
+      withdrawRemainingAmount: Amounts.getZero(currency),
       exchangeBaseUrl: canonExchange,
       hasPayback: false,
-      precoinAmount: Amounts.getZero(req.amount.currency),
-      requestedAmount: req.amount,
+      initiallyRequestedAmount: req.amount,
       reservePriv: keypair.priv,
       reservePub: keypair.pub,
       senderWire: req.senderWire,
@@ -1424,6 +1542,7 @@ export class Wallet {
       bankWithdrawStatusUrl: req.bankWithdrawStatusUrl,
       exchangeWire: req.exchangeWire,
       reserveStatus,
+      lastStatusQuery: undefined,
     };
 
     const senderWire = req.senderWire;
@@ -1463,24 +1582,50 @@ export class Wallet {
 
     const cr: CurrencyRecord = currencyRecord;
 
-    await runWithWriteTransaction(
+    const resp = await runWithWriteTransaction(
       this.db,
-      [Stores.currencies, Stores.reserves],
+      [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;
       },
     );
 
-    this.processReserve(keypair.pub).catch(e => {
+    // Asynchronously process the reserve, but return
+    // to the caller already.
+    this.processReserve(resp.reservePub).catch(e => {
       console.error("Processing reserve failed:", e);
     });
 
-    const r: CreateReserveResponse = {
-      exchange: canonExchange,
-      reservePub: keypair.pub,
-    };
-    return r;
+    return resp;
   }
 
   /**
@@ -1526,15 +1671,15 @@ export class Wallet {
     }
     logger.trace(`depleting reserve ${reservePub}`);
 
-    const withdrawAmount = reserve.currentAmount;
-    if (!withdrawAmount) {
-      throw Error("BUG: reserveStatus=WITHDRAWING, but currentAmount is 
empty");
-    }
+    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, {
@@ -1542,23 +1687,24 @@ export class Wallet {
         message: m,
         details: {},
       });
+      console.log(m);
       throw new OperationFailedAndReportedError(m);
     }
 
-    const withdrawalRecord: WithdrawalRecord = {
+    logger.trace("selected denominations");
+
+    const withdrawalSessionId = encodeCrock(randomBytes(32));
+
+    const withdrawalRecord: WithdrawalSessionRecord = {
+      withdrawSessionId: withdrawalSessionId,
       reservePub: reserve.reservePub,
       withdrawalAmount: Amounts.toString(withdrawAmount),
       startTimestamp: getTimestampNow(),
-      numCoinsTotal: denomsForWithdraw.length,
-      numCoinsWithdrawn: 0,
+      denoms: denomsForWithdraw.map(x => x.denomPub),
+      withdrawn: denomsForWithdraw.map(x => false),
+      planchetCreated: denomsForWithdraw.map(x => false),
     };
 
-    const preCoinRecords: PreCoinRecord[] = await Promise.all(
-      denomsForWithdraw.map(async denom => {
-        return await this.cryptoApi.createPreCoin(denom, reserve);
-      }),
-    );
-
     const totalCoinValue = Amounts.sum(denomsForWithdraw.map(x => x.value))
       .amount;
     const totalCoinWithdrawFee = Amounts.sum(
@@ -1570,20 +1716,24 @@ export class Wallet {
     ).amount;
 
     function mutateReserve(r: ReserveRecord): ReserveRecord {
-      const currentAmount = r.currentAmount;
-      if (!currentAmount) {
-        throw Error("can't withdraw when amount is unknown");
+      const remaining = Amounts.sub(
+        r.withdrawRemainingAmount,
+        totalWithdrawAmount,
+      );
+      if (remaining.saturated) {
+        console.error("can't create planchets, saturated");
+        throw TransactionAbort;
       }
-      r.precoinAmount = Amounts.add(
-        r.precoinAmount,
+      const allocated = Amounts.add(
+        r.withdrawAllocatedAmount,
         totalWithdrawAmount,
-      ).amount;
-      const result = Amounts.sub(currentAmount, totalWithdrawAmount);
-      if (result.saturated) {
-        console.error("can't create precoins, saturated");
+      );
+      if (allocated.saturated) {
+        console.error("can't create planchets, saturated");
         throw TransactionAbort;
       }
-      r.currentAmount = result.amount;
+      r.withdrawRemainingAmount = remaining.amount;
+      r.withdrawAllocatedAmount = allocated.amount;
       r.reserveStatus = ReserveRecordStatus.DORMANT;
 
       return r;
@@ -1591,7 +1741,7 @@ export class Wallet {
 
     const success = await runWithWriteTransaction(
       this.db,
-      [Stores.precoins, Stores.withdrawals, Stores.reserves],
+      [Stores.planchets, Stores.withdrawalSession, Stores.reserves],
       async tx => {
         const myReserve = await tx.get(Stores.reserves, reservePub);
         if (!myReserve) {
@@ -1600,20 +1750,113 @@ export class Wallet {
         if (myReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
           return false;
         }
-        for (let pcr of preCoinRecords) {
-          await tx.put(Stores.precoins, pcr);
-        }
         await tx.mutate(Stores.reserves, reserve.reservePub, mutateReserve);
-        await tx.put(Stores.withdrawals, withdrawalRecord);
+        await tx.put(Stores.withdrawalSession, withdrawalRecord);
         return true;
       },
     );
 
     if (success) {
-      logger.trace(`withdrawing ${preCoinRecords.length} coins`);
-      for (let x of preCoinRecords) {
-        await this.processPreCoin(x.coinPub);
+      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;
+  }
+
+  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],
+    );
+
+    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);
     }
   }
 
@@ -1644,7 +1887,6 @@ export class Wallet {
       resp = await this.http.get(reqUrl.href());
     } catch (e) {
       if (e.response?.status === 404) {
-        console.log("Reserve now known to exchange (yet).");
         return;
       } else {
         const m = e.message;
@@ -1657,15 +1899,40 @@ export class Wallet {
       }
     }
     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;
       }
-      reserve.currentAmount = Amounts.parseOrThrow(reserveInfo.balance);
-      reserve.reserveStatus = ReserveRecordStatus.WITHDRAWING;
+
+      // 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;
     });
-    await oneShotPut(this.db, Stores.reserves, reserve);
     this.notifier.notify();
   }
 
@@ -1752,15 +2019,21 @@ export class Wallet {
       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[];
@@ -1769,12 +2042,15 @@ export class Wallet {
       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;
@@ -1789,6 +2065,8 @@ export class Wallet {
       }
     } while (selectedDenoms.length > 0 && !allValid);
 
+    console.log("returning denoms");
+
     return selectedDenoms;
   }
 
@@ -1958,11 +2236,9 @@ export class Wallet {
     exchangeBaseUrl: string,
     supportedTargetTypes: string[],
   ): Promise<string> {
-    const exchangeRecord = await oneShotGet(
-      this.db,
-      Stores.exchanges,
-      exchangeBaseUrl,
-    );
+    // 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.`);
     }
@@ -2347,34 +2623,6 @@ export class Wallet {
           );
         });
 
-        await tx.iter(Stores.reserves).forEach(r => {
-          if (!r.timestampConfirmed) {
-            return;
-          }
-          let amount = Amounts.getZero(r.requestedAmount.currency);
-          amount = Amounts.add(amount, r.precoinAmount).amount;
-          addTo(balanceStore, "pendingIncoming", amount, r.exchangeBaseUrl);
-          addTo(
-            balanceStore,
-            "pendingIncomingWithdraw",
-            amount,
-            r.exchangeBaseUrl,
-          );
-        });
-
-        await tx.iter(Stores.reserves).forEach(r => {
-          if (!r.hasPayback) {
-            return;
-          }
-          addTo(
-            balanceStore,
-            "paybackAmount",
-            r.currentAmount!,
-            r.exchangeBaseUrl,
-          );
-          return balanceStore;
-        });
-
         await tx.iter(Stores.purchases).forEach(t => {
           if (t.finished) {
             return;
@@ -2598,8 +2846,8 @@ export class Wallet {
     const privs = Array.from(refreshSession.transferPrivs);
     privs.splice(norevealIndex, 1);
 
-    const preCoins = refreshSession.preCoinsForGammas[norevealIndex];
-    if (!preCoins) {
+    const planchets = refreshSession.planchetsForGammas[norevealIndex];
+    if (!planchets) {
       throw Error("refresh index error");
     }
 
@@ -2612,7 +2860,7 @@ export class Wallet {
       throw Error("inconsistent database");
     }
 
-    const evs = preCoins.map((x: RefreshPreCoinRecord) => x.coinEv);
+    const evs = planchets.map((x: RefreshPlanchetRecord) => x.coinEv);
 
     const linkSigs: string[] = [];
     for (let i = 0; i < refreshSession.newDenoms.length; i++) {
@@ -2621,7 +2869,7 @@ export class Wallet {
         refreshSession.newDenomHashes[i],
         refreshSession.meltCoinPub,
         refreshSession.transferPubs[norevealIndex],
-        preCoins[i].coinEv,
+        planchets[i].coinEv,
       );
       linkSigs.push(linkSig);
     }
@@ -2682,7 +2930,7 @@ export class Wallet {
         continue;
       }
       const pc =
-        refreshSession.preCoinsForGammas[refreshSession.norevealIndex!][i];
+        refreshSession.planchetsForGammas[refreshSession.norevealIndex!][i];
       const denomSig = await this.cryptoApi.rsaUnblind(
         respJson.ev_sigs[i].ev_sig,
         pc.blindingKey,
@@ -2699,6 +2947,8 @@ export class Wallet {
         exchangeBaseUrl: refreshSession.exchangeBaseUrl,
         reservePub: undefined,
         status: CoinStatus.Fresh,
+        coinIndex: -1,
+        withdrawSessionId: "",
       };
 
       coins.push(coin);
@@ -2761,7 +3011,7 @@ export class Wallet {
 
     const withdrawals = await oneShotIter(
       this.db,
-      Stores.withdrawals,
+      Stores.withdrawalSession,
     ).toArray();
     for (const w of withdrawals) {
       history.push({
@@ -2822,7 +3072,7 @@ export class Wallet {
       history.push({
         detail: {
           exchangeBaseUrl: r.exchangeBaseUrl,
-          requestedAmount: Amounts.toString(r.requestedAmount),
+          requestedAmount: Amounts.toString(r.initiallyRequestedAmount),
           reservePub: r.reservePub,
           reserveType,
           bankWithdrawStatusUrl: r.bankWithdrawStatusUrl,
@@ -2835,7 +3085,7 @@ export class Wallet {
         history.push({
           detail: {
             exchangeBaseUrl: r.exchangeBaseUrl,
-            requestedAmount: Amounts.toString(r.requestedAmount),
+            requestedAmount: Amounts.toString(r.initiallyRequestedAmount),
             reservePub: r.reservePub,
             reserveType,
             bankWithdrawStatusUrl: r.bankWithdrawStatusUrl,
@@ -2956,11 +3206,23 @@ export class Wallet {
         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:
@@ -2992,16 +3254,55 @@ export class Wallet {
         oldCoinPub: r.meltCoinPub,
         refreshStatus,
         refreshOutputSize: r.newDenoms.length,
+        refreshSessionId: r.refreshSessionId,
       });
     });
 
-    await oneShotIter(this.db, Stores.precoins).forEach(pc => {
+    await oneShotIter(this.db, Stores.planchets).forEach(pc => {
       pendingOperations.push({
-        type: "withdraw",
-        stage: "planchet",
+        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,
     };
@@ -3016,9 +3317,7 @@ export class Wallet {
     return denoms;
   }
 
-  async getProposal(
-    proposalId: number,
-  ): Promise<ProposalDownloadRecord | undefined> {
+  async getProposal(proposalId: string): Promise<ProposalRecord | undefined> {
     const proposal = await oneShotGet(this.db, Stores.proposals, proposalId);
     return proposal;
   }
@@ -3053,8 +3352,8 @@ export class Wallet {
     return await oneShotIter(this.db, Stores.coins).toArray();
   }
 
-  async getPreCoins(exchangeBaseUrl: string): Promise<PreCoinRecord[]> {
-    return await oneShotIter(this.db, Stores.precoins).filter(
+  async getPlanchets(exchangeBaseUrl: string): Promise<PlanchetRecord[]> {
+    return await oneShotIter(this.db, Stores.planchets).filter(
       c => c.exchangeBaseUrl === exchangeBaseUrl,
     );
   }
@@ -3130,9 +3429,13 @@ export class Wallet {
       feeWithdraw: Amounts.parseOrThrow(denomIn.fee_withdraw),
       isOffered: true,
       masterSig: denomIn.master_sig,
-      stampExpireDeposit: 
extractTalerStampOrThrow(denomIn.stamp_expire_deposit),
+      stampExpireDeposit: extractTalerStampOrThrow(
+        denomIn.stamp_expire_deposit,
+      ),
       stampExpireLegal: extractTalerStampOrThrow(denomIn.stamp_expire_legal),
-      stampExpireWithdraw: 
extractTalerStampOrThrow(denomIn.stamp_expire_withdraw),
+      stampExpireWithdraw: extractTalerStampOrThrow(
+        denomIn.stamp_expire_withdraw,
+      ),
       stampStart: extractTalerStampOrThrow(denomIn.stamp_start),
       status: DenominationStatus.Unverified,
       value: Amounts.parseOrThrow(denomIn.value),
@@ -3570,9 +3873,7 @@ export class Wallet {
     return feeAcc;
   }
 
-async acceptTip(
-    talerTipUri: string,
-  ): Promise<void> {
+  async acceptTip(talerTipUri: string): Promise<void> {
     const { tipId, merchantOrigin } = await this.getTipStatus(talerTipUri);
     let tipRecord = await oneShotGet(this.db, Stores.tips, [
       tipId,
@@ -3647,22 +3948,24 @@ async acceptTip(
     }
 
     for (let i = 0; i < tipRecord.planchets.length; i++) {
-      const planchet = tipRecord.planchets[i];
-      const preCoin = {
-        blindingKey: planchet.blindingKey,
-        coinEv: planchet.coinEv,
-        coinPriv: planchet.coinPriv,
-        coinPub: planchet.coinPub,
-        coinValue: planchet.coinValue,
-        denomPub: planchet.denomPub,
-        denomPubHash: planchet.denomPubHash,
+      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.precoins, preCoin);
-      await this.processPreCoin(preCoin.coinPub);
+      await oneShotPut(this.db, Stores.planchets, planchet);
+      await this.processPlanchet(planchet.coinPub);
     }
 
     tipRecord.pickedUp = true;
@@ -3794,6 +4097,19 @@ async acceptTip(
     });
   }
 
+  public async handleNotifyReserve() {
+    const reserves = await oneShotIter(this.db, Stores.reserves).toArray();
+    for (const r of reserves) {
+      if (r.reserveStatus === ReserveRecordStatus.WAIT_CONFIRM_BANK) {
+        try {
+          this.processReserveBankStatus(r.reservePub);
+        } catch (e) {
+          console.error(e);
+        }
+      }
+    }
+  }
+
   /**
    * Remove unreferenced / expired data from the wallet's database
    * based on the current system time.
@@ -3805,6 +4121,10 @@ async acceptTip(
     // strategy to test it.
   }
 
+  /**
+   * Get information about a withdrawal from
+   * a taler://withdraw URI.
+   */
   async getWithdrawalInfo(
     talerWithdrawUri: string,
   ): Promise<DownloadedWithdrawInfo> {
@@ -3843,6 +4163,10 @@ async acceptTip(
       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,
@@ -3883,13 +4207,6 @@ async acceptTip(
     };
   }
 
-  /**
-   * Reset the retry timeouts for ongoing operations.
-   */
-  resetRetryTimeouts(): void {
-    // FIXME: implement
-  }
-
   clearNotification(): void {
     this.badge.clearNotification();
   }
diff --git a/src/walletTypes.ts b/src/walletTypes.ts
index b971e300..45560694 100644
--- a/src/walletTypes.ts
+++ b/src/walletTypes.ts
@@ -465,14 +465,14 @@ export type PreparePayResult =
 
 export interface PreparePayResultPaymentPossible {
   status: "payment-possible";
-  proposalId: number;
+  proposalId: string;
   contractTerms: ContractTerms;
   totalFees: AmountJson;
 }
 
 export interface PreparePayResultInsufficientBalance {
   status: "insufficient-balance";
-  proposalId: number;
+  proposalId: string;
   contractTerms: ContractTerms;
 }
 
@@ -523,8 +523,10 @@ export interface WalletDiagnostics {
 
 export interface PendingWithdrawOperation {
   type: "withdraw";
-  stage: string;
   reservePub: string;
+  withdrawSessionId: string;
+  numCoinsWithdrawn: number;
+  numCoinsTotal: number;
 }
 
 export interface PendingRefreshOperation {
@@ -561,22 +563,47 @@ export interface PendingReserveOperation {
   stage: string;
   timestampCreated: Timestamp;
   reserveType: string;
+  reservePub: string;
+  bankWithdrawConfirmUrl?: string;
 }
 
 export interface PendingRefreshOperation {
   type: "refresh";
   lastError?: OperationError;
+  refreshSessionId: string;
   oldCoinPub: string;
   refreshStatus: string;
   refreshOutputSize: number;
 }
 
+export interface PendingPlanchetOperation {
+  type: "planchet";
+  coinPub: string;
+  reservePub: string;
+  lastError?: OperationError;
+}
+
+export interface PendingDirtyCoinOperation {
+  type: "dirty-coin";
+  coinPub: string;
+}
+
+export interface PendingProposalOperation {
+  type: "proposal";
+  merchantBaseUrl: string;
+  proposalTimestamp: Timestamp;
+  proposalId: string;
+}
+
 export type PendingOperationInfo =
   | PendingWithdrawOperation
   | PendingReserveOperation
   | PendingBugOperation
+  | PendingPlanchetOperation
+  | PendingDirtyCoinOperation
   | PendingExchangeUpdateOperation
-  | PendingRefreshOperation;
+  | PendingRefreshOperation
+  | PendingProposalOperation;
 
 export interface PendingOperationsResponse {
   pendingOperations: PendingOperationInfo[];
@@ -614,3 +641,17 @@ export function getTimestampNow(): Timestamp {
     t_ms: new Date().getTime(),
   };
 }
+
+
+export interface PlanchetCreationResult {
+  coinPub: string;
+  coinPriv: string;
+  reservePub: string;
+  denomPubHash: string;
+  denomPub: string;
+  blindingKey: string;
+  withdrawSig: string;
+  coinEv: string;
+  exchangeBaseUrl: string;
+  coinValue: AmountJson;
+}
\ No newline at end of file
diff --git a/src/webex/messages.ts b/src/webex/messages.ts
index 034bf284..e321e5ac 100644
--- a/src/webex/messages.ts
+++ b/src/webex/messages.ts
@@ -66,7 +66,7 @@ export interface MessageMap {
     response: void;
   };
   "confirm-pay": {
-    request: { proposalId: number; sessionId?: string };
+    request: { proposalId: string; sessionId?: string };
     response: walletTypes.ConfirmPayResult;
   };
   "exchange-info": {
@@ -113,9 +113,9 @@ export interface MessageMap {
     request: { reservePub: string };
     response: dbTypes.ReserveRecord[];
   };
-  "get-precoins": {
+  "get-planchets": {
     request: { exchangeBaseUrl: string };
-    response: dbTypes.PreCoinRecord[];
+    response: dbTypes.PlanchetRecord[];
   };
   "get-denoms": {
     request: { exchangeBaseUrl: string };
diff --git a/src/webex/pages/payback.tsx b/src/webex/pages/payback.tsx
index af14b95d..806bef17 100644
--- a/src/webex/pages/payback.tsx
+++ b/src/webex/pages/payback.tsx
@@ -57,7 +57,7 @@ function Payback() {
     <div>
       {reserves.map(r => (
         <div>
-          <h2>Reserve for ${renderAmount(r.currentAmount!)}</h2>
+          <h2>Reserve for ${renderAmount(r.withdrawRemainingAmount)}</h2>
           <ul>
             <li>Exchange: ${r.exchangeBaseUrl}</li>
           </ul>
diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts
index a5067213..a8b35ed3 100644
--- a/src/webex/wxApi.ts
+++ b/src/webex/wxApi.ts
@@ -28,7 +28,7 @@ import {
   CurrencyRecord,
   DenominationRecord,
   ExchangeRecord,
-  PreCoinRecord,
+  PlanchetRecord,
   ReserveRecord,
 } from "../dbTypes";
 import {
@@ -174,10 +174,10 @@ export function getCoins(exchangeBaseUrl: string): 
Promise<CoinRecord[]> {
 
 
 /**
- * Get all precoins withdrawn from the given exchange.
+ * Get all planchets withdrawn from the given exchange.
  */
-export function getPreCoins(exchangeBaseUrl: string): Promise<PreCoinRecord[]> 
{
-  return callBackend("get-precoins", { exchangeBaseUrl });
+export function getPlanchets(exchangeBaseUrl: string): 
Promise<PlanchetRecord[]> {
+  return callBackend("get-planchets", { exchangeBaseUrl });
 }
 
 
@@ -207,7 +207,7 @@ export function payback(coinPub: string): Promise<void> {
 /**
  * Pay for a proposal.
  */
-export function confirmPay(proposalId: number, sessionId: string | undefined): 
Promise<ConfirmPayResult> {
+export function confirmPay(proposalId: string, sessionId: string | undefined): 
Promise<ConfirmPayResult> {
   return callBackend("confirm-pay", { proposalId, sessionId });
 }
 
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index 57c10d94..78c86a97 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -117,8 +117,8 @@ async function handleMessage(
       return needsWallet().confirmReserve(req);
     }
     case "confirm-pay": {
-      if (typeof detail.proposalId !== "number") {
-        throw Error("proposalId must be number");
+      if (typeof detail.proposalId !== "string") {
+        throw Error("proposalId must be string");
       }
       return needsWallet().confirmPay(detail.proposalId, detail.sessionId);
     }
@@ -178,11 +178,11 @@ async function handleMessage(
       }
       return needsWallet().getCoinsForExchange(detail.exchangeBaseUrl);
     }
-    case "get-precoins": {
+    case "get-planchets": {
       if (typeof detail.exchangeBaseUrl !== "string") {
         return Promise.reject(Error("exchangBaseUrl missing"));
       }
-      return needsWallet().getPreCoins(detail.exchangeBaseUrl);
+      return needsWallet().getPlanchets(detail.exchangeBaseUrl);
     }
     case "get-denoms": {
       if (typeof detail.exchangeBaseUrl !== "string") {
@@ -658,8 +658,8 @@ export async function wxMain() {
       if (!wallet) {
         console.warn("wallet not available while handling header");
       }
-      if (details.statusCode === 402) {
-        console.log(`got 402 from ${details.url}`);
+      if (details.statusCode === 402 || details.statusCode === 202) {
+        console.log(`got 402/202 from ${details.url}`);
         for (let header of details.responseHeaders || []) {
           if (header.name.toLowerCase() === "taler") {
             const talerUri = header.value || "";
@@ -705,6 +705,15 @@ export async function wxMain() {
                   talerRefundUri: talerUri,
                 },
               );
+            } else if (talerUri.startsWith("taler://notify-reserve/")) {
+              Promise.resolve().then(() => {
+                const w = currentWallet;
+                if (!w) {
+                  return;
+                }
+                w.handleNotifyReserve();
+              });
+
             } else {
               console.warn("Unknown action in taler:// URI, ignoring.");
             }
diff --git a/yarn.lock b/yarn.lock
index 56697f55..22bcd5fd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3417,10 +3417,10 @@ iconv-lite@0.4.24, iconv-lite@^0.4.4, 
iconv-lite@~0.4.13:
   dependencies:
     safer-buffer ">= 2.1.2 < 3"
 
-idb-bridge@^0.0.14:
-  version "0.0.14"
-  resolved 
"https://registry.yarnpkg.com/idb-bridge/-/idb-bridge-0.0.14.tgz#5fd50cd68b574df0eb6b1a960cef0cb984a21ded";
-  integrity 
sha512-jc9ZYGhhIrW6nh/pWyycGWzCmsLTFQ0iMY61lN+y9YcIOCxREpAkZxdfmhwNL7H0RvsYp7iJv0GH7ujs7HPC+g==
+idb-bridge@^0.0.15:
+  version "0.0.15"
+  resolved 
"https://registry.yarnpkg.com/idb-bridge/-/idb-bridge-0.0.15.tgz#3fddc91b9aab775fae273d02b272205c6090d270";
+  integrity 
sha512-xuZM/i4vCm/NkqyrKNJDEuBaZK7M2kyj+1F4hDGqtEJZSmQMSV3v9A6Ie3fR12VXDKIbMr7uV22eWjIKwSosOA==
 
 ieee754@^1.1.4:
   version "1.1.13"

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



reply via email to

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