gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (b41ae3e9 -> aaf7e133)


From: gnunet
Subject: [taler-wallet-core] branch master updated (b41ae3e9 -> aaf7e133)
Date: Sat, 30 Nov 2019 00:36:29 +0100

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

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

    from b41ae3e9 idb-bridge: enforce store restrictions
     new 809fa186 idb-bridge: bump version
     new aaf7e133 wallet robustness WIP

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 gulpfile.js                        |   4 +-
 package.json                       |   2 +-
 packages/idb-bridge/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 +-
 21 files changed, 1074 insertions(+), 430 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/packages/idb-bridge/package.json b/packages/idb-bridge/package.json
index 6df8ab34..b0aa74a7 100644
--- a/packages/idb-bridge/package.json
+++ b/packages/idb-bridge/package.json
@@ -1,6 +1,6 @@
 {
   "name": "idb-bridge",
-  "version": "0.0.14",
+  "version": "0.0.15",
   "description": "IndexedDB implementation that uses SQLite3 as storage",
   "main": "./build/index.js",
   "types": "./build/index.d.ts",
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]