gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (035b3fda -> c6233094)


From: gnunet
Subject: [taler-wallet-core] branch master updated (035b3fda -> c6233094)
Date: Thu, 21 Nov 2019 23:09:59 +0100

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

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

    from 035b3fda version upgrade and formatting
     new f797f16e improve error reporting
     new 5b43bd85 improve error messages
     new 61558e00 fix breakage due to URI.js upgrade
     new e8f362cc idb-bridge: fix deletion
     new c6233094 WIP: simplification and error handling

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


Summary of changes:
 package.json                             |    2 +-
 packages/idb-bridge/package.json         |    2 +-
 packages/idb-bridge/src/MemoryBackend.ts |   75 +-
 packages/idb-bridge/src/util/errors.ts   |   10 +
 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/helpers.ts                           |    6 +-
 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 +-
 24 files changed, 1177 insertions(+), 921 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/packages/idb-bridge/package.json b/packages/idb-bridge/package.json
index 7ac39726..6df8ab34 100644
--- a/packages/idb-bridge/package.json
+++ b/packages/idb-bridge/package.json
@@ -1,6 +1,6 @@
 {
   "name": "idb-bridge",
-  "version": "0.0.11",
+  "version": "0.0.14",
   "description": "IndexedDB implementation that uses SQLite3 as storage",
   "main": "./build/index.js",
   "types": "./build/index.d.ts",
diff --git a/packages/idb-bridge/src/MemoryBackend.ts 
b/packages/idb-bridge/src/MemoryBackend.ts
index bb13a022..cee06197 100644
--- a/packages/idb-bridge/src/MemoryBackend.ts
+++ b/packages/idb-bridge/src/MemoryBackend.ts
@@ -32,6 +32,7 @@ import {
   InvalidStateError,
   InvalidAccessError,
   ConstraintError,
+  DataError,
 } from "./util/errors";
 import BTree, { ISortedMapF } from "./tree/b+tree";
 import compareKeys from "./util/cmp";
@@ -870,14 +871,22 @@ export class MemoryBackend implements Backend {
     } else {
       currKey = range.lower;
       // We have a range with an lowerOpen lower bound, so don't start
-      // deleting the upper bound.  Instead start with the next higher key.
+      // deleting the lower bound.  Instead start with the next higher key.
       if (range.lowerOpen && currKey !== undefined) {
         currKey = modifiedData.nextHigherKey(currKey);
       }
     }
 
-    // invariant: (currKey is undefined) or (currKey is a valid key)
+    // make sure that currKey is either undefined or pointing to an
+    // existing object.
+    let firstValue = modifiedData.get(currKey);
+    if (!firstValue) {
+      if (currKey !== undefined) {
+        currKey = modifiedData.nextHigherKey(currKey);
+      }
+    }
 
+    // loop invariant: (currKey is undefined) or (currKey is a valid key)
     while (true) {
       if (currKey === undefined) {
         // nothing more to delete!
@@ -1328,14 +1337,36 @@ export class MemoryBackend implements Backend {
       key = storeReq.key;
       value = storeReq.value;
     } else {
-      const storeKeyResult: StoreKeyResult = makeStoreKeyValue(
-        storeReq.value,
-        storeReq.key,
+      const keygen =
         objectStoreMapEntry.store.modifiedKeyGenerator ||
-          objectStoreMapEntry.store.originalKeyGenerator,
-        schema.objectStores[storeReq.objectStoreName].autoIncrement,
-        schema.objectStores[storeReq.objectStoreName].keyPath,
-      );
+        objectStoreMapEntry.store.originalKeyGenerator;
+      const autoIncrement =
+        schema.objectStores[storeReq.objectStoreName].autoIncrement;
+      const keyPath = schema.objectStores[storeReq.objectStoreName].keyPath;
+      let storeKeyResult: StoreKeyResult;
+      try {
+        storeKeyResult = makeStoreKeyValue(
+          storeReq.value,
+          storeReq.key,
+          keygen,
+          autoIncrement,
+          keyPath,
+        );
+      } catch (e) {
+        if (e instanceof DataError) {
+          const kp = JSON.stringify(keyPath);
+          const n = storeReq.objectStoreName;
+          const m = `Could not extract key from value, objectStore=${n}, 
keyPath=${kp}`;
+          if (this.enableTracing) {
+            console.error(e);
+            console.error("value was:", storeReq.value);
+            console.error("key was:", storeReq.key);
+          }
+          throw new DataError(m);
+        } else  {
+          throw e;
+        }
+      }
       key = storeKeyResult.key;
       value = storeKeyResult.value;
       objectStoreMapEntry.store.modifiedKeyGenerator =
@@ -1386,11 +1417,27 @@ export class MemoryBackend implements Backend {
       );
     }
     let indexData = index.modifiedData || index.originalData;
-    const indexKeys = getIndexKeys(
-      value,
-      indexProperties.keyPath,
-      indexProperties.multiEntry,
-    );
+    let indexKeys;
+    try {
+      indexKeys = getIndexKeys(
+        value,
+        indexProperties.keyPath,
+        indexProperties.multiEntry,
+      );
+    } catch (e) {
+      if (e instanceof DataError) {
+        const n = index.modifiedName || index.originalName;
+        const p = JSON.stringify(indexProperties.keyPath);
+        const m = `Failed to extract index keys from index ${n} for keyPath 
${p}.`;
+        if (this.enableTracing) {
+          console.error(m);
+          console.error("value was", value);
+        }
+        throw new DataError(m);
+      } else {
+        throw e;
+      }
+    }
     for (const indexKey of indexKeys) {
       const existingRecord = indexData.get(indexKey);
       if (existingRecord) {
diff --git a/packages/idb-bridge/src/util/errors.ts 
b/packages/idb-bridge/src/util/errors.ts
index bbf9498c..92de2ea9 100644
--- a/packages/idb-bridge/src/util/errors.ts
+++ b/packages/idb-bridge/src/util/errors.ts
@@ -42,6 +42,7 @@ const messages = {
 export class AbortError extends Error {
     constructor(message = messages.AbortError) {
         super();
+        Object.setPrototypeOf(this, ConstraintError.prototype);
         this.name = "AbortError";
         this.message = message;
     }
@@ -50,6 +51,7 @@ export class AbortError extends Error {
 export class ConstraintError extends Error {
     constructor(message = messages.ConstraintError) {
         super();
+        Object.setPrototypeOf(this, ConstraintError.prototype);
         this.name = "ConstraintError";
         this.message = message;
     }
@@ -58,6 +60,7 @@ export class ConstraintError extends Error {
 export class DataCloneError extends Error {
     constructor(message = messages.DataCloneError) {
         super();
+        Object.setPrototypeOf(this, DataCloneError.prototype);
         this.name = "DataCloneError";
         this.message = message;
     }
@@ -66,6 +69,7 @@ export class DataCloneError extends Error {
 export class DataError extends Error {
     constructor(message = messages.DataError) {
         super();
+        Object.setPrototypeOf(this, DataError.prototype);
         this.name = "DataError";
         this.message = message;
     }
@@ -74,6 +78,7 @@ export class DataError extends Error {
 export class InvalidAccessError extends Error {
     constructor(message = messages.InvalidAccessError) {
         super();
+        Object.setPrototypeOf(this, InvalidAccessError.prototype);
         this.name = "InvalidAccessError";
         this.message = message;
     }
@@ -82,6 +87,7 @@ export class InvalidAccessError extends Error {
 export class InvalidStateError extends Error {
     constructor(message = messages.InvalidStateError) {
         super();
+        Object.setPrototypeOf(this, InvalidStateError.prototype);
         this.name = "InvalidStateError";
         this.message = message;
     }
@@ -90,6 +96,7 @@ export class InvalidStateError extends Error {
 export class NotFoundError extends Error {
     constructor(message = messages.NotFoundError) {
         super();
+        Object.setPrototypeOf(this, NotFoundError.prototype);
         this.name = "NotFoundError";
         this.message = message;
     }
@@ -98,6 +105,7 @@ export class NotFoundError extends Error {
 export class ReadOnlyError extends Error {
     constructor(message = messages.ReadOnlyError) {
         super();
+        Object.setPrototypeOf(this, ReadOnlyError.prototype);
         this.name = "ReadOnlyError";
         this.message = message;
     }
@@ -106,6 +114,7 @@ export class ReadOnlyError extends Error {
 export class TransactionInactiveError extends Error {
     constructor(message = messages.TransactionInactiveError) {
         super();
+        Object.setPrototypeOf(this, TransactionInactiveError.prototype);
         this.name = "TransactionInactiveError";
         this.message = message;
     }
@@ -114,6 +123,7 @@ export class TransactionInactiveError extends Error {
 export class VersionError extends Error {
     constructor(message = messages.VersionError) {
         super();
+        Object.setPrototypeOf(this, VersionError.prototype);
         this.name = "VersionError";
         this.message = message;
     }
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/helpers.ts b/src/helpers.ts
index a063db16..cfebf394 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -44,10 +44,10 @@ export function amountToPretty(amount: AmountJson): string {
  * See http://api.taler.net/wallet.html#general
  */
 export function canonicalizeBaseUrl(url: string) {
-  const x = new URI(url);
-  if (!x.protocol()) {
-    x.protocol("https");
+  if (!url.startsWith("http") && !url.startsWith("https")) {
+    url = "https://"; + url;
   }
+  const x = new URI(url);
   x.path(x.path() + "/").normalizePath();
   x.fragment("");
   x.query();
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]