gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 05/05: WIP: simplification and error handling


From: gnunet
Subject: [taler-wallet-core] 05/05: WIP: simplification and error handling
Date: Thu, 21 Nov 2019 23:10:04 +0100

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

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

commit c6233094306cd264f8faa2041388dff01ff8cf01
Author: Florian Dold <address@hidden>
AuthorDate: Thu Nov 21 23:09:43 2019 +0100

    WIP: simplification and error handling
---
 package.json                       |    2 +-
 src/crypto/cryptoApi-test.ts       |   23 +-
 src/crypto/cryptoImplementation.ts |   16 +-
 src/db.ts                          |    1 -
 src/dbTypes.ts                     |  171 ++---
 src/headless/clk.ts                |    2 +-
 src/headless/helpers.ts            |   16 +-
 src/headless/integrationtest.ts    |    1 +
 src/headless/taler-wallet-cli.ts   |  380 +++++------
 src/http.ts                        |    7 -
 src/query.ts                       |   78 ++-
 src/wallet.ts                      | 1243 ++++++++++++++++++++----------------
 src/walletTypes.ts                 |   34 +-
 src/webex/messages.ts              |    2 +-
 src/webex/pages/payback.tsx        |    6 +-
 src/webex/pages/popup.tsx          |    8 +-
 src/webex/renderHtml.tsx           |    2 +-
 src/webex/wxBackend.ts             |    2 +-
 tsconfig.json                      |    3 +
 yarn.lock                          |    8 +-
 20 files changed, 1102 insertions(+), 903 deletions(-)

diff --git a/package.json b/package.json
index 980274e0..fa8cd1d0 100644
--- a/package.json
+++ b/package.json
@@ -63,7 +63,7 @@
     "@types/chrome": "^0.0.91",
     "@types/urijs": "^1.19.3",
     "axios": "^0.19.0",
-    "idb-bridge": "^0.0.11",
+    "idb-bridge": "^0.0.14",
     "qrcode-generator": "^1.4.3",
     "source-map-support": "^0.5.12",
     "urijs": "^1.18.10"
diff --git a/src/crypto/cryptoApi-test.ts b/src/crypto/cryptoApi-test.ts
index 39f46c5c..d9d42081 100644
--- a/src/crypto/cryptoApi-test.ts
+++ b/src/crypto/cryptoApi-test.ts
@@ -22,6 +22,7 @@ import {
   DenominationRecord,
   DenominationStatus,
   ReserveRecord,
+  ReserveRecordStatus,
 } from "../dbTypes";
 
 import { CryptoApi } from "./cryptoApi";
@@ -86,18 +87,18 @@ test("precoin creation", async t => {
   const crypto = new CryptoApi(new NodeCryptoWorkerFactory());
   const { priv, pub } = await crypto.createEddsaKeypair();
   const r: ReserveRecord = {
-    created: 0,
-    current_amount: null,
-    exchange_base_url: "https://example.com/exchange";,
+    created: { t_ms: 0 },
+    currentAmount: null,
+    exchangeBaseUrl: "https://example.com/exchange";,
     hasPayback: false,
-    precoin_amount: { currency: "PUDOS", value: 0, fraction: 0 },
-    requested_amount: { currency: "PUDOS", value: 0, fraction: 0 },
-    reserve_priv: priv,
-    reserve_pub: pub,
-    timestamp_confirmed: 0,
-    timestamp_depleted: 0,
-    timestamp_reserve_info_posted: 0,
-    exchangeWire: "payto://foo"
+    precoinAmount: { currency: "PUDOS", value: 0, fraction: 0 },
+    requestedAmount: { currency: "PUDOS", value: 0, fraction: 0 },
+    reservePriv: priv,
+    reservePub: pub,
+    timestampConfirmed: undefined,
+    timestampReserveInfoPosted: undefined,
+    exchangeWire: "payto://foo",
+    reserveStatus: ReserveRecordStatus.UNCONFIRMED,
   };
 
   const precoin = await crypto.createPreCoin(denomValid1, r);
diff --git a/src/crypto/cryptoImplementation.ts 
b/src/crypto/cryptoImplementation.ts
index d50d4002..7dd019c1 100644
--- a/src/crypto/cryptoImplementation.ts
+++ b/src/crypto/cryptoImplementation.ts
@@ -45,6 +45,7 @@ import * as native from "./emscInterface";
 import { AmountJson } from "../amounts";
 import * as Amounts from "../amounts";
 import * as timer from "../timer";
+import { getRandomBytes, encodeCrock } from "./nativeCrypto";
 
 export class CryptoImplementation {
   static enableTracing: boolean = false;
@@ -60,9 +61,9 @@ export class CryptoImplementation {
     reserve: ReserveRecord,
   ): PreCoinRecord {
     const reservePriv = new native.EddsaPrivateKey(this.emsc);
-    reservePriv.loadCrock(reserve.reserve_priv);
+    reservePriv.loadCrock(reserve.reservePriv);
     const reservePub = new native.EddsaPublicKey(this.emsc);
-    reservePub.loadCrock(reserve.reserve_pub);
+    reservePub.loadCrock(reserve.reservePub);
     const denomPub = native.RsaPublicKey.fromCrock(this.emsc, denom.denomPub);
     const coinPriv = native.EddsaPrivateKey.create(this.emsc);
     const coinPub = coinPriv.getPublicKey();
@@ -103,7 +104,7 @@ export class CryptoImplementation {
       coinValue: denom.value,
       denomPub: denomPub.toCrock(),
       denomPubHash: denomPubHash.toCrock(),
-      exchangeBaseUrl: reserve.exchange_base_url,
+      exchangeBaseUrl: reserve.exchangeBaseUrl,
       isFromTip: false,
       reservePub: reservePub.toCrock(),
       withdrawSig: sig.toCrock(),
@@ -199,14 +200,14 @@ export class CryptoImplementation {
   isValidWireFee(type: string, wf: WireFee, masterPub: string): boolean {
     const p = new native.MasterWireFeePS(this.emsc, {
       closing_fee: new native.Amount(this.emsc, wf.closingFee).toNbo(),
-      end_date: native.AbsoluteTimeNbo.fromStampSeconds(this.emsc, 
wf.endStamp),
+      end_date: native.AbsoluteTimeNbo.fromStampSeconds(this.emsc, 
(wf.endStamp.t_ms / 1000)),
       h_wire_method: native.ByteArray.fromStringWithNull(
         this.emsc,
         type,
       ).hash(),
       start_date: native.AbsoluteTimeNbo.fromStampSeconds(
         this.emsc,
-        wf.startStamp,
+        Math.floor(wf.startStamp.t_ms / 1000),
       ),
       wire_fee: new native.Amount(this.emsc, wf.wireFee).toNbo(),
     });
@@ -354,7 +355,7 @@ export class CryptoImplementation {
       const newAmount = new native.Amount(this.emsc, cd.coin.currentAmount);
       newAmount.sub(coinSpend);
       cd.coin.currentAmount = newAmount.toJson();
-      cd.coin.status = CoinStatus.PurchasePending;
+      cd.coin.status = CoinStatus.Dirty;
 
       const d = new native.DepositRequestPS(this.emsc, {
         amount_with_fee: coinSpend.toNbo(),
@@ -505,7 +506,10 @@ export class CryptoImplementation {
       valueOutput = Amounts.add(valueOutput, denom.value).amount;
     }
 
+    const refreshSessionId = encodeCrock(getRandomBytes(32));
+
     const refreshSession: RefreshSessionRecord = {
+      refreshSessionId,
       confirmSig,
       exchangeBaseUrl,
       finished: false,
diff --git a/src/db.ts b/src/db.ts
index 00eac432..e317b0aa 100644
--- a/src/db.ts
+++ b/src/db.ts
@@ -12,7 +12,6 @@ export function openTalerDb(
   onVersionChange: () => void,
   onUpgradeUnsupported: (oldVersion: number, newVersion: number) => void,
 ): Promise<IDBDatabase> {
-  console.log("in openTalerDb");
   return new Promise<IDBDatabase>((resolve, reject) => {
     const req = idbFactory.open(DB_NAME, WALLET_DB_VERSION);
     req.onerror = e => {
diff --git a/src/dbTypes.ts b/src/dbTypes.ts
index 0d54069e..22d98ffa 100644
--- a/src/dbTypes.ts
+++ b/src/dbTypes.ts
@@ -46,6 +46,36 @@ import { Timestamp, OperationError } from "./walletTypes";
  */
 export const WALLET_DB_VERSION = 27;
 
+export enum ReserveRecordStatus {
+  /**
+   * Waiting for manual confirmation.
+   */
+  UNCONFIRMED = "unconfirmed",
+
+  /**
+   * Reserve must be registered with the bank.
+   */
+  REGISTERING_BANK = "registering-bank",
+
+  /**
+   * Querying reserve status with the exchange.
+   */
+  QUERYING_STATUS = "querying-status",
+
+  /**
+   * Status is queried, the wallet must now select coins
+   * and start withdrawing.
+   */
+  WITHDRAWING = "withdrawing",
+
+  /**
+   * The corresponding withdraw record has been created.
+   * No further processing is done, unless explicitly requested
+   * by the user.
+   */
+  DORMANT = "dormant",
+}
+
 /**
  * A reserve record as stored in the wallet's database.
  */
@@ -53,28 +83,22 @@ export interface ReserveRecord {
   /**
    * The reserve public key.
    */
-  reserve_pub: string;
+  reservePub: string;
 
   /**
    * The reserve private key.
    */
-  reserve_priv: string;
+  reservePriv: string;
 
   /**
    * The exchange base URL.
    */
-  exchange_base_url: string;
+  exchangeBaseUrl: string;
 
   /**
    * Time when the reserve was created.
    */
-  created: number;
-
-  /**
-   * Time when the reserve was depleted.
-   * Set to 0 if not depleted yet.
-   */
-  timestamp_depleted: number;
+  created: Timestamp;
 
   /**
    * Time when the information about this reserve was posted to the bank.
@@ -83,32 +107,32 @@ export interface ReserveRecord {
    *
    * Set to 0 if that hasn't happened yet.
    */
-  timestamp_reserve_info_posted: number;
+  timestampReserveInfoPosted: Timestamp | undefined;
 
   /**
    * Time when the reserve was confirmed.
    *
    * Set to 0 if not confirmed yet.
    */
-  timestamp_confirmed: number;
+  timestampConfirmed: Timestamp | undefined;
 
   /**
    * Current amount left in the reserve
    */
-  current_amount: AmountJson | null;
+  currentAmount: AmountJson | null;
 
   /**
    * Amount requested when the reserve was created.
    * When a reserve is re-used (rare!)  the current_amount can
    * be higher than the requested_amount
    */
-  requested_amount: AmountJson;
+  requestedAmount: AmountJson;
 
   /**
    * What's the current amount that sits
    * in precoins?
    */
-  precoin_amount: AmountJson;
+  precoinAmount: AmountJson;
 
   /**
    * We got some payback to this reserve.  We'll cease to automatically
@@ -129,6 +153,10 @@ export interface ReserveRecord {
   exchangeWire: string;
 
   bankWithdrawStatusUrl?: string;
+
+  reserveStatus: ReserveRecordStatus;
+
+  lastError?: OperationError;
 }
 
 /**
@@ -341,9 +369,9 @@ export interface ExchangeDetails {
 }
 
 export enum ExchangeUpdateStatus {
-  NONE = "none",
   FETCH_KEYS = "fetch_keys",
   FETCH_WIRE = "fetch_wire",
+  FINISHED = "finished",
 }
 
 export interface ExchangeBankAccount {
@@ -374,13 +402,18 @@ export interface ExchangeRecord {
    */
   wireInfo: ExchangeWireInfo | undefined;
 
+  /**
+   * When was the exchange added to the wallet?
+   */
+  timestampAdded: Timestamp;
+
   /**
    * Time when the update to the exchange has been started or
    * undefined if no update is in progress.
    */
   updateStarted: Timestamp | undefined;
-
   updateStatus: ExchangeUpdateStatus;
+  updateReason?: "initial" | "forced";
 
   lastError?: OperationError;
 }
@@ -436,31 +469,15 @@ export enum CoinStatus {
   /**
    * Withdrawn and never shown to anybody.
    */
-  Fresh,
-  /**
-   * Currently planned to be sent to a merchant for a purchase.
-   */
-  PurchasePending,
+  Fresh = "fresh",
   /**
    * Used for a completed transaction and now dirty.
    */
-  Dirty,
+  Dirty = "dirty",
   /**
-   * A coin that was refreshed.
+   * A coin that has been spent and refreshed.
    */
-  Refreshed,
-  /**
-   * Coin marked to be paid back, but payback not finished.
-   */
-  PaybackPending,
-  /**
-   * Coin fully paid back.
-   */
-  PaybackDone,
-  /**
-   * Coin was dirty but can't be refreshed.
-   */
-  Useless,
+  Dormant = "dormant",
 }
 
 /**
@@ -569,7 +586,7 @@ export class ProposalDownloadRecord {
    * was created.
    */
   @Checkable.Number()
-  timestamp: number;
+  timestamp: Timestamp;
 
   /**
    * Private key for the nonce.
@@ -658,7 +675,7 @@ export interface TipRecord {
    */
   nextUrl?: string;
 
-  timestamp: number;
+  timestamp: Timestamp;
 
   pickupUrl: string;
 }
@@ -735,9 +752,9 @@ export interface RefreshSessionRecord {
   finished: boolean;
 
   /**
-   * Record ID when retrieved from the DB.
+   * A 32-byte base32-crockford encoded random identifier.
    */
-  id?: number;
+  refreshSessionId: string;
 }
 
 /**
@@ -771,12 +788,12 @@ export interface WireFee {
   /**
    * Start date of the fee.
    */
-  startStamp: number;
+  startStamp: Timestamp;
 
   /**
    * End date of the fee.
    */
-  endStamp: number;
+  endStamp: Timestamp;
 
   /**
    * Signature made by the exchange master key.
@@ -830,14 +847,13 @@ export interface PurchaseRecord {
    * When was the purchase made?
    * Refers to the time that the user accepted.
    */
-  timestamp: number;
+  timestamp: Timestamp;
 
   /**
    * When was the last refund made?
    * Set to 0 if no refund was made on the purchase.
    */
-  timestamp_refund: number;
-
+  timestamp_refund: Timestamp | undefined;
 
   /**
    * Last session signature that we submitted to /pay (if any).
@@ -917,7 +933,6 @@ export interface CoinsReturnRecord {
   wire: any;
 }
 
-
 export interface WithdrawalRecord {
   /**
    * Reserve that we're withdrawing from.
@@ -928,18 +943,22 @@ export interface WithdrawalRecord {
    * When was the withdrawal operation started started?
    * Timestamp in milliseconds.
    */
-  startTimestamp: number;
+  startTimestamp: Timestamp;
 
   /**
    * When was the withdrawal operation completed?
    */
-  finishTimestamp?: number;
+  finishTimestamp?: Timestamp;
 
   /**
    * Amount that is being withdrawn with this operation.
    * This does not include fees.
    */
   withdrawalAmount: string;
+
+  numCoinsTotal: number;
+
+  numCoinsWithdrawn: number;
 }
 
 /* tslint:disable:completed-docs */
@@ -983,11 +1002,6 @@ export namespace Stores {
       "urlIndex",
       "url",
     );
-    timestampIndex = new Index<string, ProposalDownloadRecord>(
-      this,
-      "timestampIndex",
-      "timestamp",
-    );
   }
 
   class PurchasesStore extends Store<PurchaseRecord> {
@@ -1005,11 +1019,6 @@ export namespace Stores {
       "orderIdIndex",
       "contractTerms.order_id",
     );
-    timestampIndex = new Index<string, PurchaseRecord>(
-      this,
-      "timestampIndex",
-      "timestamp",
-    );
   }
 
   class DenominationsStore extends Store<DenominationRecord> {
@@ -1051,23 +1060,8 @@ export namespace Stores {
 
   class ReservesStore extends Store<ReserveRecord> {
     constructor() {
-      super("reserves", { keyPath: "reserve_pub" });
+      super("reserves", { keyPath: "reservePub" });
     }
-    timestampCreatedIndex = new Index<string, ReserveRecord>(
-      this,
-      "timestampCreatedIndex",
-      "created",
-    );
-    timestampConfirmedIndex = new Index<string, ReserveRecord>(
-      this,
-      "timestampConfirmedIndex",
-      "timestamp_confirmed",
-    );
-    timestampDepletedIndex = new Index<string, ReserveRecord>(
-      this,
-      "timestampDepletedIndex",
-      "timestamp_depleted",
-    );
   }
 
   class TipsStore extends Store<TipRecord> {
@@ -1092,8 +1086,26 @@ export namespace Stores {
 
   class WithdrawalsStore extends Store<WithdrawalRecord> {
     constructor() {
-      super("withdrawals", { keyPath: "id", autoIncrement: true })
+      super("withdrawals", { keyPath: "id", autoIncrement: true });
     }
+    byReservePub = new Index<string, WithdrawalRecord>(
+      this,
+      "withdrawalsReservePubIndex",
+      "reservePub",
+    );
+  }
+
+  class PreCoinsStore extends Store<PreCoinRecord> {
+    constructor() {
+      super("precoins", {
+        keyPath: "coinPub",
+      });
+    }
+    byReservePub = new Index<string, PreCoinRecord>(
+      this,
+      "precoinsReservePubIndex",
+      "reservePub",
+    );
   }
 
   export const coins = new CoinsStore();
@@ -1104,13 +1116,10 @@ export namespace Stores {
   export const currencies = new CurrenciesStore();
   export const denominations = new DenominationsStore();
   export const exchanges = new ExchangeStore();
-  export const precoins = new Store<PreCoinRecord>("precoins", {
-    keyPath: "coinPub",
-  });
+  export const precoins = new PreCoinsStore();
   export const proposals = new ProposalsStore();
   export const refresh = new Store<RefreshSessionRecord>("refresh", {
-    keyPath: "id",
-    autoIncrement: true,
+    keyPath: "refreshSessionId",
   });
   export const reserves = new ReservesStore();
   export const purchases = new PurchasesStore();
diff --git a/src/headless/clk.ts b/src/headless/clk.ts
index f66d609e..51ee119c 100644
--- a/src/headless/clk.ts
+++ b/src/headless/clk.ts
@@ -440,7 +440,7 @@ export class CommandGroup<GN extends keyof any, TG> {
       if (option.isFlag == false && option.required == true) {
         if (!foundOptions[option.name]) {
           if (option.args.default !== undefined) {
-            parsedArgs[this.argKey] = option.args.default;
+            myArgs[option.name] = option.args.default;
           } else {
             const name = option.flagspec.join(",")
             console.error(`error: missing option '${name}'`);
diff --git a/src/headless/helpers.ts b/src/headless/helpers.ts
index 49881d46..5e06a2f2 100644
--- a/src/headless/helpers.ts
+++ b/src/headless/helpers.ts
@@ -21,7 +21,7 @@
 /**
  * Imports.
  */
-import { Wallet } from "../wallet";
+import { Wallet, OperationFailedAndReportedError } from "../wallet";
 import { Notifier, Badge } from "../walletTypes";
 import { MemoryBackend, BridgeIDBFactory, shimIndexedDB } from "idb-bridge";
 import { SynchronousCryptoWorkerFactory } from "../crypto/synchronousWorker";
@@ -139,18 +139,16 @@ export async function getDefaultNodeWallet(
 
   const storagePath = args.persistentStoragePath;
   if (storagePath) {
-    console.log(`using storage path ${storagePath}`);
-
     try {
       const dbContentStr: string = fs.readFileSync(storagePath, { encoding: 
"utf-8" });
       const dbContent = JSON.parse(dbContentStr);
       myBackend.importDump(dbContent);
-      console.log("imported wallet");
     } catch (e) {
-      console.log("could not read wallet file");
+      console.error("could not read wallet file");
     }
 
     myBackend.afterCommitCallback = async () => {
+      console.log("DATABASE COMMITTED");
       // Allow caller to stop persisting the wallet.
       if (args.persistentStoragePath === undefined) {
         return;
@@ -190,8 +188,6 @@ export async function getDefaultNodeWallet(
     myUnsupportedUpgrade,
   );
 
-  console.log("opened db");
-
   return new Wallet(
     myDb,
     myHttpLib,
@@ -214,6 +210,8 @@ export async function withdrawTestBalance(
     exchangeWire: "payto://unknown",
   });
 
+  const reservePub = reserveResponse.reservePub;
+
   const bank = new Bank(bankBaseUrl);
 
   const bankUser = await bank.registerRandomUser();
@@ -228,11 +226,11 @@ export async function withdrawTestBalance(
   await bank.createReserve(
     bankUser,
     amount,
-    reserveResponse.reservePub,
+    reservePub,
     exchangePaytoUri,
   );
 
   await myWallet.confirmReserve({ reservePub: reserveResponse.reservePub });
 
-  await myWallet.processReserve(reserveResponse.reservePub);
+  await myWallet.runUntilReserveDepleted(reservePub);
 }
diff --git a/src/headless/integrationtest.ts b/src/headless/integrationtest.ts
index 6b328690..6f2139c9 100644
--- a/src/headless/integrationtest.ts
+++ b/src/headless/integrationtest.ts
@@ -31,6 +31,7 @@ export async function runIntegrationTest(args: {
   amountToWithdraw: string;
   amountToSpend: string;
 }) {
+  console.log("running test with", args);
   const myWallet = await getDefaultNodeWallet();
 
   await withdrawTestBalance(myWallet, args.amountToWithdraw, args.bankBaseUrl, 
args.exchangeBaseUrl);
diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
index 06235d0b..0a678080 100644
--- a/src/headless/taler-wallet-cli.ts
+++ b/src/headless/taler-wallet-cli.ts
@@ -18,9 +18,14 @@ import os = require("os");
 import { getDefaultNodeWallet, withdrawTestBalance } from "./helpers";
 import { MerchantBackendConnection } from "./merchant";
 import { runIntegrationTest } from "./integrationtest";
-import { Wallet } from "../wallet";
+import { Wallet, OperationFailedAndReportedError } from "../wallet";
 import qrcodeGenerator = require("qrcode-generator");
 import * as clk from "./clk";
+import { BridgeIDBFactory, MemoryBackend } from "idb-bridge";
+import { Logger } from "../logging";
+import * as Amounts from "../amounts";
+
+const logger = new Logger("taler-wallet-cli.ts");
 
 const walletDbPath = os.homedir + "/" + ".talerwalletdb.json";
 
@@ -82,6 +87,7 @@ function applyVerbose(verbose: boolean) {
   if (verbose) {
     console.log("enabled verbose logging");
     Wallet.enableTracing = true;
+    BridgeIDBFactory.enableTracing = true;
   }
 }
 
@@ -103,62 +109,32 @@ async function withWallet<T>(
   walletCliArgs: WalletCliArgsType,
   f: (w: Wallet) => Promise<T>,
 ): Promise<T> {
-  applyVerbose(walletCliArgs.wallet.verbose);
   const wallet = await getDefaultNodeWallet({
     persistentStoragePath: walletDbPath,
   });
+  applyVerbose(walletCliArgs.wallet.verbose);
   try {
     await wallet.fillDefaults();
     const ret = await f(wallet);
     return ret;
   } catch (e) {
-    console.error("caught exception:", e);
+    if (e instanceof OperationFailedAndReportedError) {
+      console.error("Operation failed: " + e.message);
+      console.log("Hint: check pending operations for details.");
+    } else {
+      console.error("caught exception:", e);
+    }
     process.exit(1);
   } finally {
     wallet.stop();
   }
 }
 
-walletCli
-  .subcommand("testPayCmd", "test-pay", { help: "create contract and pay" })
-  .requiredOption("amount", ["-a", "--amount"], clk.STRING)
-  .requiredOption("summary", ["-s", "--summary"], clk.STRING, {
-    default: "Test Payment",
-  })
-  .action(async args => {
-    const cmdArgs = args.testPayCmd;
-    console.log("creating order");
-    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;
-    }
-    console.log("taler pay URI:", talerPayUri);
-
-    const wallet = await getDefaultNodeWallet({
-      persistentStoragePath: walletDbPath,
-    });
-
-    await doPay(wallet, talerPayUri, { alwaysYes: true });
-  });
-
 walletCli
   .subcommand("", "balance", { help: "Show wallet balance." })
   .action(async args => {
     console.log("balance command called");
-    withWallet(args, async (wallet) => {
+    await withWallet(args, async wallet => {
       const balance = await wallet.getBalances();
       console.log(JSON.stringify(balance, undefined, 2));
     });
@@ -166,12 +142,12 @@ walletCli
 
 walletCli
   .subcommand("", "history", { help: "Show wallet event history." })
-  .requiredOption("from", ["--from"], clk.STRING)
-  .requiredOption("to", ["--to"], clk.STRING)
-  .requiredOption("limit", ["--limit"], clk.STRING)
-  .requiredOption("contEvt", ["--continue-with"], clk.STRING)
+  .maybeOption("from", ["--from"], clk.STRING)
+  .maybeOption("to", ["--to"], clk.STRING)
+  .maybeOption("limit", ["--limit"], clk.STRING)
+  .maybeOption("contEvt", ["--continue-with"], clk.STRING)
   .action(async args => {
-    withWallet(args, async (wallet) => {
+    await withWallet(args, async wallet => {
       const history = await wallet.getHistory();
       console.log(JSON.stringify(history, undefined, 2));
     });
@@ -180,7 +156,7 @@ walletCli
 walletCli
   .subcommand("", "pending", { help: "Show pending operations." })
   .action(async args => {
-    withWallet(args, async (wallet) => {
+    await withWallet(args, async wallet => {
       const pending = await wallet.getPendingOperations();
       console.log(JSON.stringify(pending, undefined, 2));
     });
@@ -194,25 +170,129 @@ async function asyncSleep(milliSeconds: number): 
Promise<void> {
 
 walletCli
   .subcommand("runPendingOpt", "run-pending", {
-    help: "Run pending operations."
+    help: "Run pending operations.",
   })
-  .action(async (args) => {
-    withWallet(args, async (wallet) => {
-      await wallet.processPending();
+  .action(async args => {
+    await withWallet(args, async wallet => {
+      await wallet.runPending();
     });
   });
 
 walletCli
-  .subcommand("testMerchantQrcodeCmd", "test-merchant-qrcode")
-  .requiredOption("amount", ["-a", "--amount"], clk.STRING, {
-    default: "TESTKUDOS:1",
+  .subcommand("handleUri", "handle-uri", {
+    help: "Handle a taler:// URI.",
   })
+  .requiredArgument("uri", clk.STRING)
+  .flag("autoYes", ["-y", "--yes"])
+  .action(async args => {
+    await withWallet(args, async wallet => {
+      const uri: string = args.handleUri.uri;
+      if (uri.startsWith("taler://pay/")) {
+        await doPay(wallet, uri, { alwaysYes: args.handleUri.autoYes });
+      } else if (uri.startsWith("taler://tip/")) {
+        const res = await wallet.getTipStatus(uri);
+        console.log("tip status", res);
+        await wallet.acceptTip(uri);
+      } else if (uri.startsWith("taler://refund/")) {
+        await wallet.applyRefund(uri);
+      } else if (uri.startsWith("taler://withdraw/")) {
+        const withdrawInfo = await wallet.getWithdrawalInfo(uri);
+        const selectedExchange = withdrawInfo.suggestedExchange;
+        if (!selectedExchange) {
+          console.error("no suggested exchange!");
+          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 exchangesCli = walletCli.subcommand("exchangesCmd", "exchanges", {
+  help: "Manage exchanges.",
+});
+
+exchangesCli
+  .subcommand("exchangesListCmd", "list", {
+    help: "List known exchanges.",
+  })
+  .action(async args => {
+    console.log("Listing exchanges ...");
+    await withWallet(args, async wallet => {
+      const exchanges = await wallet.getExchanges();
+      console.log("exchanges", exchanges);
+    });
+  });
+
+exchangesCli
+  .subcommand("exchangesUpdateCmd", "update", {
+    help: "Update or add an exchange by base URL.",
+  })
+  .requiredArgument("url", clk.STRING, {
+    help: "Base URL of the exchange.",
+  })
+  .flag("force", ["-f", "--force"])
+  .action(async args => {
+    await withWallet(args, async wallet => {
+      const res = await wallet.updateExchangeFromUrl(
+        args.exchangesUpdateCmd.url,
+        args.exchangesUpdateCmd.force,
+      );
+    });
+  });
+
+const advancedCli = walletCli.subcommand("advancedArgs", "advanced", {
+  help:
+    "Subcommands for advanced operations (only use if you know what you're 
doing!).",
+});
+
+advancedCli
+  .subcommand("refresh", "force-refresh", {
+    help: "Force a refresh on a coin.",
+  })
+  .requiredArgument("coinPub", clk.STRING)
+  .action(async args => {
+    await withWallet(args, async wallet => {
+      await wallet.refresh(args.refresh.coinPub, true);
+    });
+  });
+
+advancedCli
+  .subcommand("coins", "list-coins", {
+    help: "List coins.",
+  })
+  .action(async args => {
+    await withWallet(args, async wallet => {
+      const coins = await wallet.getCoins();
+      for (const coin of coins) {
+        console.log(`coin ${coin.coinPub}`);
+        console.log(` status ${coin.status}`);
+        console.log(` exchange ${coin.exchangeBaseUrl}`);
+        console.log(` remaining amount 
${Amounts.toString(coin.currentAmount)}`);
+      }
+    });
+  });
+
+const testCli = walletCli.subcommand("testingArgs", "testing", {
+  help: "Subcommands for testing GNU Taler deployments.",
+});
+
+testCli
+  .subcommand("testPayCmd", "test-pay", { help: "create contract and pay" })
+  .requiredOption("amount", ["-a", "--amount"], clk.STRING)
   .requiredOption("summary", ["-s", "--summary"], clk.STRING, {
     default: "Test Payment",
   })
   .action(async args => {
-    const cmdArgs = args.testMerchantQrcodeCmd;
-    applyVerbose(args.wallet.verbose);
+    const cmdArgs = args.testPayCmd;
     console.log("creating order");
     const merchantBackend = new MerchantBackendConnection(
       "https://backend.test.taler.net/";,
@@ -225,7 +305,6 @@ walletCli
     );
     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");
@@ -233,23 +312,13 @@ walletCli
       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;
-      }
-    }
+    await withWallet(args, async (wallet) => {
+      await doPay(wallet, talerPayUri, { alwaysYes: true });
+    });
   });
 
-walletCli
+
+testCli
   .subcommand("integrationtestCmd", "integrationtest", {
     help: "Run integration test with bank, exchange and merchant.",
   })
@@ -265,13 +334,14 @@ walletCli
   .requiredOption("bank", ["-b", "--bank"], clk.STRING, {
     default: "https://bank.test.taler.net/";,
   })
-  .requiredOption("withdrawAmount", ["-b", "--bank"], clk.STRING, {
+  .requiredOption("withdrawAmount", ["-a", "--amount"], clk.STRING, {
     default: "TESTKUDOS:10",
   })
   .requiredOption("spendAmount", ["-s", "--spend-amount"], clk.STRING, {
     default: "TESTKUDOS:4",
   })
   .action(async args => {
+    console.log("parsed args", args);
     applyVerbose(args.wallet.verbose);
     let cmdObj = args.integrationtestCmd;
 
@@ -295,128 +365,61 @@ walletCli
     }
   });
 
-walletCli
-  .subcommand("withdrawUriCmd", "withdraw-uri")
-  .requiredArgument("withdrawUri", clk.STRING)
+testCli
+  .subcommand("testMerchantQrcodeCmd", "test-merchant-qrcode")
+  .requiredOption("amount", ["-a", "--amount"], clk.STRING, {
+    default: "TESTKUDOS:1",
+  })
+  .requiredOption("summary", ["-s", "--summary"], clk.STRING, {
+    default: "Test Payment",
+  })
   .action(async args => {
+    const cmdArgs = args.testMerchantQrcodeCmd;
     applyVerbose(args.wallet.verbose);
-    const cmdArgs = args.withdrawUriCmd;
-    const withdrawUrl = cmdArgs.withdrawUri;
-    console.log("withdrawing", withdrawUrl);
-    const wallet = await getDefaultNodeWallet({
-      persistentStoragePath: walletDbPath,
-    });
-
-    const withdrawInfo = await wallet.getWithdrawalInfo(withdrawUrl);
-
-    console.log("withdraw info", withdrawInfo);
-
-    const selectedExchange = withdrawInfo.suggestedExchange;
-    if (!selectedExchange) {
-      console.error("no suggested exchange!");
+    console.log("creating order");
+    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 qrcode = qrcodeGenerator(0, "M");
+    const talerPayUri = checkPayResp.taler_pay_uri;
+    if (!talerPayUri) {
+      console.error("fatal: no taler pay URI received from backend");
       process.exit(1);
       return;
     }
-
-    const { reservePub, confirmTransferUrl } = await wallet.acceptWithdrawal(
-      withdrawUrl,
-      selectedExchange,
-    );
-
-    if (confirmTransferUrl) {
-      console.log("please confirm the transfer at", confirmTransferUrl);
+    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;
+      }
     }
-
-    await wallet.processReserve(reservePub);
-
-    console.log("finished withdrawing");
-
-    wallet.stop();
-  });
-
-walletCli
-  .subcommand("tipUriCmd", "tip-uri")
-  .requiredArgument("uri", clk.STRING)
-  .action(async args => {
-    applyVerbose(args.wallet.verbose);
-    const tipUri = args.tipUriCmd.uri;
-    console.log("getting tip", tipUri);
-    const wallet = await getDefaultNodeWallet({
-      persistentStoragePath: walletDbPath,
-    });
-    const res = await wallet.getTipStatus(tipUri);
-    console.log("tip status", res);
-    await wallet.acceptTip(tipUri);
-    wallet.stop();
   });
 
-walletCli
-  .subcommand("refundUriCmd", "refund-uri")
-  .requiredArgument("uri", clk.STRING)
-  .action(async args => {
-    applyVerbose(args.wallet.verbose);
-    const refundUri = args.refundUriCmd.uri;
-    console.log("getting refund", refundUri);
-    const wallet = await getDefaultNodeWallet({
-      persistentStoragePath: walletDbPath,
-    });
-    await wallet.applyRefund(refundUri);
-    wallet.stop();
-  });
-
-const exchangesCli = walletCli.subcommand("exchangesCmd", "exchanges", {
-  help: "Manage exchanges.",
-});
-
-exchangesCli
-  .subcommand("exchangesListCmd", "list", {
-    help: "List known exchanges.",
-  })
-  .action(async args => {
-    console.log("Listing exchanges ...");
-    withWallet(args, async (wallet) => {
-      const exchanges = await wallet.getExchanges();
-      console.log("exchanges", exchanges);
-    });
-  });
-
-exchangesCli
-  .subcommand("exchangesUpdateCmd", "update", {
-    help: "Update or add an exchange by base URL.",
-  })
-  .requiredArgument("url", clk.STRING, {
-    help: "Base URL of the exchange.",
-  })
-  .action(async args => {
-    withWallet(args, async (wallet) => {
-      const res = await 
wallet.updateExchangeFromUrl(args.exchangesUpdateCmd.url);
-    });
-  });
-
-walletCli
-  .subcommand("payUriCmd", "pay-uri")
-  .requiredArgument("url", clk.STRING)
-  .flag("autoYes", ["-y", "--yes"])
-  .action(async args => {
-    applyVerbose(args.wallet.verbose);
-    const payUrl = args.payUriCmd.url;
-    console.log("paying for", payUrl);
-    const wallet = await getDefaultNodeWallet({
-      persistentStoragePath: walletDbPath,
-    });
-
-    await doPay(wallet, payUrl, { alwaysYes: args.payUriCmd.autoYes });
-    wallet.stop();
-  });
-
-const testCli = walletCli.subcommand("testingArgs", "testing", {
-  help: "Subcommands for testing GNU Taler deployments.",
-});
-
 testCli
   .subcommand("withdrawArgs", "withdraw", {
     help: "Withdraw from a test bank (must support test registrations).",
   })
+  .requiredOption("amount", ["-a", "--amount"], clk.STRING, {
+    default: "TESTKUDOS:10",
+    help: "Amount to withdraw.",
+  })
   .requiredOption("exchange", ["-e", "--exchange"], clk.STRING, {
     default: "https://exchange.test.taler.net/";,
     help: "Exchange base URL.",
@@ -426,14 +429,15 @@ testCli
     help: "Bank base URL",
   })
   .action(async args => {
-    applyVerbose(args.wallet.verbose);
-    console.log("balance command called");
-    const wallet = await getDefaultNodeWallet({
-      persistentStoragePath: walletDbPath,
+    await withWallet(args, async wallet => {
+      await withdrawTestBalance(
+        wallet,
+        args.withdrawArgs.amount,
+        args.withdrawArgs.bank,
+        args.withdrawArgs.exchange,
+      );
+      logger.info("Withdraw done");
     });
-    console.log("got wallet");
-    const balance = await wallet.getBalances();
-    console.log(JSON.stringify(balance, undefined, 2));
   });
 
 walletCli.run();
diff --git a/src/http.ts b/src/http.ts
index 8c1f772d..a2bfab27 100644
--- a/src/http.ts
+++ b/src/http.ts
@@ -107,10 +107,3 @@ export class BrowserHttpLib implements HttpRequestLibrary {
     return this.req("post", url, { req: form });
   }
 }
-
-/**
- * Exception thrown on request errors.
- */
-export class RequestException {
-  constructor(public detail: any) {}
-}
diff --git a/src/query.ts b/src/query.ts
index f510da55..5726bcaa 100644
--- a/src/query.ts
+++ b/src/query.ts
@@ -1,5 +1,3 @@
-import { openPromise } from "./promiseUtils";
-
 /*
  This file is part of TALER
  (C) 2016 GNUnet e.V.
@@ -22,6 +20,12 @@ import { openPromise } from "./promiseUtils";
  * @author Florian Dold
  */
 
+/**
+ * Imports.
+ */
+import { openPromise } from "./promiseUtils";
+
+
 /**
  * Result of an inner join.
  */
@@ -63,27 +67,48 @@ export interface IndexOptions {
 }
 
 function requestToPromise(req: IDBRequest): Promise<any> {
+  const stack = Error("Failed request was started here.")
   return new Promise((resolve, reject) => {
     req.onsuccess = () => {
       resolve(req.result);
     };
     req.onerror = () => {
+      console.log("error in DB request", req.error);
       reject(req.error);
+      console.log("Request failed:", stack);
     };
   });
 }
 
-export function oneShotGet<T>(
+function transactionToPromise(tx: IDBTransaction): Promise<void> {
+  const stack = Error("Failed transaction was started here.");
+  return new Promise((resolve, reject) => {
+    tx.onabort = () => {
+      reject(TransactionAbort);
+    };
+    tx.oncomplete = () => {
+      resolve();
+    };
+    tx.onerror = () => {
+      console.error("Transaction failed:", stack);
+      reject(tx.error);
+    };
+  });
+}
+
+export async function oneShotGet<T>(
   db: IDBDatabase,
   store: Store<T>,
   key: any,
 ): Promise<T | undefined> {
   const tx = db.transaction([store.name], "readonly");
   const req = tx.objectStore(store.name).get(key);
-  return requestToPromise(req);
+  const v = await requestToPromise(req)
+  await transactionToPromise(tx);
+  return v;
 }
 
-export function oneShotGetIndexed<S extends IDBValidKey, T>(
+export async function oneShotGetIndexed<S extends IDBValidKey, T>(
   db: IDBDatabase,
   index: Index<S, T>,
   key: any,
@@ -93,10 +118,12 @@ export function oneShotGetIndexed<S extends IDBValidKey, 
T>(
     .objectStore(index.storeName)
     .index(index.indexName)
     .get(key);
-  return requestToPromise(req);
+  const v = await requestToPromise(req);
+  await transactionToPromise(tx);
+  return v;
 }
 
-export function oneShotPut<T>(
+export async function oneShotPut<T>(
   db: IDBDatabase,
   store: Store<T>,
   value: T,
@@ -104,7 +131,9 @@ export function oneShotPut<T>(
 ): Promise<any> {
   const tx = db.transaction([store.name], "readwrite");
   const req = tx.objectStore(store.name).put(value, key);
-  return requestToPromise(req);
+  const v = await requestToPromise(req);
+  await transactionToPromise(tx);
+  return v;
 }
 
 function applyMutation<T>(
@@ -115,7 +144,7 @@ function applyMutation<T>(
     req.onsuccess = () => {
       const cursor = req.result;
       if (cursor) {
-        const val = cursor.value();
+        const val = cursor.value;
         const modVal = f(val);
         if (modVal !== undefined && modVal !== null) {
           const req2: IDBRequest = cursor.update(modVal);
@@ -138,7 +167,7 @@ function applyMutation<T>(
   });
 }
 
-export function oneShotMutate<T>(
+export async function oneShotMutate<T>(
   db: IDBDatabase,
   store: Store<T>,
   key: any,
@@ -146,7 +175,8 @@ export function oneShotMutate<T>(
 ): Promise<void> {
   const tx = db.transaction([store.name], "readwrite");
   const req = tx.objectStore(store.name).openCursor(key);
-  return applyMutation(req, f);
+  await applyMutation(req, f);
+  await transactionToPromise(tx);
 }
 
 type CursorResult<T> = CursorEmptyResult<T> | CursorValueResult<T>;
@@ -326,15 +356,12 @@ export function runWithWriteTransaction<T>(
   stores: Store<any>[],
   f: (t: TransactionHandle) => Promise<T>,
 ): Promise<T> {
+  const stack = Error("Failed transaction was started here.");
   return new Promise((resolve, reject) => {
     const storeName = stores.map(x => x.name);
     const tx = db.transaction(storeName, "readwrite");
     let funResult: any = undefined;
     let gotFunResult: boolean = false;
-    tx.onerror = () => {
-      console.error("error in transaction:", tx.error);
-      reject(tx.error);
-    };
     tx.oncomplete = () => {
       // This is a fatal error: The transaction completed *before*
       // the transaction function returned.  Likely, the transaction
@@ -350,15 +377,30 @@ export function runWithWriteTransaction<T>(
       }
       resolve(funResult);
     };
+    tx.onerror = () => {
+      console.error("error in transaction");
+    };
     tx.onabort = () => {
-      console.error("aborted transaction");
-      reject(AbortTransaction);
+      if (tx.error) {
+        console.error("Transaction aborted with error:", tx.error);
+      } else {
+        console.log("Trasaction aborted (no error)");
+      }
+      reject(TransactionAbort);
     };
     const th = new TransactionHandle(tx);
     const resP = f(th);
     resP.then(result => {
       gotFunResult = true;
       funResult = result;
+    }).catch((e) => {
+      if (e == TransactionAbort) {
+        console.info("aborting transaction");
+      } else {
+        tx.abort();
+        console.error("Transaction failed:", e);
+        console.error(stack);
+      }
     });
   });
 }
@@ -401,4 +443,4 @@ export class Index<S extends IDBValidKey, T> {
 /**
  * Exception that should be thrown by client code to abort a transaction.
  */
-export const AbortTransaction = Symbol("abort_transaction");
+export const TransactionAbort = Symbol("transaction_abort");
diff --git a/src/wallet.ts b/src/wallet.ts
index 71e058fd..58bb6b8c 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -31,10 +31,10 @@ import {
   strcmp,
   extractTalerStamp,
 } from "./helpers";
-import { HttpRequestLibrary, RequestException } from "./http";
+import { HttpRequestLibrary } from "./http";
 import * as LibtoolVersion from "./libtoolVersion";
 import {
-  AbortTransaction,
+  TransactionAbort,
   oneShotPut,
   oneShotGet,
   runWithWriteTransaction,
@@ -43,7 +43,6 @@ import {
   oneShotGetIndexed,
   oneShotMutate,
 } from "./query";
-import { TimerGroup } from "./timer";
 
 import { AmountJson } from "./amounts";
 import * as Amounts from "./amounts";
@@ -70,6 +69,7 @@ import {
   WithdrawalRecord,
   ExchangeDetails,
   ExchangeUpdateStatus,
+  ReserveRecordStatus,
 } from "./dbTypes";
 import {
   Auditor,
@@ -99,7 +99,7 @@ import {
   ConfirmReserveRequest,
   CreateReserveRequest,
   CreateReserveResponse,
-  HistoryRecord,
+  HistoryEvent,
   NextUrlResult,
   Notifier,
   PayCoinInfo,
@@ -119,8 +119,8 @@ import {
   HistoryQuery,
   getTimestampNow,
   OperationError,
+  Timestamp,
 } from "./walletTypes";
-import { openPromise } from "./promiseUtils";
 import {
   parsePayUri,
   parseWithdrawUri,
@@ -128,6 +128,7 @@ import {
   parseRefundUri,
 } from "./taleruri";
 import { isFirefox } from "./webex/compat";
+import { Logger } from "./logging";
 
 interface SpeculativePayData {
   payCoinInfo: PayCoinInfo;
@@ -343,22 +344,24 @@ interface CoinsForPaymentArgs {
   paymentAmount: AmountJson;
   wireFeeAmortization: number;
   wireFeeLimit: AmountJson;
-  wireFeeTime: number;
+  wireFeeTime: Timestamp;
   wireMethod: string;
 }
 
 /**
  * This error is thrown when an
  */
-class OperationFailedAndReportedError extends Error {
-  constructor(public reason: Error) {
-    super("Reported failed operation: " + reason.message);
+export class OperationFailedAndReportedError extends Error {
+  constructor(message: string) {
+    super(message);
 
     // Set the prototype explicitly.
     Object.setPrototypeOf(this, OperationFailedAndReportedError.prototype);
   }
 }
 
+const logger = new Logger("wallet.ts");
+
 /**
  * The platform-independent wallet implementation.
  */
@@ -372,26 +375,8 @@ export class Wallet {
   private badge: Badge;
   private notifier: Notifier;
   private cryptoApi: CryptoApi;
-  private processPreCoinConcurrent = 0;
-  private processPreCoinThrottle: { [url: string]: number } = {};
-  private timerGroup: TimerGroup;
   private speculativePayData: SpeculativePayData | undefined;
   private cachedNextUrl: { [fulfillmentUrl: string]: NextUrlResult } = {};
-  private activeTipOperations: { [s: string]: Promise<void> } = {};
-  private activeProcessReserveOperations: {
-    [reservePub: string]: Promise<void>;
-  } = {};
-  private activeProcessPreCoinOperations: {
-    [preCoinPub: string]: Promise<void>;
-  } = {};
-  private activeRefreshOperations: {
-    [coinPub: string]: Promise<void>;
-  } = {};
-
-  /**
-   * Set of identifiers for running operations.
-   */
-  private runningOperations: Set<string> = new Set();
 
   constructor(
     db: IDBDatabase,
@@ -405,10 +390,13 @@ export class Wallet {
     this.badge = badge;
     this.notifier = notifier;
     this.cryptoApi = new CryptoApi(cryptoWorkerFactory);
-    this.timerGroup = new TimerGroup();
   }
 
-  public async processPending(): Promise<void> {
+  /**
+   * Process pending operations.
+   */
+  public async runPending(): Promise<void> {
+    // FIXME:  maybe prioritize pending operations by their urgency?
     const exchangeBaseUrlList = await oneShotIter(
       this.db,
       Stores.exchanges,
@@ -417,19 +405,61 @@ export class Wallet {
     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);
+    }
+
+    const refreshSessionList = await oneShotIter(this.db, Stores.refresh).map(
+      x => x.refreshSessionId,
+    );
+    for (let rs of refreshSessionList) {
+      await this.processRefreshSession(rs);
+    }
   }
 
   /**
-   * Start processing pending operations asynchronously.
+   * Process pending operations and wait for scheduled operations in
+   * a loop until the wallet is stopped explicitly.
    */
-  public start() {
-    const work = async () => {
-      await this.collectGarbage().catch(e => console.log(e));
-      this.updateExchanges();
-      this.resumePendingFromDb();
-      this.timerGroup.every(1000 * 60 * 15, () => this.updateExchanges());
-    };
-    work();
+  public async runUntilStopped(): Promise<void> {
+    throw Error("not implemented");
+  }
+
+  /**
+   * Run until all coins have been withdrawn from the given reserve,
+   * or an error has occured.
+   */
+  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);
+        }
+        break;
+      }
+      await this.processReserve(reservePub);
+    }
   }
 
   /**
@@ -457,18 +487,6 @@ export class Wallet {
     );
   }
 
-  private startOperation(operationId: string) {
-    this.runningOperations.add(operationId);
-    this.badge.startBusy();
-  }
-
-  private stopOperation(operationId: string) {
-    this.runningOperations.delete(operationId);
-    if (this.runningOperations.size === 0) {
-      this.badge.stopBusy();
-    }
-  }
-
   async updateExchanges(): Promise<void> {
     const exchangeUrls = await oneShotIter(this.db, Stores.exchanges).map(
       e => e.baseUrl,
@@ -481,35 +499,6 @@ export class Wallet {
     }
   }
 
-  /**
-   * Resume various pending operations that are pending
-   * by looking at the database.
-   */
-  private resumePendingFromDb(): void {
-    Wallet.enableTracing && console.log("resuming pending operations from db");
-
-    oneShotIter(this.db, Stores.reserves).forEach(reserve => {
-      Wallet.enableTracing &&
-        console.log("resuming reserve", reserve.reserve_pub);
-      this.processReserve(reserve.reserve_pub);
-    });
-
-    oneShotIter(this.db, Stores.precoins).forEach(preCoin => {
-      Wallet.enableTracing && console.log("resuming precoin");
-      this.processPreCoin(preCoin.coinPub);
-    });
-
-    oneShotIter(this.db, Stores.refresh).forEach((r: RefreshSessionRecord) => {
-      this.continueRefreshSession(r);
-    });
-
-    oneShotIter(this.db, Stores.coinsReturns).forEach(
-      (r: CoinsReturnRecord) => {
-        this.depositReturnedCoins(r);
-      },
-    );
-  }
-
   private async getCoinsForReturn(
     exchangeBaseUrl: string,
     amount: AmountJson,
@@ -752,8 +741,8 @@ export class Wallet {
       payReq,
       refundsDone: {},
       refundsPending: {},
-      timestamp: new Date().getTime(),
-      timestamp_refund: 0,
+      timestamp: getTimestampNow(),
+      timestamp_refund: undefined,
     };
 
     await runWithWriteTransaction(
@@ -819,7 +808,13 @@ export class Wallet {
       proposal.contractTerms.fulfillment_url,
     );
 
-    if (differentPurchase) {
+    let fulfillmentUrl = proposal.contractTerms.fulfillment_url;
+    let doublePurchaseDetection = false;
+    if (fulfillmentUrl.startsWith("http")) {
+      doublePurchaseDetection = true;
+    }
+
+    if (differentPurchase && doublePurchaseDetection) {
       // We do this check to prevent merchant B to find out if we bought a
       // digital product with merchant A by abusing the existing payment
       // redirect feature.
@@ -870,7 +865,10 @@ export class Wallet {
         paymentAmount,
         wireFeeAmortization: proposal.contractTerms.wire_fee_amortization || 1,
         wireFeeLimit,
-        wireFeeTime: getTalerStampSec(proposal.contractTerms.timestamp) || 0,
+        // FIXME: parse this properly
+        wireFeeTime: extractTalerStamp(proposal.contractTerms.timestamp) || {
+          t_ms: 0,
+        },
         wireMethod: proposal.contractTerms.wire_method,
       });
 
@@ -962,7 +960,7 @@ export class Wallet {
       contractTermsHash,
       merchantSig: proposal.sig,
       noncePriv: priv,
-      timestamp: new Date().getTime(),
+      timestamp: getTimestampNow(),
       url,
       downloadSessionId: sessionId,
     };
@@ -1143,7 +1141,10 @@ export class Wallet {
       paymentAmount: Amounts.parseOrThrow(proposal.contractTerms.amount),
       wireFeeAmortization: proposal.contractTerms.wire_fee_amortization || 1,
       wireFeeLimit,
-      wireFeeTime: getTalerStampSec(proposal.contractTerms.timestamp) || 0,
+      // FIXME: parse this properly
+      wireFeeTime: extractTalerStamp(proposal.contractTerms.timestamp) || {
+        t_ms: 0,
+      },
       wireMethod: proposal.contractTerms.wire_method,
     });
 
@@ -1218,7 +1219,7 @@ export class Wallet {
   }
 
   /**
-   * Send reserve details
+   * Send reserve details to the bank.
    */
   private async sendReserveInfoToBank(reservePub: string) {
     const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
@@ -1226,12 +1227,16 @@ export class Wallet {
       throw Error("reserve not in db");
     }
 
+    if (reserve.reserveStatus != ReserveRecordStatus.REGISTERING_BANK) {
+      return;
+    }
+
     const bankStatusUrl = reserve.bankWithdrawStatusUrl;
     if (!bankStatusUrl) {
-      throw Error("reserve not confirmed yet, and no status URL available.");
+      throw Error("no bank withdraw status URL available.");
     }
 
-    const now = new Date().getTime();
+    const now = getTimestampNow();
     let status;
     try {
       const statusResp = await this.http.get(bankStatusUrl);
@@ -1243,10 +1248,10 @@ export class Wallet {
 
     if (status.transfer_done) {
       await oneShotMutate(this.db, Stores.reserves, reservePub, r => {
-        r.timestamp_confirmed = now;
+        r.timestampConfirmed = now;
         return r;
       });
-    } else if (reserve.timestamp_reserve_info_posted === 0) {
+    } else if (reserve.timestampReserveInfoPosted === undefined) {
       try {
         if (!status.selection_done) {
           const bankResp = await this.http.postJson(bankStatusUrl, {
@@ -1259,7 +1264,7 @@ export class Wallet {
         throw e;
       }
       await oneShotMutate(this.db, Stores.reserves, reservePub, r => {
-        r.timestamp_reserve_info_posted = now;
+        r.timestampReserveInfoPosted = now;
         return r;
       });
     }
@@ -1268,73 +1273,38 @@ export class Wallet {
   /**
    * First fetch information requred to withdraw from the reserve,
    * then deplete the reserve, withdrawing coins until it is empty.
+   *
+   * The returned promise resolves once the reserve is set to the
+   * state DORMANT.
    */
   async processReserve(reservePub: string): Promise<void> {
-    const activeOperation = this.activeProcessReserveOperations[reservePub];
-
-    if (activeOperation) {
-      return activeOperation;
+    const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
+    if (!reserve) {
+      console.log("not processing reserve: reserve does not exist");
+      return;
     }
-
-    const opId = "reserve-" + reservePub;
-    this.startOperation(opId);
-
-    // This opened promise gets resolved only once the
-    // reserve withdraw operation succeeds, even after retries.
-    const op = openPromise<void>();
-
-    const processReserveInternal = async (retryDelayMs: number = 250) => {
-      let isHardError = false;
-      // By default, do random, exponential backoff truncated at 3 minutes.
-      // Sometimes though, we want to try again faster.
-      let maxTimeout = 3000 * 60;
-      try {
-        const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
-        if (!reserve) {
-          isHardError = true;
-          throw Error("reserve not in db");
-        }
-
-        if (reserve.timestamp_confirmed === 0) {
-          const bankStatusUrl = reserve.bankWithdrawStatusUrl;
-          if (!bankStatusUrl) {
-            isHardError = true;
-            throw Error(
-              "reserve not confirmed yet, and no status URL available.",
-            );
-          }
-          maxTimeout = 2000;
-          /* This path is only taken if the wallet crashed after a withdraw 
was accepted,
-           * and before the information could be sent to the bank. */
-          await this.sendReserveInfoToBank(reservePub);
-          throw Error("waiting for reserve to be confirmed");
-        }
-
-        const updatedReserve = await this.updateReserve(reservePub);
-        await this.depleteReserve(updatedReserve);
-        op.resolve();
-      } catch (e) {
-        if (isHardError) {
-          op.reject(e);
-        }
-        const nextDelay = Math.min(
-          2 * retryDelayMs + retryDelayMs * Math.random(),
-          maxTimeout,
-        );
-
-        this.timerGroup.after(retryDelayMs, () =>
-          processReserveInternal(nextDelay),
-        );
-      }
-    };
-
-    try {
-      processReserveInternal();
-      this.activeProcessReserveOperations[reservePub] = op.promise;
-      await op.promise;
-    } finally {
-      this.stopOperation(opId);
-      delete this.activeProcessReserveOperations[reservePub];
+    logger.trace(
+      `Processing reserve ${reservePub} with status ${reserve.reserveStatus}`,
+    );
+    switch (reserve.reserveStatus) {
+      case ReserveRecordStatus.UNCONFIRMED:
+        // nothing to do
+        break;
+      case ReserveRecordStatus.REGISTERING_BANK:
+        await this.sendReserveInfoToBank(reservePub);
+        return this.processReserve(reservePub);
+      case ReserveRecordStatus.QUERYING_STATUS:
+        await this.updateReserve(reservePub);
+        return this.processReserve(reservePub);
+      case ReserveRecordStatus.WITHDRAWING:
+        await this.depleteReserve(reservePub);
+        break;
+      case ReserveRecordStatus.DORMANT:
+        // nothing to do
+        break;
+      default:
+        console.warn("unknown reserve record status:", reserve.reserveStatus);
+        break;
     }
   }
 
@@ -1342,117 +1312,89 @@ export class Wallet {
    * Given a planchet, withdraw a coin from the exchange.
    */
   private async processPreCoin(preCoinPub: string): Promise<void> {
-    const activeOperation = this.activeProcessPreCoinOperations[preCoinPub];
-    if (activeOperation) {
-      return activeOperation;
+    console.log("processPreCoin", preCoinPub);
+    const preCoin = await oneShotGet(this.db, Stores.precoins, preCoinPub);
+    if (!preCoin) {
+      console.log("processPreCoin: preCoinPub not found");
+      return;
+    }
+    const exchange = await oneShotGet(
+      this.db,
+      Stores.exchanges,
+      preCoin.exchangeBaseUrl,
+    );
+    if (!exchange) {
+      console.error("db inconsistent: exchange for precoin not found");
+      return;
     }
 
-    const op = openPromise<void>();
-
-    const processPreCoinInternal = async (retryDelayMs: number = 200) => {
-      const preCoin = await oneShotGet(this.db, Stores.precoins, preCoinPub);
-      if (!preCoin) {
-        console.log("processPreCoin: preCoinPub not found");
-        return;
-      }
-      // Throttle concurrent executions of this function,
-      // so we don't withdraw too many coins at once.
-      if (
-        this.processPreCoinConcurrent >= 4 ||
-        this.processPreCoinThrottle[preCoin.exchangeBaseUrl]
-      ) {
-        const timeout = Math.min(retryDelayMs * 2, 5 * 60 * 1000);
-        Wallet.enableTracing &&
-          console.log(
-            `throttling processPreCoin of ${preCoinPub} for ${timeout}ms`,
-          );
-        this.timerGroup.after(retryDelayMs, () => processPreCoinInternal());
-        return op.promise;
-      }
-
-      this.processPreCoinConcurrent++;
-
-      try {
-        const exchange = await oneShotGet(
-          this.db,
-          Stores.exchanges,
-          preCoin.exchangeBaseUrl,
-        );
-        if (!exchange) {
-          console.error("db inconsistent: exchange for precoin not found");
-          return;
-        }
-        const denom = await oneShotGet(this.db, Stores.denominations, [
-          preCoin.exchangeBaseUrl,
-          preCoin.denomPub,
-        ]);
-        if (!denom) {
-          console.error("db inconsistent: denom for precoin not found");
-          return;
-        }
+    const denom = await oneShotGet(this.db, Stores.denominations, [
+      preCoin.exchangeBaseUrl,
+      preCoin.denomPub,
+    ]);
 
-        const coin = await this.withdrawExecute(preCoin);
+    if (!denom) {
+      console.error("db inconsistent: denom for precoin not found");
+      return;
+    }
 
-        const mutateReserve = (r: ReserveRecord) => {
-          const x = Amounts.sub(
-            r.precoin_amount,
-            preCoin.coinValue,
-            denom.feeWithdraw,
-          );
-          if (x.saturated) {
-            console.error("database inconsistent");
-            throw AbortTransaction;
-          }
-          r.precoin_amount = x.amount;
-          return r;
-        };
+    const wd: any = {};
+    wd.denom_pub_hash = preCoin.denomPubHash;
+    wd.reserve_pub = preCoin.reservePub;
+    wd.reserve_sig = preCoin.withdrawSig;
+    wd.coin_ev = preCoin.coinEv;
+    const reqUrl = new URI("reserve/withdraw").absoluteTo(exchange.baseUrl);
+    const resp = await this.http.postJson(reqUrl.href(), wd);
 
-        await runWithWriteTransaction(
-          this.db,
-          [Stores.reserves, Stores.precoins, Stores.coins],
-          async tx => {
-            await tx.mutate(Stores.reserves, preCoin.reservePub, 
mutateReserve);
-            await tx.delete(Stores.precoins, coin.coinPub);
-            await tx.add(Stores.coins, coin);
-          },
-        );
+    const r = resp.responseJson;
 
-        this.badge.showNotification();
+    const denomSig = await this.cryptoApi.rsaUnblind(
+      r.ev_sig,
+      preCoin.blindingKey,
+      preCoin.denomPub,
+    );
 
-        this.notifier.notify();
-        op.resolve();
-      } catch (e) {
-        console.error(
-          "Failed to withdraw coin from precoin, retrying in",
-          retryDelayMs,
-          "ms",
-          e,
-        );
-        // exponential backoff truncated at one minute
-        const nextRetryDelayMs = Math.min(retryDelayMs * 2, 5 * 60 * 1000);
-        this.timerGroup.after(retryDelayMs, () =>
-          processPreCoinInternal(nextRetryDelayMs),
-        );
+    const coin: CoinRecord = {
+      blindingKey: preCoin.blindingKey,
+      coinPriv: preCoin.coinPriv,
+      coinPub: preCoin.coinPub,
+      currentAmount: preCoin.coinValue,
+      denomPub: preCoin.denomPub,
+      denomPubHash: preCoin.denomPubHash,
+      denomSig,
+      exchangeBaseUrl: preCoin.exchangeBaseUrl,
+      reservePub: preCoin.reservePub,
+      status: CoinStatus.Fresh,
+    };
 
-        const currentThrottle =
-          this.processPreCoinThrottle[preCoin.exchangeBaseUrl] || 0;
-        this.processPreCoinThrottle[preCoin.exchangeBaseUrl] =
-          currentThrottle + 1;
-        this.timerGroup.after(retryDelayMs, () => {
-          this.processPreCoinThrottle[preCoin.exchangeBaseUrl]--;
-        });
-      } finally {
-        this.processPreCoinConcurrent--;
+    const mutateReserve = (r: ReserveRecord) => {
+      const x = Amounts.sub(
+        r.precoinAmount,
+        preCoin.coinValue,
+        denom.feeWithdraw,
+      );
+      if (x.saturated) {
+        console.error("database inconsistent");
+        throw TransactionAbort;
       }
+      r.precoinAmount = x.amount;
+      return r;
     };
 
-    try {
-      this.activeProcessPreCoinOperations[preCoinPub] = op.promise;
-      await processPreCoinInternal();
-      return op.promise;
-    } finally {
-      delete this.activeProcessPreCoinOperations[preCoinPub];
-    }
+    await runWithWriteTransaction(
+      this.db,
+      [Stores.reserves, Stores.precoins, Stores.coins],
+      async tx => {
+        const currentPc = await tx.get(Stores.precoins, coin.coinPub);
+        if (!currentPc) {
+          return;
+        }
+        await tx.mutate(Stores.reserves, preCoin.reservePub, mutateReserve);
+        await tx.delete(Stores.precoins, coin.coinPub);
+        await tx.add(Stores.coins, coin);
+      },
+    );
+    logger.trace(`withdraw of one coin ${coin.coinPub} finished`);
   }
 
   /**
@@ -1465,24 +1407,31 @@ export class Wallet {
     req: CreateReserveRequest,
   ): Promise<CreateReserveResponse> {
     const keypair = await this.cryptoApi.createEddsaKeypair();
-    const now = new Date().getTime();
+    const now = getTimestampNow();
     const canonExchange = canonicalizeBaseUrl(req.exchange);
 
+    let reserveStatus;
+    if (req.bankWithdrawStatusUrl) {
+      reserveStatus = ReserveRecordStatus.REGISTERING_BANK;
+    } else {
+      reserveStatus = ReserveRecordStatus.UNCONFIRMED;
+    }
+
     const reserveRecord: ReserveRecord = {
       created: now,
-      current_amount: null,
-      exchange_base_url: canonExchange,
+      currentAmount: null,
+      exchangeBaseUrl: canonExchange,
       hasPayback: false,
-      precoin_amount: Amounts.getZero(req.amount.currency),
-      requested_amount: req.amount,
-      reserve_priv: keypair.priv,
-      reserve_pub: keypair.pub,
+      precoinAmount: Amounts.getZero(req.amount.currency),
+      requestedAmount: req.amount,
+      reservePriv: keypair.priv,
+      reservePub: keypair.pub,
       senderWire: req.senderWire,
-      timestamp_confirmed: 0,
-      timestamp_reserve_info_posted: 0,
-      timestamp_depleted: 0,
+      timestampConfirmed: undefined,
+      timestampReserveInfoPosted: undefined,
       bankWithdrawStatusUrl: req.bankWithdrawStatusUrl,
       exchangeWire: req.exchangeWire,
+      reserveStatus,
     };
 
     const senderWire = req.senderWire;
@@ -1522,7 +1471,7 @@ export class Wallet {
 
     const cr: CurrencyRecord = currencyRecord;
 
-    runWithWriteTransaction(
+    await runWithWriteTransaction(
       this.db,
       [Stores.currencies, Stores.reserves],
       async tx => {
@@ -1531,9 +1480,9 @@ export class Wallet {
       },
     );
 
-    if (req.bankWithdrawStatusUrl) {
-      this.processReserve(keypair.pub);
-    }
+    this.processReserve(keypair.pub).catch(e => {
+      console.error("Processing reserve failed:", e);
+    });
 
     const r: CreateReserveResponse = {
       exchange: canonExchange,
@@ -1552,53 +1501,21 @@ export class Wallet {
    * an unconfirmed reserve should be hidden.
    */
   async confirmReserve(req: ConfirmReserveRequest): Promise<void> {
-    const now = new Date().getTime();
-    const reserve = await oneShotGet(this.db, Stores.reserves, req.reservePub);
-    if (!reserve) {
-      console.error("Unable to confirm reserve, not found in DB");
-      return;
-    }
-    reserve.timestamp_confirmed = now;
-    await oneShotPut(this.db, Stores.reserves, reserve);
-    this.notifier.notify();
-
-    this.processReserve(reserve.reserve_pub);
-  }
+    const now = getTimestampNow();
+    await oneShotMutate(this.db, Stores.reserves, req.reservePub, reserve => {
+      if (reserve.reserveStatus !== ReserveRecordStatus.UNCONFIRMED) {
+        return;
+      }
+      reserve.timestampConfirmed = now;
+      reserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
+      return reserve;
+    });
 
-  private async withdrawExecute(pc: PreCoinRecord): Promise<CoinRecord> {
-    const wd: any = {};
-    wd.denom_pub_hash = pc.denomPubHash;
-    wd.reserve_pub = pc.reservePub;
-    wd.reserve_sig = pc.withdrawSig;
-    wd.coin_ev = pc.coinEv;
-    const reqUrl = new URI("reserve/withdraw").absoluteTo(pc.exchangeBaseUrl);
-    const resp = await this.http.postJson(reqUrl.href(), wd);
+    this.notifier.notify();
 
-    if (resp.status !== 200) {
-      throw new RequestException({
-        hint: "Withdrawal failed",
-        status: resp.status,
-      });
-    }
-    const r = resp.responseJson;
-    const denomSig = await this.cryptoApi.rsaUnblind(
-      r.ev_sig,
-      pc.blindingKey,
-      pc.denomPub,
-    );
-    const coin: CoinRecord = {
-      blindingKey: pc.blindingKey,
-      coinPriv: pc.coinPriv,
-      coinPub: pc.coinPub,
-      currentAmount: pc.coinValue,
-      denomPub: pc.denomPub,
-      denomPubHash: pc.denomPubHash,
-      denomSig,
-      exchangeBaseUrl: pc.exchangeBaseUrl,
-      reservePub: pc.reservePub,
-      status: CoinStatus.Fresh,
-    };
-    return coin;
+    this.processReserve(req.reservePub).catch(e => {
+      console.log("processing reserve failed:", e);
+    });
   }
 
   /**
@@ -1607,31 +1524,41 @@ export class Wallet {
    * When finished, marks the reserve as depleted by setting
    * the depleted timestamp.
    */
-  private async depleteReserve(reserve: ReserveRecord): Promise<void> {
-    Wallet.enableTracing && console.log("depleting reserve");
-    if (!reserve.current_amount) {
-      throw Error("can't withdraw when amount is unknown");
+  private async depleteReserve(reservePub: string): Promise<void> {
+    const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
+    if (!reserve) {
+      return;
     }
-    const withdrawAmount = reserve.current_amount;
+    if (reserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
+      return;
+    }
+    logger.trace(`depleting reserve ${reservePub}`);
+
+    const withdrawAmount = reserve.currentAmount;
     if (!withdrawAmount) {
-      throw Error("can't withdraw when amount is unknown");
+      throw Error("BUG: reserveStatus=WITHDRAWING, but currentAmount is 
empty");
     }
+
     const denomsForWithdraw = await this.getVerifiedWithdrawDenomList(
-      reserve.exchange_base_url,
+      reserve.exchangeBaseUrl,
       withdrawAmount,
     );
-    const smallestAmount = await this.getVerifiedSmallestWithdrawAmount(
-      reserve.exchange_base_url,
-    );
-
-    console.log(`withdrawing ${denomsForWithdraw.length} coins`);
-
-    const stampMsNow = Math.floor(new Date().getTime());
+    if (denomsForWithdraw.length === 0) {
+      const m = `Unable to withdraw from reserve, no denominations are 
available to withdraw.`;
+      await this.setReserveError(reserve.reservePub, {
+        type: "internal",
+        message: m,
+        details: {},
+      });
+      throw new OperationFailedAndReportedError(m);
+    }
 
     const withdrawalRecord: WithdrawalRecord = {
-      reservePub: reserve.reserve_pub,
+      reservePub: reserve.reservePub,
       withdrawalAmount: Amounts.toString(withdrawAmount),
-      startTimestamp: stampMsNow,
+      startTimestamp: getTimestampNow(),
+      numCoinsTotal: denomsForWithdraw.length,
+      numCoinsWithdrawn: 0,
     };
 
     const preCoinRecords: PreCoinRecord[] = await Promise.all(
@@ -1651,49 +1578,50 @@ export class Wallet {
     ).amount;
 
     function mutateReserve(r: ReserveRecord): ReserveRecord {
-      const currentAmount = r.current_amount;
+      const currentAmount = r.currentAmount;
       if (!currentAmount) {
         throw Error("can't withdraw when amount is unknown");
       }
-      r.precoin_amount = Amounts.add(
-        r.precoin_amount,
+      r.precoinAmount = Amounts.add(
+        r.precoinAmount,
         totalWithdrawAmount,
       ).amount;
       const result = Amounts.sub(currentAmount, totalWithdrawAmount);
       if (result.saturated) {
         console.error("can't create precoins, saturated");
-        throw AbortTransaction;
-      }
-      r.current_amount = result.amount;
-
-      // Reserve is depleted if the amount left is too small to withdraw
-      if (Amounts.cmp(r.current_amount, smallestAmount) < 0) {
-        r.timestamp_depleted = new Date().getTime();
+        throw TransactionAbort;
       }
+      r.currentAmount = result.amount;
+      r.reserveStatus = ReserveRecordStatus.DORMANT;
 
       return r;
     }
 
-    // This will fail and throw an exception if the remaining amount in the
-    // reserve is too low to create a pre-coin.
-    try {
-      await runWithWriteTransaction(
-        this.db,
-        [Stores.precoins, Stores.withdrawals, Stores.reserves],
-        async tx => {
-          for (let pcr of preCoinRecords) {
-            await tx.put(Stores.precoins, pcr);
-          }
-          await tx.mutate(Stores.reserves, reserve.reserve_pub, mutateReserve);
-          await tx.put(Stores.withdrawals, withdrawalRecord);
-        },
-      );
-    } catch (e) {
-      return;
-    }
+    const success = await runWithWriteTransaction(
+      this.db,
+      [Stores.precoins, Stores.withdrawals, Stores.reserves],
+      async tx => {
+        const myReserve = await tx.get(Stores.reserves, reservePub);
+        if (!myReserve) {
+          return false;
+        }
+        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);
+        return true;
+      },
+    );
 
-    for (let x of preCoinRecords) {
-      await this.processPreCoin(x.coinPub);
+    if (success) {
+      logger.trace(`withdrawing ${preCoinRecords.length} coins`);
+      for (let x of preCoinRecords) {
+        await this.processPreCoin(x.coinPub);
+      }
     }
   }
 
@@ -1701,34 +1629,52 @@ export class Wallet {
    * Update the information about a reserve that is stored in the wallet
    * by quering the reserve's exchange.
    */
-  private async updateReserve(reservePub: string): Promise<ReserveRecord> {
+  private async updateReserve(reservePub: string): Promise<void> {
     const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
     if (!reserve) {
       throw Error("reserve not in db");
     }
 
-    if (reserve.timestamp_confirmed === 0) {
-      throw Error("");
+    if (reserve.timestampConfirmed === undefined) {
+      throw Error("reserve not confirmed yet");
+    }
+
+    if (reserve.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
+      return;
     }
 
     const reqUrl = new URI("reserve/status").absoluteTo(
-      reserve.exchange_base_url,
+      reserve.exchangeBaseUrl,
     );
     reqUrl.query({ reserve_pub: reservePub });
-    const resp = await this.http.get(reqUrl.href());
-    if (resp.status !== 200) {
-      Wallet.enableTracing &&
-        console.warn(`reserve/status returned ${resp.status}`);
-      throw Error();
+    let resp;
+    try {
+      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;
+        this.setReserveError(reservePub, {
+          type: "network",
+          details: {},
+          message: m,
+        });
+        throw new OperationFailedAndReportedError(m);
+      }
     }
     const reserveInfo = ReserveStatus.checked(resp.responseJson);
-    if (!reserveInfo) {
-      throw Error();
-    }
-    reserve.current_amount = 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;
+      return r;
+    });
     await oneShotPut(this.db, Stores.reserves, reserve);
     this.notifier.notify();
-    return reserve;
   }
 
   async getPossibleDenoms(
@@ -1984,7 +1930,8 @@ export class Wallet {
         versionMatch.currentCmp === -1
       ) {
         console.warn(
-          `wallet version ${WALLET_PROTOCOL_VERSION} might be outdated 
(exchange has ${exchangeDetails.protocolVersion}), checking for updates`,
+          `wallet version ${WALLET_PROTOCOL_VERSION} might be outdated ` +
+            `(exchange has ${exchangeDetails.protocolVersion}), checking for 
updates`,
         );
         if (isFirefox()) {
           console.log("skipping update check on Firefox");
@@ -2060,19 +2007,25 @@ export class Wallet {
         wireInfo: undefined,
         updateStatus: ExchangeUpdateStatus.FETCH_KEYS,
         updateStarted: now,
+        updateReason: "initial",
+        timestampAdded: getTimestampNow(),
       };
       await oneShotPut(this.db, Stores.exchanges, newExchangeRecord);
     } else {
-      runWithWriteTransaction(this.db, [Stores.exchanges], async t => {
+      await runWithWriteTransaction(this.db, [Stores.exchanges], async t => {
         const rec = await t.get(Stores.exchanges, baseUrl);
         if (!rec) {
           return;
         }
-        if (rec.updateStatus != ExchangeUpdateStatus.NONE && !force) {
+        if (rec.updateStatus != ExchangeUpdateStatus.FETCH_KEYS && !force) {
           return;
         }
+        if (rec.updateStatus != ExchangeUpdateStatus.FETCH_KEYS && force) {
+          rec.updateReason = "forced";
+        }
         rec.updateStarted = now;
         rec.updateStatus = ExchangeUpdateStatus.FETCH_KEYS;
+        rec.lastError = undefined;
         t.put(Stores.exchanges, rec);
       });
     }
@@ -2104,6 +2057,17 @@ export class Wallet {
     await oneShotMutate(this.db, Stores.exchanges, baseUrl, mut);
   }
 
+  private async setReserveError(
+    reservePub: string,
+    err: OperationError,
+  ): Promise<void> {
+    const mut = (reserve: ReserveRecord) => {
+      reserve.lastError = err;
+      return reserve;
+    };
+    await oneShotMutate(this.db, Stores.reserves, reservePub, mut);
+  }
+
   /**
    * Fetch the exchange's /keys and update our database accordingly.
    *
@@ -2129,23 +2093,27 @@ export class Wallet {
     try {
       keysResp = await this.http.get(keysUrl.href());
     } catch (e) {
+      const m = `Fetching keys failed: ${e.message}`;
       await this.setExchangeError(baseUrl, {
         type: "network",
-        details: {},
-        message: `Fetching keys failed: ${e.message}`,
+        details: {
+          requestUrl: e.config?.url,
+        },
+        message: m,
       });
-      throw e;
+      throw new OperationFailedAndReportedError(m);
     }
     let exchangeKeysJson: KeysJson;
     try {
       exchangeKeysJson = KeysJson.checked(keysResp.responseJson);
     } catch (e) {
+      const m = `Parsing /keys response failed: ${e.message}`;
       await this.setExchangeError(baseUrl, {
         type: "protocol-violation",
         details: {},
-        message: `Parsing /keys response failed: ${e.message}`,
+        message: m,
       });
-      throw e;
+      throw new OperationFailedAndReportedError(m);
     }
 
     const lastUpdateTimestamp = extractTalerStamp(
@@ -2158,7 +2126,7 @@ export class Wallet {
         details: {},
         message: m,
       });
-      throw Error(m);
+      throw new OperationFailedAndReportedError(m);
     }
 
     if (exchangeKeysJson.denoms.length === 0) {
@@ -2168,7 +2136,7 @@ export class Wallet {
         details: {},
         message: m,
       });
-      throw Error(m);
+      throw new OperationFailedAndReportedError(m);
     }
 
     const protocolVersion = exchangeKeysJson.version;
@@ -2179,32 +2147,69 @@ export class Wallet {
         details: {},
         message: m,
       });
-      throw Error(m);
+      throw new OperationFailedAndReportedError(m);
     }
 
     const currency = Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value)
       .currency;
 
-    const mutExchangeRecord = (r: ExchangeRecord) => {
-      if (r.updateStatus != ExchangeUpdateStatus.FETCH_KEYS) {
-        console.log("not updating, wrong state (concurrent modification?)");
-        return undefined;
-      }
-      r.details = {
-        currency,
-        protocolVersion,
-        lastUpdateTime: lastUpdateTimestamp,
-        masterPublicKey: exchangeKeysJson.master_public_key,
-        auditors: exchangeKeysJson.auditors,
-      };
-      r.updateStatus = ExchangeUpdateStatus.FETCH_WIRE;
-      r.lastError = undefined;
-      return r;
-    };
+    const newDenominations = await Promise.all(
+      exchangeKeysJson.denoms.map(d =>
+        this.denominationRecordFromKeys(baseUrl, d),
+      ),
+    );
+
+    await runWithWriteTransaction(
+      this.db,
+      [Stores.exchanges, Stores.denominations],
+      async tx => {
+        const r = await tx.get(Stores.exchanges, baseUrl);
+        if (!r) {
+          console.warn(`exchange ${baseUrl} no longer present`);
+          return;
+        }
+        if (r.details) {
+          // FIXME: We need to do some consistency checks!
+        }
+        r.details = {
+          auditors: exchangeKeysJson.auditors,
+          currency: currency,
+          lastUpdateTime: lastUpdateTimestamp,
+          masterPublicKey: exchangeKeysJson.master_public_key,
+          protocolVersion: protocolVersion,
+        };
+        r.updateStatus = ExchangeUpdateStatus.FETCH_WIRE;
+        r.lastError = undefined;
+        await tx.put(Stores.exchanges, r);
+
+        for (const newDenom of newDenominations) {
+          const oldDenom = await tx.get(Stores.denominations, [
+            baseUrl,
+            newDenom.denomPub,
+          ]);
+          if (oldDenom) {
+            // FIXME: Do consistency check
+          } else {
+            await tx.put(Stores.denominations, newDenom);
+          }
+        }
+      },
+    );
   }
 
+  /**
+   * Fetch wire information for an exchange and store it in the database.
+   *
+   * @param exchangeBaseUrl Exchange base URL, assumed to be already 
normalized.
+   */
   private async updateExchangeWithWireInfo(exchangeBaseUrl: string) {
-    exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
+    const exchange = await this.findExchange(exchangeBaseUrl);
+    if (!exchange) {
+      return;
+    }
+    if (exchange.updateStatus != ExchangeUpdateStatus.FETCH_WIRE) {
+      return;
+    }
     const reqUrl = new URI("wire")
       .absoluteTo(exchangeBaseUrl)
       .addQuery("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
@@ -2215,6 +2220,45 @@ export class Wallet {
       throw Error("/wire response malformed");
     }
     const wireInfo = ExchangeWireJson.checked(wiJson);
+    const feesForType: { [wireMethod: string]: WireFee[] } = {};
+    for (const wireMethod of Object.keys(wireInfo.fees)) {
+      const feeList: WireFee[] = [];
+      for (const x of wireInfo.fees[wireMethod]) {
+        const startStamp = extractTalerStamp(x.start_date);
+        if (!startStamp) {
+          throw Error("wrong date format");
+        }
+        const endStamp = extractTalerStamp(x.end_date);
+        if (!endStamp) {
+          throw Error("wrong date format");
+        }
+        feeList.push({
+          closingFee: Amounts.parseOrThrow(x.closing_fee),
+          endStamp,
+          sig: x.sig,
+          startStamp,
+          wireFee: Amounts.parseOrThrow(x.wire_fee),
+        });
+      }
+      feesForType[wireMethod] = feeList;
+    }
+
+    await runWithWriteTransaction(this.db, [Stores.exchanges], async tx => {
+      const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
+      if (!r) {
+        return;
+      }
+      if (r.updateStatus != ExchangeUpdateStatus.FETCH_WIRE) {
+        return;
+      }
+      r.wireInfo = {
+        accounts: wireInfo.accounts,
+        feesForType: feesForType,
+      };
+      r.updateStatus = ExchangeUpdateStatus.FINISHED;
+      r.lastError = undefined;
+      await tx.put(Stores.exchanges, r);
+    });
   }
 
   /**
@@ -2312,17 +2356,17 @@ export class Wallet {
         });
 
         await tx.iter(Stores.reserves).forEach(r => {
-          if (!r.timestamp_confirmed) {
+          if (!r.timestampConfirmed) {
             return;
           }
-          let amount = Amounts.getZero(r.requested_amount.currency);
-          amount = Amounts.add(amount, r.precoin_amount).amount;
-          addTo(balanceStore, "pendingIncoming", amount, r.exchange_base_url);
+          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.exchange_base_url,
+            r.exchangeBaseUrl,
           );
         });
 
@@ -2333,8 +2377,8 @@ export class Wallet {
           addTo(
             balanceStore,
             "paybackAmount",
-            r.current_amount!,
-            r.exchange_base_url,
+            r.currentAmount!,
+            r.exchangeBaseUrl,
           );
           return balanceStore;
         });
@@ -2359,23 +2403,27 @@ export class Wallet {
     return balanceStore;
   }
 
-  async createRefreshSession(
-    oldCoinPub: string,
-  ): Promise<RefreshSessionRecord | undefined> {
+  async refresh(oldCoinPub: string, force: boolean = false): Promise<void> {
     const coin = await oneShotGet(this.db, Stores.coins, oldCoinPub);
-
     if (!coin) {
-      throw Error("coin not found");
+      console.warn("can't refresh, coin not in database");
+      return;
     }
-
-    if (coin.currentAmount.value === 0 && coin.currentAmount.fraction === 0) {
-      return undefined;
+    switch (coin.status) {
+      case CoinStatus.Dirty:
+        break;
+      case CoinStatus.Dormant:
+        return;
+      case CoinStatus.Fresh:
+        if (!force) {
+          return;
+        }
+        break;
     }
 
     const exchange = await this.updateExchangeFromUrl(coin.exchangeBaseUrl);
-
     if (!exchange) {
-      throw Error("db inconsistent");
+      throw Error("db inconsistent: exchange of coin not found");
     }
 
     const oldDenom = await oneShotGet(this.db, Stores.denominations, [
@@ -2384,7 +2432,7 @@ export class Wallet {
     ]);
 
     if (!oldDenom) {
-      throw Error("db inconsistent");
+      throw Error("db inconsistent: denomination for coin not found");
     }
 
     const availableDenoms: DenominationRecord[] = await oneShotIterIndex(
@@ -2401,20 +2449,22 @@ export class Wallet {
       availableDenoms,
     );
 
-    Wallet.enableTracing && console.log("refreshing coin", coin);
-    Wallet.enableTracing && console.log("refreshing into", newCoinDenoms);
-
     if (newCoinDenoms.length === 0) {
-      Wallet.enableTracing &&
-        console.log(
-          `not refreshing, available amount ${amountToPretty(
-            availableAmount,
-          )} too small`,
-        );
-      coin.status = CoinStatus.Useless;
-      await oneShotPut(this.db, Stores.coins, coin);
+      logger.trace(
+        `not refreshing, available amount ${amountToPretty(
+          availableAmount,
+        )} too small`,
+      );
+      await oneShotMutate(this.db, Stores.coins, oldCoinPub, x => {
+        if (x.status != coin.status) {
+          // Concurrent modification?
+          return;
+        }
+        x.status = CoinStatus.Dormant;
+        return x;
+      });
       this.notifier.notify();
-      return undefined;
+      return;
     }
 
     const refreshSession: RefreshSessionRecord = await 
this.cryptoApi.createRefreshSession(
@@ -2429,114 +2479,58 @@ export class Wallet {
       const r = Amounts.sub(c.currentAmount, refreshSession.valueWithFee);
       if (r.saturated) {
         // Something else must have written the coin value
-        throw AbortTransaction;
+        throw TransactionAbort;
       }
       c.currentAmount = r.amount;
-      c.status = CoinStatus.Refreshed;
+      c.status = CoinStatus.Dormant;
       return c;
     }
 
-    let key;
-
     // Store refresh session and subtract refreshed amount from
     // coin in the same transaction.
     await runWithWriteTransaction(
       this.db,
       [Stores.refresh, Stores.coins],
       async tx => {
-        key = await tx.put(Stores.refresh, refreshSession);
+        await tx.put(Stores.refresh, refreshSession);
         await tx.mutate(Stores.coins, coin.coinPub, mutateCoin);
       },
     );
+    logger.info(`created refresh session ${refreshSession.refreshSessionId}`);
     this.notifier.notify();
 
-    if (!key || typeof key !== "number") {
-      throw Error("insert failed");
-    }
-
-    refreshSession.id = key;
-
-    return refreshSession;
+    await this.processRefreshSession(refreshSession.refreshSessionId);
   }
 
-  async refresh(oldCoinPub: string): Promise<void> {
-    const refreshImpl = async () => {
-      const oldRefreshSessions = await oneShotIter(
-        this.db,
-        Stores.refresh,
-      ).toArray();
-      for (const session of oldRefreshSessions) {
-        if (session.finished) {
-          continue;
-        }
-        Wallet.enableTracing &&
-          console.log(
-            "waiting for unfinished old refresh session for",
-            oldCoinPub,
-            session,
-          );
-        await this.continueRefreshSession(session);
-      }
-      const coin = await oneShotGet(this.db, Stores.coins, oldCoinPub);
-      if (!coin) {
-        console.warn("can't refresh, coin not in database");
-        return;
-      }
-      if (
-        coin.status === CoinStatus.Useless ||
-        coin.status === CoinStatus.Fresh
-      ) {
-        Wallet.enableTracing &&
-          console.log(
-            "not refreshing due to coin status",
-            CoinStatus[coin.status],
-          );
-        return;
-      }
-      const refreshSession = await this.createRefreshSession(oldCoinPub);
-      if (!refreshSession) {
-        // refreshing not necessary
-        Wallet.enableTracing && console.log("not refreshing", oldCoinPub);
-        return;
-      }
-      return this.continueRefreshSession(refreshSession);
-    };
-
-    const activeRefreshOp = this.activeRefreshOperations[oldCoinPub];
-
-    if (activeRefreshOp) {
-      return activeRefreshOp;
-    }
-
-    try {
-      const newOp = refreshImpl();
-      this.activeRefreshOperations[oldCoinPub] = newOp;
-      const res = await newOp;
-      return res;
-    } finally {
-      delete this.activeRefreshOperations[oldCoinPub];
+  async processRefreshSession(refreshSessionId: string) {
+    const refreshSession = await oneShotGet(
+      this.db,
+      Stores.refresh,
+      refreshSessionId,
+    );
+    if (!refreshSession) {
+      return;
     }
-  }
-
-  async continueRefreshSession(refreshSession: RefreshSessionRecord) {
     if (refreshSession.finished) {
       return;
     }
     if (typeof refreshSession.norevealIndex !== "number") {
-      await this.refreshMelt(refreshSession);
-      const r = await oneShotGet(this.db, Stores.refresh, refreshSession.id);
-      if (!r) {
-        throw Error("refresh session does not exist anymore");
-      }
-      refreshSession = r;
+      await this.refreshMelt(refreshSession.refreshSessionId);
     }
-
-    await this.refreshReveal(refreshSession);
+    await this.refreshReveal(refreshSession.refreshSessionId);
+    logger.trace("refresh finished");
   }
 
-  async refreshMelt(refreshSession: RefreshSessionRecord): Promise<void> {
+  async refreshMelt(refreshSessionId: string): Promise<void> {
+    const refreshSession = await oneShotGet(
+      this.db,
+      Stores.refresh,
+      refreshSessionId,
+    );
+    if (!refreshSession) {
+      return;
+    }
     if (refreshSession.norevealIndex !== undefined) {
-      console.error("won't melt again");
       return;
     }
 
@@ -2582,12 +2576,29 @@ export class Wallet {
 
     refreshSession.norevealIndex = norevealIndex;
 
-    await oneShotPut(this.db, Stores.refresh, refreshSession);
+    await oneShotMutate(this.db, Stores.refresh, refreshSessionId, rs => {
+      if (rs.norevealIndex !== undefined) {
+        return;
+      }
+      if (rs.finished) {
+        return;
+      }
+      rs.norevealIndex = norevealIndex;
+      return rs;
+    });
 
     this.notifier.notify();
   }
 
-  async refreshReveal(refreshSession: RefreshSessionRecord): Promise<void> {
+  private async refreshReveal(refreshSessionId: string): Promise<void> {
+    const refreshSession = await oneShotGet(
+      this.db,
+      Stores.refresh,
+      refreshSessionId,
+    );
+    if (!refreshSession) {
+      return;
+    }
     const norevealIndex = refreshSession.norevealIndex;
     if (norevealIndex === undefined) {
       throw Error("can't reveal without melting first");
@@ -2706,6 +2717,13 @@ export class Wallet {
       this.db,
       [Stores.coins, Stores.refresh],
       async tx => {
+        const rs = await tx.get(Stores.refresh, refreshSessionId);
+        if (!rs) {
+          return;
+        }
+        if (rs.finished) {
+          return;
+        }
         for (let coin of coins) {
           await tx.put(Stores.coins, coin);
         }
@@ -2726,8 +2744,8 @@ export class Wallet {
    */
   async getHistory(
     historyQuery?: HistoryQuery,
-  ): Promise<{ history: HistoryRecord[] }> {
-    const history: HistoryRecord[] = [];
+  ): Promise<{ history: HistoryEvent[] }> {
+    const history: HistoryEvent[] = [];
 
     // FIXME: do pagination instead of generating the full history
 
@@ -2744,6 +2762,7 @@ export class Wallet {
         },
         timestamp: p.timestamp,
         type: "claim-order",
+        explicit: false,
       });
     }
 
@@ -2758,6 +2777,7 @@ export class Wallet {
         },
         timestamp: w.startTimestamp,
         type: "withdraw",
+        explicit: false,
       });
     }
 
@@ -2772,6 +2792,7 @@ export class Wallet {
         },
         timestamp: p.timestamp,
         type: "pay",
+        explicit: false,
       });
       if (p.timestamp_refund) {
         const contractAmount = Amounts.parseOrThrow(p.contractTerms.amount);
@@ -2796,6 +2817,7 @@ export class Wallet {
           },
           timestamp: p.timestamp_refund,
           type: "refund",
+          explicit: false,
         });
       }
     }
@@ -2803,24 +2825,31 @@ export class Wallet {
     const reserves = await oneShotIter(this.db, Stores.reserves).toArray();
 
     for (const r of reserves) {
+      const reserveType = r.bankWithdrawStatusUrl ? "taler-bank" : "manual";
       history.push({
         detail: {
-          exchangeBaseUrl: r.exchange_base_url,
-          requestedAmount: Amounts.toString(r.requested_amount),
-          reservePub: r.reserve_pub,
+          exchangeBaseUrl: r.exchangeBaseUrl,
+          requestedAmount: Amounts.toString(r.requestedAmount),
+          reservePub: r.reservePub,
+          reserveType,
+          bankWithdrawStatusUrl: r.bankWithdrawStatusUrl,
         },
         timestamp: r.created,
-        type: "create-reserve",
+        type: "reserve-created",
+        explicit: false,
       });
-      if (r.timestamp_depleted) {
+      if (r.timestampConfirmed) {
         history.push({
           detail: {
-            exchangeBaseUrl: r.exchange_base_url,
-            requestedAmount: r.requested_amount,
-            reservePub: r.reserve_pub,
+            exchangeBaseUrl: r.exchangeBaseUrl,
+            requestedAmount: Amounts.toString(r.requestedAmount),
+            reservePub: r.reservePub,
+            reserveType,
+            bankWithdrawStatusUrl: r.bankWithdrawStatusUrl,
           },
-          timestamp: r.timestamp_depleted,
-          type: "depleted-reserve",
+          timestamp: r.created,
+          type: "reserve-confirmed",
+          explicit: false,
         });
       }
     }
@@ -2835,11 +2864,23 @@ export class Wallet {
           tipId: tip.tipId,
         },
         timestamp: tip.timestamp,
+        explicit: false,
         type: "tip",
       });
     }
 
-    history.sort((h1, h2) => Math.sign(h1.timestamp - h2.timestamp));
+    await oneShotIter(this.db, Stores.exchanges).forEach(exchange => {
+      history.push({
+        type: "exchange-added",
+        explicit: false,
+        timestamp: exchange.timestampAdded,
+        detail: {
+          exchangeBaseUrl: exchange.baseUrl,
+        },
+      });
+    });
+
+    history.sort((h1, h2) => Math.sign(h1.timestamp.t_ms - h2.timestamp.t_ms));
 
     return { history };
   }
@@ -2849,7 +2890,17 @@ export class Wallet {
     const exchanges = await this.getExchanges();
     for (let e of exchanges) {
       switch (e.updateStatus) {
-        case ExchangeUpdateStatus.NONE:
+        case ExchangeUpdateStatus.FINISHED:
+          if (e.lastError) {
+            pendingOperations.push({
+              type: "bug",
+              message:
+                "Exchange record is in FINISHED state but has lastError set",
+              details: {
+                exchangeBaseUrl: e.baseUrl,
+              },
+            });
+          }
           if (!e.details) {
             pendingOperations.push({
               type: "bug",
@@ -2860,12 +2911,24 @@ export class Wallet {
               },
             });
           }
+          if (!e.wireInfo) {
+            pendingOperations.push({
+              type: "bug",
+              message:
+                "Exchange record does not have wire info, but no update in 
progress.",
+              details: {
+                exchangeBaseUrl: e.baseUrl,
+              },
+            });
+          }
           break;
         case ExchangeUpdateStatus.FETCH_KEYS:
           pendingOperations.push({
             type: "exchange-update",
             stage: "fetch-keys",
             exchangeBaseUrl: e.baseUrl,
+            lastError: e.lastError,
+            reason: e.updateReason || "unknown",
           });
           break;
         case ExchangeUpdateStatus.FETCH_WIRE:
@@ -2873,10 +2936,79 @@ export class Wallet {
             type: "exchange-update",
             stage: "fetch-wire",
             exchangeBaseUrl: e.baseUrl,
+            lastError: e.lastError,
+            reason: e.updateReason || "unknown",
+          });
+          break;
+        default:
+          pendingOperations.push({
+            type: "bug",
+            message: "Unknown exchangeUpdateStatus",
+            details: {
+              exchangeBaseUrl: e.baseUrl,
+              exchangeUpdateStatus: e.updateStatus,
+            },
           });
           break;
       }
     }
+    await oneShotIter(this.db, Stores.reserves).forEach(reserve => {
+      const reserveType = reserve.bankWithdrawStatusUrl
+        ? "taler-bank"
+        : "manual";
+      switch (reserve.reserveStatus) {
+        case ReserveRecordStatus.DORMANT:
+          // nothing to report as pending
+          break;
+        case ReserveRecordStatus.WITHDRAWING:
+        case ReserveRecordStatus.UNCONFIRMED:
+        case ReserveRecordStatus.QUERYING_STATUS:
+          pendingOperations.push({
+            type: "reserve",
+            stage: reserve.reserveStatus,
+            timestampCreated: reserve.created,
+            reserveType,
+          });
+          break;
+        default:
+          pendingOperations.push({
+            type: "bug",
+            message: "Unknown reserve record status",
+            details: {
+              reservePub: reserve.reservePub,
+              reserveStatus: reserve.reserveStatus,
+            },
+          });
+          break;
+      }
+    });
+
+    await oneShotIter(this.db, Stores.refresh).forEach(r => {
+      if (r.finished) {
+        return;
+      }
+      let refreshStatus: string;
+      if (r.norevealIndex === undefined) {
+        refreshStatus = "melt";
+      } else {
+        refreshStatus = "reveal";
+      }
+
+      pendingOperations.push({
+        type: "refresh",
+        oldCoinPub: r.meltCoinPub,
+        refreshStatus,
+        refreshOutputSize: r.newDenoms.length,
+      });
+    });
+
+    await oneShotIter(this.db, Stores.precoins).forEach(pc => {
+      pendingOperations.push({
+        type: "withdraw",
+        stage: "planchet",
+        reservePub: pc.reservePub,
+      });
+    });
     return {
       pendingOperations,
     };
@@ -2914,16 +3046,20 @@ export class Wallet {
 
   async getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> {
     return await oneShotIter(this.db, Stores.reserves).filter(
-      r => r.exchange_base_url === exchangeBaseUrl,
+      r => r.exchangeBaseUrl === exchangeBaseUrl,
     );
   }
 
-  async getCoins(exchangeBaseUrl: string): Promise<CoinRecord[]> {
+  async getCoinsForExchange(exchangeBaseUrl: string): Promise<CoinRecord[]> {
     return await oneShotIter(this.db, Stores.coins).filter(
       c => c.exchangeBaseUrl === exchangeBaseUrl,
     );
   }
 
+  async getCoins(): Promise<CoinRecord[]> {
+    return await oneShotIter(this.db, Stores.coins).toArray();
+  }
+
   async getPreCoins(exchangeBaseUrl: string): Promise<PreCoinRecord[]> {
     return await oneShotIter(this.db, Stores.precoins).filter(
       c => c.exchangeBaseUrl === exchangeBaseUrl,
@@ -2948,15 +3084,10 @@ export class Wallet {
       throw Error(`Reserve of coin ${coinPub} not found`);
     }
     switch (coin.status) {
-      case CoinStatus.Refreshed:
-        throw Error(
-          `Can't do payback for coin ${coinPub} since it's refreshed`,
-        );
-      case CoinStatus.PaybackDone:
-        console.log(`Coin ${coinPub} already payed back`);
-        return;
+      case CoinStatus.Dormant:
+        throw Error(`Can't do payback for coin ${coinPub} since it's dormant`);
     }
-    coin.status = CoinStatus.PaybackPending;
+    coin.status = CoinStatus.Dormant;
     // Even if we didn't get the payback yet, we suspend withdrawal, since
     // technically we might update reserve status before we get the response
     // from the reserve for the payback request.
@@ -2985,7 +3116,7 @@ export class Wallet {
     if (!coin) {
       throw Error(`Coin ${coinPub} not found, can't confirm payback`);
     }
-    coin.status = CoinStatus.PaybackDone;
+    coin.status = CoinStatus.Dormant;
     await oneShotPut(this.db, Stores.coins, coin);
     this.notifier.notify();
     await this.updateReserve(reservePub!);
@@ -3023,7 +3154,9 @@ export class Wallet {
     }
     reserve.hasPayback = false;
     await oneShotPut(this.db, Stores.reserves, reserve);
-    this.depleteReserve(reserve);
+    this.depleteReserve(reserve.reservePub).catch(e => {
+      console.error("Error depleting reserve after payback", e);
+    });
   }
 
   async getPaybackReserves(): Promise<ReserveRecord[]> {
@@ -3036,7 +3169,7 @@ export class Wallet {
    * Stop ongoing processing.
    */
   stop() {
-    this.timerGroup.stopCurrentAndFutureTimers();
+    //this.timerGroup.stopCurrentAndFutureTimers();
     this.cryptoApi.stop();
   }
 
@@ -3249,7 +3382,7 @@ export class Wallet {
         return;
       }
 
-      t.timestamp_refund = new Date().getTime();
+      t.timestamp_refund = getTimestampNow();
 
       for (const perm of refundPermissions) {
         if (
@@ -3444,25 +3577,10 @@ export class Wallet {
     return feeAcc;
   }
 
-  async acceptTip(talerTipUri: string): Promise<void> {
-    const { tipId, merchantOrigin } = await this.getTipStatus(talerTipUri);
-    const key = `${tipId}${merchantOrigin}`;
-    if (this.activeTipOperations[key]) {
-      return this.activeTipOperations[key];
-    }
-    const p = this.acceptTipImpl(tipId, merchantOrigin);
-    this.activeTipOperations[key] = p;
-    try {
-      return await p;
-    } finally {
-      delete this.activeTipOperations[key];
-    }
-  }
-
-  private async acceptTipImpl(
-    tipId: string,
-    merchantOrigin: string,
+async acceptTip(
+    talerTipUri: string,
   ): Promise<void> {
+    const { tipId, merchantOrigin } = await this.getTipStatus(talerTipUri);
     let tipRecord = await oneShotGet(this.db, Stores.tips, [
       tipId,
       merchantOrigin,
@@ -3603,7 +3721,7 @@ export class Wallet {
         pickedUp: false,
         planchets: undefined,
         response: undefined,
-        timestamp: new Date().getTime(),
+        timestamp: getTimestampNow(),
         tipId: res.tipId,
         pickupUrl: res.tipPickupUrl,
         totalFees: Amounts.add(
@@ -3732,7 +3850,6 @@ export class Wallet {
       senderWire: withdrawInfo.senderWire,
       exchangeWire: exchangeWire,
     });
-    await this.sendReserveInfoToBank(reserve.reservePub);
     return {
       reservePub: reserve.reservePub,
       confirmTransferUrl: withdrawInfo.confirmTransferUrl,
@@ -3767,7 +3884,7 @@ export class Wallet {
     const totalFees = totalRefundFees;
     return {
       contractTerms: purchase.contractTerms,
-      hasRefund: purchase.timestamp_refund !== 0,
+      hasRefund: purchase.timestamp_refund !== undefined,
       totalRefundAmount: totalRefundAmount,
       totalRefundAndRefreshFees: totalFees,
     };
diff --git a/src/walletTypes.ts b/src/walletTypes.ts
index b227ca81..a11da029 100644
--- a/src/walletTypes.ts
+++ b/src/walletTypes.ts
@@ -233,7 +233,7 @@ export interface ConfirmPayResult {
 /**
  * Activity history record.
  */
-export interface HistoryRecord {
+export interface HistoryEvent {
   /**
    * Type of the history event.
    */
@@ -242,7 +242,7 @@ export interface HistoryRecord {
   /**
    * Time when the activity was recorded.
    */
-  timestamp: number;
+  timestamp: Timestamp;
 
   /**
    * Subject of the entry.  Used to group multiple history records together.
@@ -254,6 +254,13 @@ export interface HistoryRecord {
    * Details used when rendering the history record.
    */
   detail: any;
+
+  /**
+   * Set to 'true' if the event has been explicitly created,
+   * and set to 'false' if the event has been derived from the
+   * state of the database.
+   */
+  explicit: boolean;
 }
 
 /**
@@ -516,6 +523,8 @@ export interface WalletDiagnostics {
 
 export interface PendingWithdrawOperation {
   type: "withdraw";
+  stage: string;
+  reservePub: string;
 }
 
 export interface PendingRefreshOperation {
@@ -535,6 +544,7 @@ export interface OperationError {
 export interface PendingExchangeUpdateOperation {
   type: "exchange-update";
   stage: string;
+  reason: string;
   exchangeBaseUrl: string;
   lastError?: OperationError;
 }
@@ -545,10 +555,28 @@ export interface PendingBugOperation {
   details: any;
 }
 
+export interface PendingReserveOperation {
+  type: "reserve";
+  lastError?: OperationError;
+  stage: string;
+  timestampCreated: Timestamp;
+  reserveType: string;
+}
+
+export interface PendingRefreshOperation {
+  type: "refresh";
+  lastError?: OperationError;
+  oldCoinPub: string;
+  refreshStatus: string;
+  refreshOutputSize: number;
+}
+
 export type PendingOperationInfo =
   | PendingWithdrawOperation
+  | PendingReserveOperation
   | PendingBugOperation
-  | PendingExchangeUpdateOperation;
+  | PendingExchangeUpdateOperation
+  | PendingRefreshOperation;
 
 export interface PendingOperationsResponse {
   pendingOperations: PendingOperationInfo[];
diff --git a/src/webex/messages.ts b/src/webex/messages.ts
index 3f6e5cc4..034bf284 100644
--- a/src/webex/messages.ts
+++ b/src/webex/messages.ts
@@ -79,7 +79,7 @@ export interface MessageMap {
   };
   "get-history": {
     request: {};
-    response: walletTypes.HistoryRecord[];
+    response: walletTypes.HistoryEvent[];
   };
   "get-coins": {
     request: { exchangeBaseUrl: string };
diff --git a/src/webex/pages/payback.tsx b/src/webex/pages/payback.tsx
index 934c28c0..af14b95d 100644
--- a/src/webex/pages/payback.tsx
+++ b/src/webex/pages/payback.tsx
@@ -57,11 +57,11 @@ function Payback() {
     <div>
       {reserves.map(r => (
         <div>
-          <h2>Reserve for ${renderAmount(r.current_amount!)}</h2>
+          <h2>Reserve for ${renderAmount(r.currentAmount!)}</h2>
           <ul>
-            <li>Exchange: ${r.exchange_base_url}</li>
+            <li>Exchange: ${r.exchangeBaseUrl}</li>
           </ul>
-          <button onClick={() => withdrawPaybackReserve(r.reserve_pub)}>
+          <button onClick={() => withdrawPaybackReserve(r.reservePub)}>
             Withdraw again
           </button>
         </div>
diff --git a/src/webex/pages/popup.tsx b/src/webex/pages/popup.tsx
index 20594547..78b7374b 100644
--- a/src/webex/pages/popup.tsx
+++ b/src/webex/pages/popup.tsx
@@ -30,7 +30,7 @@ import { AmountJson } from "../../amounts";
 import * as Amounts from "../../amounts";
 
 import {
-  HistoryRecord,
+  HistoryEvent,
   WalletBalance,
   WalletBalanceEntry,
 } from "../../walletTypes";
@@ -327,7 +327,7 @@ class WalletBalanceView extends React.Component<any, any> {
   }
 }
 
-function formatHistoryItem(historyItem: HistoryRecord) {
+function formatHistoryItem(historyItem: HistoryEvent) {
   const d = historyItem.detail;
   console.log("hist item", historyItem);
   switch (historyItem.type) {
@@ -459,7 +459,7 @@ class WalletHistory extends React.Component<any, any> {
 
   render(): JSX.Element {
     console.log("rendering history");
-    const history: HistoryRecord[] = this.myHistory;
+    const history: HistoryEvent[] = this.myHistory;
     if (this.gotError) {
       return i18n.str`Error: could not retrieve event history`;
     }
@@ -474,7 +474,7 @@ class WalletHistory extends React.Component<any, any> {
       const item = (
         <div className="historyItem">
           <div className="historyDate">
-            {new Date(record.timestamp).toString()}
+            {new Date(record.timestamp.t_ms).toString()}
           </div>
           {formatHistoryItem(record)}
         </div>
diff --git a/src/webex/renderHtml.tsx b/src/webex/renderHtml.tsx
index f2cccfba..c2fdb1f1 100644
--- a/src/webex/renderHtml.tsx
+++ b/src/webex/renderHtml.tsx
@@ -215,7 +215,7 @@ function FeeDetailsView(props: {
       <tbody>
         {rci!.wireFees.feesForType[s].map(f => (
           <tr>
-            <td>{moment.unix(f.endStamp).format("llll")}</td>
+            <td>{moment.unix(Math.floor(f.endStamp.t_ms / 
1000)).format("llll")}</td>
             <td>{renderAmount(f.wireFee)}</td>
             <td>{renderAmount(f.closingFee)}</td>
           </tr>
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index f4decbc6..57c10d94 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -176,7 +176,7 @@ async function handleMessage(
       if (typeof detail.exchangeBaseUrl !== "string") {
         return Promise.reject(Error("exchangBaseUrl missing"));
       }
-      return needsWallet().getCoins(detail.exchangeBaseUrl);
+      return needsWallet().getCoinsForExchange(detail.exchangeBaseUrl);
     }
     case "get-precoins": {
       if (typeof detail.exchangeBaseUrl !== "string") {
diff --git a/tsconfig.json b/tsconfig.json
index 25087b60..bcab91de 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -33,6 +33,8 @@
     "src/crypto/cryptoWorker.ts",
     "src/crypto/emscInterface-test.ts",
     "src/crypto/emscInterface.ts",
+    "src/crypto/nativeCrypto-test.ts",
+    "src/crypto/nativeCrypto.ts",
     "src/crypto/nodeEmscriptenLoader.ts",
     "src/crypto/nodeProcessWorker.ts",
     "src/crypto/nodeWorkerEntry.ts",
@@ -53,6 +55,7 @@
     "src/index.ts",
     "src/libtoolVersion-test.ts",
     "src/libtoolVersion.ts",
+    "src/logging.ts",
     "src/promiseUtils.ts",
     "src/query.ts",
     "src/talerTypes.ts",
diff --git a/yarn.lock b/yarn.lock
index 4c9012d4..aeec2b42 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3412,10 +3412,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.11:
-  version "0.0.11"
-  resolved 
"https://registry.yarnpkg.com/idb-bridge/-/idb-bridge-0.0.11.tgz#ba2fbd24b7e6f7f4de8333ed12b0912e64dda308";
-  integrity 
sha512-fLlHce/WwT6eD3sc54gsfvM5fZqrhAPwBNH4uU/y6D0C1+0higH7OgC5/wploMhkmNYkQID3BMNZvSUBr0leSQ==
+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==
 
 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]