gnunet-svn
[Top][All Lists]
Advanced

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

[GNUnet-SVN] [taler-wallet-webex] branch master updated (cfa1df73 -> 5f6


From: gnunet
Subject: [GNUnet-SVN] [taler-wallet-webex] branch master updated (cfa1df73 -> 5f62d83a)
Date: Thu, 01 Aug 2019 23:21:20 +0200

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

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

    from cfa1df73 add link signature to /refresh/reveal
     new 16ecbc9f headless wallet skeleton, type fixes
     new bcefbd7a idb-bridge: test cases, package structure and missing 
functionality
     new cc4e8ddc headless wallet WIP
     new 92b04858 idb: make put/add return the effective store key
     new 5f62d83a headless/android port, PoC for CLI / headless tests

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:
 .vscode/settings.json                              |   3 +-
 gulpfile.js                                        |   2 +-
 package.json                                       |   6 +-
 packages/idb-bridge/.vscode/settings.json          |   3 +
 packages/idb-bridge/package.json                   |   9 +-
 packages/idb-bridge/src/BridgeIDBCursor.ts         |  36 ++-
 .../idb-bridge/src/BridgeIDBCursorWithValue.ts     |  49 +--
 packages/idb-bridge/src/BridgeIDBFactory.ts        |   2 +-
 packages/idb-bridge/src/BridgeIDBIndex.ts          |   2 +-
 packages/idb-bridge/src/BridgeIDBKeyRange.ts       |  11 +-
 packages/idb-bridge/src/BridgeIDBObjectStore.ts    |  70 ++--
 packages/idb-bridge/src/BridgeIDBTransaction.ts    |  21 +-
 packages/idb-bridge/src/MemoryBackend.test.ts      |  57 ++++
 packages/idb-bridge/src/MemoryBackend.ts           | 218 +++++++++++--
 packages/idb-bridge/src/backend-interface.ts       |  17 +-
 packages/idb-bridge/src/index.ts                   |  60 ++++
 packages/idb-bridge/src/util/FakeEventTarget.ts    |   4 +-
 packages/idb-bridge/src/util/makeStoreKeyValue.ts  |   2 +-
 packages/idb-bridge/tsconfig.json                  |   2 +-
 src/checkable.ts                                   |  13 +-
 src/crypto/cryptoApi-test.ts                       |  24 +-
 src/crypto/cryptoApi.ts                            |  46 ++-
 src/crypto/cryptoWorker.ts                         |  21 +-
 src/crypto/emscInterface-test.ts                   |   8 +-
 src/crypto/emscInterface.ts                        |   3 +
 src/crypto/emscLoader.js                           |  36 ++-
 src/crypto/nodeWorker.ts                           |   1 +
 src/db.ts                                          | 122 +++++++
 src/headless/taler-wallet-cli.ts                   | 335 +++++++++++++++++++
 src/http.ts                                        |  10 +-
 src/logging.ts                                     |   2 +-
 src/promiseUtils.ts                                |  39 +++
 src/query.ts                                       | 290 +++++++++--------
 src/talerTypes.ts                                  |  29 +-
 src/timer.ts                                       |   2 +-
 src/wallet.ts                                      | 354 ++++++++++++---------
 src/walletTypes.ts                                 |   2 +-
 src/webex/pages/return-coins.tsx                   |   2 +-
 src/webex/wxBackend.ts                             | 142 +--------
 tsconfig.json                                      |   5 +-
 yarn.lock                                          |  56 ++--
 41 files changed, 1516 insertions(+), 600 deletions(-)
 create mode 100644 packages/idb-bridge/.vscode/settings.json
 create mode 100644 packages/idb-bridge/src/index.ts
 create mode 100644 src/db.ts
 create mode 100644 src/headless/taler-wallet-cli.ts
 create mode 100644 src/promiseUtils.ts

diff --git a/.vscode/settings.json b/.vscode/settings.json
index 565900b9..6482c5da 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -35,5 +35,6 @@
         "**/*.js.map": true
     },
     "tslint.enable": true,
-    "editor.wrappingIndent": "same"
+    "editor.wrappingIndent": "same",
+    "editor.tabSize": 2
 }
\ No newline at end of file
diff --git a/gulpfile.js b/gulpfile.js
index cbcd366e..22bcfe13 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -109,7 +109,7 @@ const tsBaseArgs = {
   noImplicitAny: true,
   allowJs: true,
   checkJs: true,
-  noUnusedLocals: true,
+  incremental: true,
 };
 
 
diff --git a/package.json b/package.json
index 90f4ec58..ae5e9452 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,6 @@
     "@types/react-dom": "^16.0.0",
     "ava": "^1.4.1",
     "awesome-typescript-loader": "^5.2.1",
-    "axios": "^0.18.0",
     "glob": "^7.1.1",
     "gulp": "^4.0.0",
     "gulp-gzip": "^1.2.0",
@@ -57,5 +56,10 @@
     "webpack-bundle-analyzer": "^3.0.2",
     "webpack-cli": "^3.1.0",
     "webpack-merge": "^4.1.0"
+  },
+  "dependencies": {
+    "axios": "^0.19.0",
+    "commander": "^2.20.0",
+    "source-map-support": "^0.5.12"
   }
 }
diff --git a/packages/idb-bridge/.vscode/settings.json 
b/packages/idb-bridge/.vscode/settings.json
new file mode 100644
index 00000000..ff30c446
--- /dev/null
+++ b/packages/idb-bridge/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+    "editor.tabSize": 2
+}
\ No newline at end of file
diff --git a/packages/idb-bridge/package.json b/packages/idb-bridge/package.json
index 52396bfe..940f1747 100644
--- a/packages/idb-bridge/package.json
+++ b/packages/idb-bridge/package.json
@@ -2,15 +2,14 @@
   "name": "idb-bridge",
   "version": "0.0.1",
   "description": "IndexedDB implementation that uses SQLite3 as storage",
-  "main": "index.js",
+  "main": "./build/index.js",
+  "types": "./build/index.d.ts",
   "author": "Florian Dold",
   "license": "AGPL-3.0-or-later",
   "private": false,
-  "dependencies": {
-    "sqlite3": "^4.0.8"
-  },
   "scripts": {
-    "test": "tsc && ava"
+    "test": "tsc && ava",
+    "build": "tsc"
   },
   "devDependencies": {
     "ava": "2.1.0",
diff --git a/packages/idb-bridge/src/BridgeIDBCursor.ts 
b/packages/idb-bridge/src/BridgeIDBCursor.ts
index ed5aa3e8..6c313908 100644
--- a/packages/idb-bridge/src/BridgeIDBCursor.ts
+++ b/packages/idb-bridge/src/BridgeIDBCursor.ts
@@ -42,12 +42,14 @@ import {
   Backend,
   DatabaseTransaction,
   RecordStoreRequest,
+  StoreLevel,
 } from "./backend-interface";
+import BridgeIDBFactory from "./BridgeIDBFactory";
 
 /**
  * http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#cursor
  */
-class BridgeIDBCursor {
+export class BridgeIDBCursor {
   _request: BridgeIDBRequest | undefined;
 
   private _gotValue: boolean = false;
@@ -119,14 +121,24 @@ class BridgeIDBCursor {
   get primaryKey() {
     return this._primaryKey;
   }
+
   set primaryKey(val) {
     /* For babel */
   }
 
+  protected get _isValueCursor(): boolean {
+    return false;
+  }
+
   /**
    * https://w3c.github.io/IndexedDB/#iterate-a-cursor
    */
   async _iterate(key?: Key, primaryKey?: Key): Promise<any> {
+    BridgeIDBFactory.enableTracing &&
+      console.log(
+        `iterating cursor os=${this._objectStoreName},idx=${this._indexName}`,
+      );
+    BridgeIDBFactory.enableTracing && console.log("cursor type ", 
this.toString());
     const recordGetRequest: RecordGetRequest = {
       direction: this.direction,
       indexName: this._indexName,
@@ -145,7 +157,10 @@ class BridgeIDBCursor {
     let response = await this._backend.getRecords(btx, recordGetRequest);
 
     if (response.count === 0) {
-      console.log("cursor is returning empty result");
+      if (BridgeIDBFactory.enableTracing) {
+        console.log("cursor is returning empty result");
+      }
+      this._gotValue = false;
       return null;
     }
 
@@ -153,8 +168,10 @@ class BridgeIDBCursor {
       throw Error("invariant failed");
     }
 
-    console.log("request is:", JSON.stringify(recordGetRequest));
-    console.log("get response is:", JSON.stringify(response));
+    if (BridgeIDBFactory.enableTracing) {
+      console.log("request is:", JSON.stringify(recordGetRequest));
+      console.log("get response is:", JSON.stringify(response));
+    }
 
     if (this._indexName !== undefined) {
       this._key = response.indexKeys![0];
@@ -204,20 +221,23 @@ class BridgeIDBCursor {
       throw new InvalidStateError();
     }
 
-    if (!this._gotValue || !this.hasOwnProperty("value")) {
+    if (!this._gotValue || !this._isValueCursor) {
       throw new InvalidStateError();
     }
 
     const storeReq: RecordStoreRequest = {
-      overwrite: true,
       key: this._primaryKey,
       value: value,
       objectStoreName: this._objectStoreName,
+      storeLevel: StoreLevel.UpdateExisting,
     };
 
     const operation = async () => {
+      if (BridgeIDBFactory.enableTracing) {
+        console.log("updating at cursor")
+      }
       const { btx } = this.source._confirmActiveTransaction();
-      this._backend.storeRecord(btx, storeReq);
+      await this._backend.storeRecord(btx, storeReq);
     };
     return transaction._execRequestAsync({
       operation,
@@ -318,7 +338,7 @@ class BridgeIDBCursor {
       throw new InvalidStateError();
     }
 
-    if (!this._gotValue || !this.hasOwnProperty("value")) {
+    if (!this._gotValue || !this._isValueCursor) {
       throw new InvalidStateError();
     }
 
diff --git a/packages/idb-bridge/src/BridgeIDBCursorWithValue.ts 
b/packages/idb-bridge/src/BridgeIDBCursorWithValue.ts
index b2f23147..d75bd21e 100644
--- a/packages/idb-bridge/src/BridgeIDBCursorWithValue.ts
+++ b/packages/idb-bridge/src/BridgeIDBCursorWithValue.ts
@@ -16,32 +16,35 @@
 
 import BridgeIDBCursor from "./BridgeIDBCursor";
 import {
-    CursorRange,
-    CursorSource,
-    BridgeIDBCursorDirection,
-    Value,
+  CursorRange,
+  CursorSource,
+  BridgeIDBCursorDirection,
+  Value,
 } from "./util/types";
 
 class BridgeIDBCursorWithValue extends BridgeIDBCursor {
-
-    get value(): Value {
-        return this._value;
-    }
-    
-    constructor(
-        source: CursorSource,
-        objectStoreName: string,
-        indexName: string | undefined,
-        range: CursorRange,
-        direction: BridgeIDBCursorDirection,
-        request?: any,
-    ) {
-        super(source, objectStoreName, indexName, range, direction, request, 
false);
-    }
-
-    public toString() {
-        return "[object IDBCursorWithValue]";
-    }
+  get value(): Value {
+    return this._value;
+  }
+
+  protected get _isValueCursor(): boolean {
+    return true;
+  }
+
+  constructor(
+    source: CursorSource,
+    objectStoreName: string,
+    indexName: string | undefined,
+    range: CursorRange,
+    direction: BridgeIDBCursorDirection,
+    request?: any,
+  ) {
+    super(source, objectStoreName, indexName, range, direction, request, 
false);
+  }
+
+  public toString() {
+    return "[object IDBCursorWithValue]";
+  }
 }
 
 export default BridgeIDBCursorWithValue;
diff --git a/packages/idb-bridge/src/BridgeIDBFactory.ts 
b/packages/idb-bridge/src/BridgeIDBFactory.ts
index ad02be46..f6234b49 100644
--- a/packages/idb-bridge/src/BridgeIDBFactory.ts
+++ b/packages/idb-bridge/src/BridgeIDBFactory.ts
@@ -27,7 +27,7 @@ import queueTask from "./util/queueTask";
 
 type DatabaseList = Array<{ name: string; version: number }>;
 
-class BridgeIDBFactory {
+export class BridgeIDBFactory {
   public cmp = compareKeys;
   private backend: Backend;
   private connections: BridgeIDBDatabase[] = [];
diff --git a/packages/idb-bridge/src/BridgeIDBIndex.ts 
b/packages/idb-bridge/src/BridgeIDBIndex.ts
index 8179be83..4d2022d3 100644
--- a/packages/idb-bridge/src/BridgeIDBIndex.ts
+++ b/packages/idb-bridge/src/BridgeIDBIndex.ts
@@ -50,7 +50,7 @@ const confirmActiveTransaction = (
 };
 
 // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#idl-def-IDBIndex
-class BridgeIDBIndex {
+export class BridgeIDBIndex {
   objectStore: BridgeIDBObjectStore;
 
   get _schema(): Schema {
diff --git a/packages/idb-bridge/src/BridgeIDBKeyRange.ts 
b/packages/idb-bridge/src/BridgeIDBKeyRange.ts
index 2b32c7e0..4055e092 100644
--- a/packages/idb-bridge/src/BridgeIDBKeyRange.ts
+++ b/packages/idb-bridge/src/BridgeIDBKeyRange.ts
@@ -113,21 +113,20 @@ class BridgeIDBKeyRange {
 
   static _valueToKeyRange(value: any, nullDisallowedFlag: boolean = false) {
     if (value instanceof BridgeIDBKeyRange) {
-        return value;
+      return value;
     }
 
     if (value === null || value === undefined) {
-        if (nullDisallowedFlag) {
-            throw new DataError();
-        }
-        return new BridgeIDBKeyRange(undefined, undefined, false, false);
+      if (nullDisallowedFlag) {
+        throw new DataError();
+      }
+      return new BridgeIDBKeyRange(undefined, undefined, false, false);
     }
 
     const key = valueToKey(value);
 
     return BridgeIDBKeyRange.only(key);
   }
-
 }
 
 export default BridgeIDBKeyRange;
diff --git a/packages/idb-bridge/src/BridgeIDBObjectStore.ts 
b/packages/idb-bridge/src/BridgeIDBObjectStore.ts
index eca4c198..b951463a 100644
--- a/packages/idb-bridge/src/BridgeIDBObjectStore.ts
+++ b/packages/idb-bridge/src/BridgeIDBObjectStore.ts
@@ -46,10 +46,10 @@ import {
   DatabaseTransaction,
   RecordGetRequest,
   ResultLevel,
+  StoreLevel,
 } from "./backend-interface";
 import BridgeIDBFactory from "./BridgeIDBFactory";
 
-
 // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#object-store
 class BridgeIDBObjectStore {
   _indexesCache: Map<string, BridgeIDBIndex> = new Map();
@@ -61,7 +61,9 @@ class BridgeIDBObjectStore {
   }
 
   get indexNames(): FakeDOMStringList {
-    return 
fakeDOMStringList(this._schema.objectStores[this._name].indexes).sort();
+    return fakeDOMStringList(
+      this._schema.objectStores[this._name].indexes,
+    ).sort();
   }
 
   get keyPath(): KeyPath | null {
@@ -111,7 +113,6 @@ class BridgeIDBObjectStore {
 
     let { btx } = this._confirmActiveTransaction();
 
-
     newName = String(newName);
 
     const oldName = this._name;
@@ -121,7 +122,9 @@ class BridgeIDBObjectStore {
     }
 
     this._backend.renameObjectStore(btx, oldName, newName);
-    this.transaction.db._schema = 
this._backend.getSchema(this._backendConnection);
+    this.transaction.db._schema = this._backend.getSchema(
+      this._backendConnection,
+    );
   }
 
   public _store(value: Value, key: Key | undefined, overwrite: boolean) {
@@ -133,12 +136,15 @@ class BridgeIDBObjectStore {
     }
     const operation = async () => {
       const { btx } = this._confirmActiveTransaction();
-      return this._backend.storeRecord(btx, {
+      const result = await this._backend.storeRecord(btx, {
         objectStoreName: this._name,
         key: key,
         value: value,
-        overwrite,
+        storeLevel: overwrite
+          ? StoreLevel.AllowOverwrite
+          : StoreLevel.NoOverwrite,
       });
+      return result.key;
     };
 
     return this.transaction._execRequestAsync({ operation, source: this });
@@ -158,7 +164,7 @@ class BridgeIDBObjectStore {
     return this._store(value, key, false);
   }
 
-  public delete(key: Key) {
+  public delete(key: Key | BridgeIDBKeyRange) {
     if (arguments.length === 0) {
       throw new TypeError();
     }
@@ -167,15 +173,19 @@ class BridgeIDBObjectStore {
       throw new ReadOnlyError();
     }
 
-    if (!(key instanceof BridgeIDBKeyRange)) {
-      key = valueToKey(key);
+    let keyRange: BridgeIDBKeyRange;
+
+    if (key instanceof BridgeIDBKeyRange) {
+      keyRange = key;
+    } else {
+      keyRange = BridgeIDBKeyRange.only(valueToKey(key));
     }
 
     const operation = async () => {
       const { btx } = this._confirmActiveTransaction();
-      return this._backend.deleteRecord(btx, this._name, key);
-    }
-      
+      return this._backend.deleteRecord(btx, this._name, keyRange);
+    };
+
     return this.transaction._execRequestAsync({
       operation,
       source: this,
@@ -183,12 +193,20 @@ class BridgeIDBObjectStore {
   }
 
   public get(key?: BridgeIDBKeyRange | Key) {
+    if (BridgeIDBFactory.enableTracing) {
+      console.log(`getting from object store ${this._name} key ${key}`);
+    }
+
     if (arguments.length === 0) {
       throw new TypeError();
     }
 
-    if (!(key instanceof BridgeIDBKeyRange)) {
-      key = valueToKey(key);
+    let keyRange: BridgeIDBKeyRange;
+
+    if (key instanceof BridgeIDBKeyRange) {
+      keyRange = key;
+    } else {
+      keyRange = BridgeIDBKeyRange.only(valueToKey(key));
     }
 
     const recordRequest: RecordGetRequest = {
@@ -199,16 +217,21 @@ class BridgeIDBObjectStore {
       direction: "next",
       limit: 1,
       resultLevel: ResultLevel.Full,
-      range: key,
+      range: keyRange,
     };
 
     const operation = async () => {
+      if (BridgeIDBFactory.enableTracing) {
+        console.log("running get operation:", recordRequest);
+      }
       const { btx } = this._confirmActiveTransaction();
-      const result = await this._backend.getRecords(
-        btx,
-        recordRequest,
-      );
-      if (result.count == 0) {
+      const result = await this._backend.getRecords(btx, recordRequest);
+
+      if (BridgeIDBFactory.enableTracing) {
+        console.log("get operation result count:", result.count);
+      }
+
+      if (result.count === 0) {
         return undefined;
       }
       const values = result.values;
@@ -247,7 +270,6 @@ class BridgeIDBObjectStore {
     range?: BridgeIDBKeyRange | Key,
     direction: BridgeIDBCursorDirection = "next",
   ) {
-
     if (range === null) {
       range = undefined;
     }
@@ -406,7 +428,6 @@ class BridgeIDBObjectStore {
 
   // 
http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBObjectStore-count-IDBRequest-any-key
   public count(key?: Key | BridgeIDBKeyRange) {
-
     if (key === null) {
       key = undefined;
     }
@@ -427,10 +448,7 @@ class BridgeIDBObjectStore {
 
     const operation = async () => {
       const { btx } = this._confirmActiveTransaction();
-      const result = await this._backend.getRecords(
-        btx,
-        recordGetRequest,
-      );
+      const result = await this._backend.getRecords(btx, recordGetRequest);
       return result.count;
     };
 
diff --git a/packages/idb-bridge/src/BridgeIDBTransaction.ts 
b/packages/idb-bridge/src/BridgeIDBTransaction.ts
index a9f0201d..250e2714 100644
--- a/packages/idb-bridge/src/BridgeIDBTransaction.ts
+++ b/packages/idb-bridge/src/BridgeIDBTransaction.ts
@@ -174,12 +174,12 @@ class BridgeIDBTransaction extends FakeEventTarget {
    */
   public async _start() {
     if (BridgeIDBFactory.enableTracing) {
-      console.log(`TRACE: IDBTransaction._start, ${this._requests.length} 
queued`);
+      console.log(
+        `TRACE: IDBTransaction._start, ${this._requests.length} queued`,
+      );
     }
     this._started = true;
 
-    console.log("beginning transaction");
-
     if (!this._backendTransaction) {
       this._backendTransaction = await this._backend.beginTransaction(
         this.db._backendConnection,
@@ -188,8 +188,6 @@ class BridgeIDBTransaction extends FakeEventTarget {
       );
     }
 
-    console.log("beginTransaction completed");
-
     // Remove from request queue - cursor ones will be added back if necessary 
by cursor.continue and such
     let operation;
     let request;
@@ -208,16 +206,17 @@ class BridgeIDBTransaction extends FakeEventTarget {
       if (!request.source) {
         // Special requests like indexes that just need to run some code, with 
error handling already built into
         // operation
-        console.log("running operation without source");
         await operation();
       } else {
-        console.log("running operation with source");
         let event;
         try {
+          BridgeIDBFactory.enableTracing &&
+            console.log("TRACE: running operation in transaction");
           const result = await operation();
-          if (BridgeIDBFactory.enableTracing) {
-            console.log("TRACE: tx operation finished with success");
-          }
+          BridgeIDBFactory.enableTracing &&
+            console.log(
+              "TRACE: operation in transaction finished with success",
+            );
           request.readyState = "done";
           request.result = result;
           request.error = undefined;
@@ -295,7 +294,7 @@ class BridgeIDBTransaction extends FakeEventTarget {
 
       if (!this.error) {
         if (BridgeIDBFactory.enableTracing) {
-          console.log("dispatching 'complete' event");
+          console.log("dispatching 'complete' event on transaction");
         }
         const event = new FakeEvent("complete");
         event.eventPath = [this, this.db];
diff --git a/packages/idb-bridge/src/MemoryBackend.test.ts 
b/packages/idb-bridge/src/MemoryBackend.test.ts
index c21c2d06..5ec818f5 100644
--- a/packages/idb-bridge/src/MemoryBackend.test.ts
+++ b/packages/idb-bridge/src/MemoryBackend.test.ts
@@ -235,3 +235,60 @@ test("Spec: Example 1 Part 3", async t => {
 
   t.pass();
 });
+
+
+test("simple deletion", async t => {
+  const backend = new MemoryBackend();
+  const idb = new BridgeIDBFactory(backend);
+
+  const request = idb.open("library");
+  request.onupgradeneeded = () => {
+    const db = request.result;
+    const store = db.createObjectStore("books", { keyPath: "isbn" });
+    const titleIndex = store.createIndex("by_title", "title", { unique: true 
});
+    const authorIndex = store.createIndex("by_author", "author");
+  };
+
+  const db: BridgeIDBDatabase = await promiseFromRequest(request);
+
+  t.is(db.name, "library");
+
+  const tx = db.transaction("books", "readwrite");
+  tx.oncomplete = () => {
+    console.log("oncomplete called");
+  };
+
+  const store = tx.objectStore("books");
+
+  store.put({ title: "Quarry Memories", author: "Fred", isbn: 123456 });
+  store.put({ title: "Water Buffaloes", author: "Fred", isbn: 234567 });
+  store.put({ title: "Bedrock Nights", author: "Barney", isbn: 345678 });
+
+  await promiseFromTransaction(tx);
+
+  const tx2 = db.transaction("books", "readwrite");
+
+  const store2 = tx2.objectStore("books");
+
+  const req1 = store2.get(234567);
+  await promiseFromRequest(req1);
+  t.is(req1.readyState, "done");
+  t.is(req1.result.author, "Fred");
+
+  store2.delete(123456);
+
+  const req2 = store2.get(123456);
+  await promiseFromRequest(req2);
+  t.is(req2.readyState, "done");
+  t.is(req2.result, undefined);
+
+  const req3 = store2.get(234567);
+  await promiseFromRequest(req3);
+  t.is(req3.readyState, "done");
+  t.is(req3.result.author, "Fred");
+
+  await promiseFromTransaction(tx2);
+
+  t.pass();
+});
+
diff --git a/packages/idb-bridge/src/MemoryBackend.ts 
b/packages/idb-bridge/src/MemoryBackend.ts
index bd9b8996..5e158749 100644
--- a/packages/idb-bridge/src/MemoryBackend.ts
+++ b/packages/idb-bridge/src/MemoryBackend.ts
@@ -8,6 +8,8 @@ import {
   RecordGetRequest,
   RecordGetResponse,
   ResultLevel,
+  StoreLevel,
+  RecordStoreResponse,
 } from "./backend-interface";
 import structuredClone from "./util/structuredClone";
 import {
@@ -655,10 +657,10 @@ export class MemoryBackend implements Backend {
   async deleteRecord(
     btx: DatabaseTransaction,
     objectStoreName: string,
-    range: import("./BridgeIDBKeyRange").default,
+    range: BridgeIDBKeyRange,
   ): Promise<void> {
     if (this.enableTracing) {
-      console.log(`TRACING: deleteRecord`);
+      console.log(`TRACING: deleteRecord from store ${objectStoreName}`);
     }
     const myConn = this.connectionsByTransaction[btx.transactionCookie];
     if (!myConn) {
@@ -671,7 +673,112 @@ export class MemoryBackend implements Backend {
     if (db.txLevel < TransactionLevel.Write) {
       throw Error("only allowed in write transaction");
     }
-    throw Error("not implemented");
+    if (typeof range !== "object") {
+      throw Error("deleteRecord got invalid range (must be object)");
+    }
+    if (!("lowerOpen" in range)) {
+      throw Error("deleteRecord got invalid range (sanity check failed, 
'lowerOpen' missing)");
+    }
+
+    const schema = myConn.modifiedSchema
+      ? myConn.modifiedSchema
+      : db.committedSchema;
+    const objectStore = myConn.objectStoreMap[objectStoreName];
+
+    if (!objectStore.modifiedData) {
+      objectStore.modifiedData = objectStore.originalData;
+    }
+
+    let modifiedData = objectStore.modifiedData;
+    let currKey: Key | undefined;
+
+    if (range.lower === undefined || range.lower === null) {
+      currKey = modifiedData.minKey();
+    } 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.
+      if (range.lowerOpen && currKey !== undefined) {
+       currKey = modifiedData.nextHigherKey(currKey);
+      }
+    }
+
+    // invariant: (currKey is undefined) or (currKey is a valid key)
+
+    while (true) {
+      if (currKey === undefined) {
+        // nothing more to delete!
+        break;
+      }
+      if (range.upper !== null && range.upper !== undefined) {
+        if (range.upperOpen && compareKeys(currKey, range.upper) === 0) {
+          // We have a range that's upperOpen, so stop before we delete the 
upper bound.
+          break;
+        }
+        if ((!range.upperOpen) && compareKeys(currKey, range.upper) > 0) {
+          // The upper range is inclusive, only stop if we're after the upper 
range.
+          break;
+        }
+      }
+
+      const storeEntry = modifiedData.get(currKey);
+      if (!storeEntry) {
+        throw Error("assertion failed");
+      }
+
+      for (const indexName of schema.objectStores[objectStoreName].indexes) {
+        const index = myConn.indexMap[indexName];
+        if (!index) {
+          throw Error("index referenced by object store does not exist");
+        }
+        const indexProperties = schema.indexes[indexName];
+        this.deleteFromIndex(index, storeEntry.primaryKey, storeEntry.value, 
indexProperties);
+      }
+
+      modifiedData = modifiedData.without(currKey);
+
+      currKey = modifiedData.nextHigherKey(currKey);
+    }
+
+    objectStore.modifiedData = modifiedData;
+  }
+
+  private deleteFromIndex(
+    index: Index,
+    primaryKey: Key,
+    value: Value,
+    indexProperties: IndexProperties,
+  ): void {
+    if (this.enableTracing) {
+      console.log(
+        `deleteFromIndex(${index.modifiedName || index.originalName})`,
+      );
+    }
+    if (value === undefined || value === null) {
+      throw Error("cannot delete null/undefined value from index");
+    }
+    let indexData = index.modifiedData || index.originalData;
+    const indexKeys = getIndexKeys(
+      value,
+      indexProperties.keyPath,
+      indexProperties.multiEntry,
+    );
+    for (const indexKey of indexKeys) {
+      const existingRecord = indexData.get(indexKey);
+      if (!existingRecord) {
+        throw Error("db inconsistent: expected index entry missing");
+      }
+      const newPrimaryKeys = existingRecord.primaryKeys.filter((x) => 
compareKeys(x, primaryKey) !== 0);
+      if (newPrimaryKeys.length === 0) {
+        index.originalData = indexData.without(indexKey);
+      } else {
+        const newIndexRecord = {
+          indexKey,
+          primaryKeys: newPrimaryKeys,
+        }
+        index.modifiedData = indexData.with(indexKey, newIndexRecord, true);
+      }
+    }
   }
 
   async getRecords(
@@ -705,6 +812,18 @@ export class MemoryBackend implements Backend {
       range = req.range;
     }
 
+    if (typeof range !== "object") {
+      throw Error(
+        "getRecords was given an invalid range (sanity check failed, not an 
object)",
+      );
+    }
+
+    if (!("lowerOpen" in range)) {
+      throw Error(
+        "getRecords was given an invalid range (sanity check failed, lowerOpen 
missing)",
+      );
+    }
+
     let numResults = 0;
     let indexKeys: Key[] = [];
     let primaryKeys: Key[] = [];
@@ -779,20 +898,21 @@ export class MemoryBackend implements Backend {
         compareKeys(indexEntry.indexKey, req.lastIndexPosition) === 0
       ) {
         let pos = forward ? 0 : indexEntry.primaryKeys.length - 1;
-        console.log("number of primary keys", indexEntry.primaryKeys.length);
-        console.log("start pos is", pos);
+        this.enableTracing &&
+          console.log("number of primary keys", indexEntry.primaryKeys.length);
+        this.enableTracing && console.log("start pos is", pos);
         // Advance past the lastObjectStorePosition
         do {
           const cmpResult = compareKeys(
             req.lastObjectStorePosition,
             indexEntry.primaryKeys[pos],
           );
-          console.log("cmp result is", cmpResult);
+          this.enableTracing && console.log("cmp result is", cmpResult);
           if ((forward && cmpResult < 0) || (!forward && cmpResult > 0)) {
             break;
           }
           pos += forward ? 1 : -1;
-          console.log("now pos is", pos);
+          this.enableTracing && console.log("now pos is", pos);
         } while (pos >= 0 && pos < indexEntry.primaryKeys.length);
 
         // Make sure we're at least at advancedPrimaryPos
@@ -815,8 +935,10 @@ export class MemoryBackend implements Backend {
         primkeySubPos = forward ? 0 : indexEntry.primaryKeys.length - 1;
       }
 
-      console.log("subPos=", primkeySubPos);
-      console.log("indexPos=", indexPos);
+      if (this.enableTracing) {
+        console.log("subPos=", primkeySubPos);
+        console.log("indexPos=", indexPos);
+      }
 
       while (1) {
         if (req.limit != 0 && numResults == req.limit) {
@@ -867,12 +989,16 @@ export class MemoryBackend implements Backend {
           }
         }
         if (!skip) {
-          console.log(`not skipping!, subPos=${primkeySubPos}`);
+          if (this.enableTracing) {
+            console.log(`not skipping!, subPos=${primkeySubPos}`);
+          }
           indexKeys.push(indexEntry.indexKey);
           primaryKeys.push(indexEntry.primaryKeys[primkeySubPos]);
           numResults++;
         } else {
-          console.log("skipping!");
+          if (this.enableTracing) {
+            console.log("skipping!");
+          }
         }
         primkeySubPos += forward ? 1 : -1;
       }
@@ -881,13 +1007,11 @@ export class MemoryBackend implements Backend {
       // if requested.
       if (req.resultLevel === ResultLevel.Full) {
         for (let i = 0; i < numResults; i++) {
-          console.log("getting value for index", i);
-          console.log("with key", primaryKeys[i]);
           const result = storeData.get(primaryKeys[i]);
           if (!result) {
             throw Error("invariant violated");
           }
-          values.push(result);
+          values.push(result.value);
         }
       }
     } else {
@@ -907,7 +1031,9 @@ export class MemoryBackend implements Backend {
         // Advance store position if we are either still at the last returned
         // store key, or if we are currently not on a key.
         const storeEntry = storeData.get(storePos);
-        console.log("store entry:", storeEntry);
+        if (this.enableTracing) {
+          console.log("store entry:", storeEntry);
+        }
         if (
           !storeEntry ||
           (req.lastObjectStorePosition !== undefined &&
@@ -917,7 +1043,9 @@ export class MemoryBackend implements Backend {
         }
       } else {
         storePos = forward ? storeData.minKey() : storeData.maxKey();
-        console.log("setting starting store store pos to", storePos);
+        if (this.enableTracing) {
+          console.log("setting starting store pos to", storePos);
+        }
       }
 
       while (1) {
@@ -942,7 +1070,7 @@ export class MemoryBackend implements Backend {
         }
 
         if (req.resultLevel >= ResultLevel.Full) {
-          values.push(res);
+          values.push(res.value);
         }
 
         numResults++;
@@ -967,7 +1095,7 @@ export class MemoryBackend implements Backend {
   async storeRecord(
     btx: DatabaseTransaction,
     storeReq: RecordStoreRequest,
-  ): Promise<void> {
+  ): Promise<RecordStoreResponse> {
     if (this.enableTracing) {
       console.log(`TRACING: storeRecord`);
     }
@@ -985,30 +1113,50 @@ export class MemoryBackend implements Backend {
     const schema = myConn.modifiedSchema
       ? myConn.modifiedSchema
       : db.committedSchema;
-
     const objectStore = myConn.objectStoreMap[storeReq.objectStoreName];
 
-    const storeKeyResult: StoreKeyResult = makeStoreKeyValue(
-      storeReq.value,
-      storeReq.key,
-      objectStore.modifiedKeyGenerator || objectStore.originalKeyGenerator,
-      schema.objectStores[storeReq.objectStoreName].autoIncrement,
-      schema.objectStores[storeReq.objectStoreName].keyPath,
-    );
-    let key = storeKeyResult.key;
-    let value = storeKeyResult.value;
-    objectStore.modifiedKeyGenerator = storeKeyResult.updatedKeyGenerator;
-
     if (!objectStore.modifiedData) {
       objectStore.modifiedData = objectStore.originalData;
     }
     const modifiedData = objectStore.modifiedData;
-    const hasKey = modifiedData.has(key);
-    if (hasKey && !storeReq.overwrite) {
-      throw Error("refusing to overwrite");
+
+    let key;
+    let value;
+
+    if (storeReq.storeLevel === StoreLevel.UpdateExisting) {
+      if (storeReq.key === null || storeReq.key === undefined) {
+        throw Error("invalid update request (key not given)");
+      }
+
+      if (!objectStore.modifiedData.has(storeReq.key)) {
+        throw Error("invalid update request (record does not exist)");
+      }
+      key = storeReq.key;
+      value = storeReq.value;
+    } else {
+      const storeKeyResult: StoreKeyResult = makeStoreKeyValue(
+        storeReq.value,
+        storeReq.key,
+        objectStore.modifiedKeyGenerator || objectStore.originalKeyGenerator,
+        schema.objectStores[storeReq.objectStoreName].autoIncrement,
+        schema.objectStores[storeReq.objectStoreName].keyPath,
+      );
+      key = storeKeyResult.key;
+      value = storeKeyResult.value;
+      objectStore.modifiedKeyGenerator = storeKeyResult.updatedKeyGenerator;
+      const hasKey = modifiedData.has(key);
+
+      if (hasKey && storeReq.storeLevel !== StoreLevel.AllowOverwrite) {
+        throw Error("refusing to overwrite");
+      }
     }
 
-    objectStore.modifiedData = modifiedData.with(key, value, true);
+    const objectStoreRecord: ObjectStoreRecord = {
+      primaryKey: key,
+      value: value,
+    };
+
+    objectStore.modifiedData = modifiedData.with(key, objectStoreRecord, true);
 
     for (const indexName of schema.objectStores[storeReq.objectStoreName]
       .indexes) {
@@ -1019,6 +1167,8 @@ export class MemoryBackend implements Backend {
       const indexProperties = schema.indexes[indexName];
       this.insertIntoIndex(index, key, value, indexProperties);
     }
+
+    return { key };
   }
 
   private insertIntoIndex(
diff --git a/packages/idb-bridge/src/backend-interface.ts 
b/packages/idb-bridge/src/backend-interface.ts
index ab093d9c..f4feac95 100644
--- a/packages/idb-bridge/src/backend-interface.ts
+++ b/packages/idb-bridge/src/backend-interface.ts
@@ -41,6 +41,12 @@ export enum ResultLevel {
   Full,
 }
 
+export enum StoreLevel {
+  NoOverwrite,
+  AllowOverwrite,
+  UpdateExisting,
+}
+
 export interface RecordGetRequest {
   direction: BridgeIDBCursorDirection;
   objectStoreName: string;
@@ -94,7 +100,14 @@ export interface RecordStoreRequest {
   objectStoreName: string;
   value: Value;
   key: Key | undefined;
-  overwrite: boolean;
+  storeLevel: StoreLevel;
+}
+
+export interface RecordStoreResponse {
+  /**
+   * Key that the record was stored under in the object store.
+   */
+  key: Key;
 }
 
 export interface Backend {
@@ -170,5 +183,5 @@ export interface Backend {
   storeRecord(
     btx: DatabaseTransaction,
     storeReq: RecordStoreRequest,
-  ): Promise<void>;
+  ): Promise<RecordStoreResponse>;
 }
diff --git a/packages/idb-bridge/src/index.ts b/packages/idb-bridge/src/index.ts
new file mode 100644
index 00000000..a6545874
--- /dev/null
+++ b/packages/idb-bridge/src/index.ts
@@ -0,0 +1,60 @@
+import { BridgeIDBFactory } from "./BridgeIDBFactory";
+import { BridgeIDBCursor } from "./BridgeIDBCursor";
+import { BridgeIDBIndex } from "./BridgeIDBIndex";
+import BridgeIDBDatabase from "./BridgeIDBDatabase";
+import BridgeIDBKeyRange from "./BridgeIDBKeyRange";
+import BridgeIDBObjectStore from "./BridgeIDBObjectStore";
+import BridgeIDBOpenDBRequest from "./BridgeIDBOpenDBRequest";
+import BridgeIDBRequest from "./BridgeIDBRequest";
+import BridgeIDBTransaction from "./BridgeIDBTransaction";
+import BridgeIDBVersionChangeEvent from "./BridgeIDBVersionChangeEvent";
+
+export { BridgeIDBFactory, BridgeIDBCursor };
+
+export { MemoryBackend } from "./MemoryBackend";
+
+// globalThis polyfill, see https://mathiasbynens.be/notes/globalthis
+(function() {
+  if (typeof globalThis === "object") return;
+  Object.defineProperty(Object.prototype, "__magic__", {
+    get: function() {
+      return this;
+    },
+    configurable: true, // This makes it possible to `delete` the getter later.
+  });
+  // @ts-ignore: polyfill magic
+  __magic__.globalThis = __magic__; // lolwat
+  // @ts-ignore: polyfill magic
+  delete Object.prototype.__magic__;
+})();
+
+/**
+ * Populate the global name space such that the given IndexedDB factory is made
+ * available globally.
+ */
+export function shimIndexedDB(factory: BridgeIDBFactory): void {
+  // @ts-ignore: shimming
+  globalThis.indexedDB = factory;
+  // @ts-ignore: shimming
+  globalThis.IDBCursor = BridgeIDBCursor;
+  // @ts-ignore: shimming
+  globalThis.IDBKeyRange = BridgeIDBKeyRange;
+  // @ts-ignore: shimming
+  globalThis.IDBDatabase = BridgeIDBDatabase;
+  // @ts-ignore: shimming
+  globalThis.IDBFactory = BridgeIDBFactory;
+  // @ts-ignore: shimming
+  globalThis.IDBIndex = BridgeIDBIndex;
+  // @ts-ignore: shimming
+  globalThis.IDBKeyRange = BridgeIDBKeyRange;
+  // @ts-ignore: shimming
+  globalThis.IDBObjectStore = BridgeIDBObjectStore;
+  // @ts-ignore: shimming
+  globalThis.IDBOpenDBRequest = BridgeIDBOpenDBRequest;
+  // @ts-ignore: shimming
+  globalThis.IDBRequest = BridgeIDBRequest;
+  // @ts-ignore: shimming
+  globalThis.IDBTransaction = BridgeIDBTransaction;
+  // @ts-ignore: shimming
+  globalThis.IDBVersionChangeEvent = BridgeIDBVersionChangeEvent;
+}
diff --git a/packages/idb-bridge/src/util/FakeEventTarget.ts 
b/packages/idb-bridge/src/util/FakeEventTarget.ts
index f20432df..025f21b4 100644
--- a/packages/idb-bridge/src/util/FakeEventTarget.ts
+++ b/packages/idb-bridge/src/util/FakeEventTarget.ts
@@ -54,7 +54,6 @@ const invokeEventListeners = (event: FakeEvent, obj: 
FakeEventTarget) => {
       continue;
     }
 
-    console.log(`invoking ${event.type} event listener`, listener);
     // @ts-ignore
     listener.callback.call(event.currentTarget, event);
   }
@@ -81,7 +80,6 @@ const invokeEventListeners = (event: FakeEvent, obj: 
FakeEventTarget) => {
       type: event.type,
     };
     if (!stopped(event, listener)) {
-      console.log(`invoking on${event.type} event listener`, listener);
       // @ts-ignore
       listener.callback.call(event.currentTarget, event);
     }
@@ -100,7 +98,7 @@ abstract class FakeEventTarget {
   public readonly onupgradeneeded: EventCallback | null | undefined;
   public readonly onversionchange: EventCallback | null | undefined;
 
-  static enableTracing: boolean = true;
+  static enableTracing: boolean = false;
 
   public addEventListener(
     type: EventType,
diff --git a/packages/idb-bridge/src/util/makeStoreKeyValue.ts 
b/packages/idb-bridge/src/util/makeStoreKeyValue.ts
index 4f45e0d8..845634ac 100644
--- a/packages/idb-bridge/src/util/makeStoreKeyValue.ts
+++ b/packages/idb-bridge/src/util/makeStoreKeyValue.ts
@@ -18,7 +18,7 @@ export function makeStoreKeyValue(
   autoIncrement: boolean,
   keyPath: KeyPath | null,
 ): StoreKeyResult {
-  const haveKey = key !== undefined && key !== null;
+  const haveKey = key !== null && key !== undefined;
   const haveKeyPath = keyPath !== null && keyPath !== undefined;
 
   // This models a decision table on (haveKey, haveKeyPath, autoIncrement)
diff --git a/packages/idb-bridge/tsconfig.json 
b/packages/idb-bridge/tsconfig.json
index 1b9102e4..d3a746e1 100644
--- a/packages/idb-bridge/tsconfig.json
+++ b/packages/idb-bridge/tsconfig.json
@@ -4,8 +4,8 @@
         "module": "commonjs",
         "target": "es5",
         "noImplicitAny": true,
-        "sourceMap": false,
         "outDir": "build",
+        "declaration": true,
         "noEmitOnError": true,
         "strict": true,
         "incremental": true,
diff --git a/src/checkable.ts b/src/checkable.ts
index a8cc3822..3c9fe5bc 100644
--- a/src/checkable.ts
+++ b/src/checkable.ts
@@ -60,10 +60,10 @@ export namespace Checkable {
     stringChecker?: (s: string) => boolean;
     valueProp?: any;
     optional?: boolean;
-    extraAllowed?: boolean;
   }
 
   interface CheckableInfo {
+    extraAllowed: boolean;
     props: Prop[];
   }
 
@@ -91,7 +91,7 @@ export namespace Checkable {
   function getCheckableInfo(target: any): CheckableInfo {
     let chk = target[checkableInfoSym] as CheckableInfo|undefined;
     if (!chk) {
-      chk = { props: [] };
+      chk = { props: [], extraAllowed: false };
       target[checkableInfoSym] = chk;
     }
     return chk;
@@ -188,7 +188,8 @@ export namespace Checkable {
       throw new SchemaError(
         `expected object for ${path.join(".")}, got ${typeof v} instead`);
     }
-    const props = type.prototype[checkableInfoSym].props;
+    const chk = type.prototype[checkableInfoSym];
+    const props = chk.props;
     const remainingPropNames = new Set(Object.getOwnPropertyNames(v));
     const obj = new type();
     for (const innerProp of props) {
@@ -207,7 +208,7 @@ export namespace Checkable {
         path.concat([innerProp.propertyKey]));
     }
 
-    if (!prop.extraAllowed && remainingPropNames.size !== 0) {
+    if (!chk.extraAllowed && remainingPropNames.size !== 0) {
       const err = `superfluous properties 
${JSON.stringify(Array.from(remainingPropNames.values()))} of ${typeName}`;
       throw new SchemaError(err);
     }
@@ -222,16 +223,16 @@ export namespace Checkable {
    */
   export function Class(opts: {extra?: boolean, validate?: boolean} = {}) {
     return (target: any) => {
+      const chk = getCheckableInfo(target.prototype);
+      chk.extraAllowed = !!opts.extra;
       target.checked = (v: any) => {
         const cv = checkValue(v, {
           checker: checkValue,
-          extraAllowed: !!opts.extra,
           propertyKey: "(root)",
           type: target,
         }, ["(root)"]);
         if (opts.validate) {
           if (typeof target.validate !== "function") {
-            console.error("target", target);
             throw Error("invalid Checkable annotion: validate method 
required");
           }
           // May throw exception
diff --git a/src/crypto/cryptoApi-test.ts b/src/crypto/cryptoApi-test.ts
index 24342a43..6d43e2e6 100644
--- a/src/crypto/cryptoApi-test.ts
+++ b/src/crypto/cryptoApi-test.ts
@@ -26,10 +26,12 @@ import {
 
 import { CryptoApi } from "./cryptoApi";
 
-const masterPub1: string = 
"CQQZ9DY3MZ1ARMN5K1VKDETS04Y2QCKMMCFHZSWJWWVN82BTTH00";
+const masterPub1: string =
+  "CQQZ9DY3MZ1ARMN5K1VKDETS04Y2QCKMMCFHZSWJWWVN82BTTH00";
 
 const denomValid1: DenominationRecord = {
-  denomPub: 
"51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30GHS84R3JHHP6GSM2D9Q6514CGT568R32C9J6CWM4DSH64TM4DSM851K0CA48CVKAC1P6H144C2160T46DHK8CVM4HJ274S38C1M6S338D9N6GWM8DT684T3JCT36S13EC9G88R3EGHQ8S0KJGSQ60SKGD216N33AGJ2651K2E9S60TMCD1N75244HHQ6X33EDJ570R3GGJ2651MACA38D130DA560VK4HHJ68WK2CA26GW3ECSH6D13EC9S88VK2GT66WVK8D9G750K0D9R8RRK4DHQ71332GHK8D23GE26710M2H9K6WVK8HJ38MVKEGA66N23AC9H88VKACT58MV3CCSJ6H1K4DT38GRK0C9M8N33CE1R60V4AHA38H1KECSH6S33JH9N8GRKGH1K68S36GH354520818CMG26C1H60R30C9
 [...]
+  denomPub:
+    
"51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30GHS84R3JHHP6GSM2D9Q6514CGT568R32C9J6CWM4DSH64TM4DSM851K0CA48CVKAC1P6H144C2160T46DHK8CVM4HJ274S38C1M6S338D9N6GWM8DT684T3JCT36S13EC9G88R3EGHQ8S0KJGSQ60SKGD216N33AGJ2651K2E9S60TMCD1N75244HHQ6X33EDJ570R3GGJ2651MACA38D130DA560VK4HHJ68WK2CA26GW3ECSH6D13EC9S88VK2GT66WVK8D9G750K0D9R8RRK4DHQ71332GHK8D23GE26710M2H9K6WVK8HJ38MVKEGA66N23AC9H88VKACT58MV3CCSJ6H1K4DT38GRK0C9M8N33CE1R60V4AHA38H1KECSH6S33JH9N8GRKGH1K68S36GH354520818CMG26C1H60R30C935452081
 [...]
   denomPubHash: "dummy",
   exchangeBaseUrl: "https://exchange.example.com/";,
   feeDeposit: {
@@ -53,7 +55,8 @@ const denomValid1: DenominationRecord = {
     value: 0,
   },
   isOffered: true,
-  masterSig: 
"CJFJCQ48Q45PSGJ5KY94N6M2TPARESM2E15BSPBD95YVVPEARAEQ6V6G4Z2XBMS0QM0F3Y9EYVP276FCS90EQ1578ZC8JHFBZ3NGP3G",
+  masterSig:
+    
"CJFJCQ48Q45PSGJ5KY94N6M2TPARESM2E15BSPBD95YVVPEARAEQ6V6G4Z2XBMS0QM0F3Y9EYVP276FCS90EQ1578ZC8JHFBZ3NGP3G",
   stampExpireDeposit: "/Date(1851580381)/",
   stampExpireLegal: "/Date(1567756381)/",
   stampExpireWithdraw: "/Date(2482300381)/",
@@ -69,24 +72,25 @@ const denomValid1: DenominationRecord = {
 const denomInvalid1 = JSON.parse(JSON.stringify(denomValid1));
 denomInvalid1.value.value += 1;
 
-test("string hashing", async (t) => {
+test("string hashing", async t => {
   const crypto = new CryptoApi();
   const s = await crypto.hashString("hello taler");
-  const sh = 
"8RDMADB3YNF3QZBS3V467YZVJAMC2QAQX0TZGVZ6Q5PFRRAJFT70HHN0QF661QR9QWKYMMC7YEMPD679D2RADXCYK8Y669A2A5MKQFR";
+  const sh =
+    
"8RDMADB3YNF3QZBS3V467YZVJAMC2QAQX0TZGVZ6Q5PFRRAJFT70HHN0QF661QR9QWKYMMC7YEMPD679D2RADXCYK8Y669A2A5MKQFR";
   t.true(s === sh);
   t.pass();
 });
 
-test("precoin creation", async (t) => {
+test("precoin creation", async t => {
   const crypto = new CryptoApi();
-  const {priv, pub} = await crypto.createEddsaKeypair();
+  const { priv, pub } = await crypto.createEddsaKeypair();
   const r: ReserveRecord = {
     created: 0,
     current_amount: null,
     exchange_base_url: "https://example.com/exchange";,
     hasPayback: false,
-    precoin_amount: {currency: "PUDOS", value: 0, fraction: 0},
-    requested_amount: {currency: "PUDOS", value: 0, fraction: 0},
+    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,
@@ -98,7 +102,7 @@ test("precoin creation", async (t) => {
   t.pass();
 });
 
-test("denom validation", async (t) => {
+test("denom validation", async t => {
   const crypto = new CryptoApi();
   let v: boolean;
   v = await crypto.isValidDenom(denomValid1, masterPub1);
diff --git a/src/crypto/cryptoApi.ts b/src/crypto/cryptoApi.ts
index 43a3bc22..5e323783 100644
--- a/src/crypto/cryptoApi.ts
+++ b/src/crypto/cryptoApi.ts
@@ -41,6 +41,7 @@ import { BenchmarkResult, CoinWithDenom, PayCoinInfo } from 
"../walletTypes";
 import * as timer from "../timer";
 
 import { startWorker } from "./startWorker";
+import { throws } from "assert";
 
 /**
  * State of a crypto worker.
@@ -99,9 +100,46 @@ export class CryptoApi {
   private numBusy: number = 0;
 
   /**
+   * Did we stop accepting new requests?
+   */
+  private stopped: boolean = false;
+
+  public enableTracing = false;
+
+  /**
+   * Terminate all worker threads.
+   */
+  terminateWorkers() {
+    for (let worker of this.workers) {
+      if (worker.w) {
+        this.enableTracing && console.log("terminating worker");
+        worker.w.terminate();
+        if (worker.terminationTimerHandle) {
+          worker.terminationTimerHandle.clear();
+          worker.terminationTimerHandle = null;
+        }
+        if (worker.currentWorkItem) {
+          worker.currentWorkItem.reject(Error("explicitly terminated"));
+          worker.currentWorkItem = null;
+        }
+        worker.w = null;
+      }
+    }
+  }
+
+  stop() {
+    this.terminateWorkers();
+    this.stopped = true;
+  }
+
+  /**
    * Start a worker (if not started) and set as busy.
    */
   wake(ws: WorkerState, work: WorkItem): void {
+    if (this.stopped) {
+      this.enableTracing && console.log("not waking, as cryptoApi is stopped");
+      return;
+    }
     if (ws.currentWorkItem !== null) {
       throw Error("assertion failed");
     }
@@ -136,7 +174,7 @@ export class CryptoApi {
         ws.w = null;
       }
     };
-    ws.terminationTimerHandle = timer.after(20 * 1000, destroy);
+    ws.terminationTimerHandle = timer.after(15 * 1000, destroy);
   }
 
   handleWorkerError(ws: WorkerState, e: ErrorEvent) {
@@ -163,7 +201,7 @@ export class CryptoApi {
     this.findWork(ws);
   }
 
-  findWork(ws: WorkerState) {
+  private findWork(ws: WorkerState) {
     // try to find more work for this worker
     for (let i = 0; i < NUM_PRIO; i++) {
       const q = this.workQueues[NUM_PRIO - i - 1];
@@ -193,7 +231,8 @@ export class CryptoApi {
       console.error(`RPC with id ${id} has no registry entry`);
       return;
     }
-    console.log(
+
+    this.enableTracing && console.log(
       `rpc ${currentWorkItem.operation} took ${timer.performanceNow() -
         currentWorkItem.startTime}ms`,
     );
@@ -230,6 +269,7 @@ export class CryptoApi {
     priority: number,
     ...args: any[]
   ): Promise<T> {
+    this.enableTracing && console.log("cryptoApi: doRpc called");
     const p: Promise<T> = new Promise<T>((resolve, reject) => {
       const rpcId = this.nextRpcId++;
       const workItem: WorkItem = {
diff --git a/src/crypto/cryptoWorker.ts b/src/crypto/cryptoWorker.ts
index 9c5263a6..5acda905 100644
--- a/src/crypto/cryptoWorker.ts
+++ b/src/crypto/cryptoWorker.ts
@@ -56,6 +56,9 @@ import {
 import * as native from "./emscInterface";
 
 namespace RpcFunctions {
+
+  export let enableTracing: boolean = false;
+
   /**
    * Create a pre-coin of the given denomination to be withdrawn from then 
given
    * reserve.
@@ -735,19 +738,25 @@ worker.onmessage = (msg: MessageEvent) => {
     return;
   }
 
-  console.log("onmessage with", msg.data.operation);
-  console.log("foo");
+  if (RpcFunctions.enableTracing) {
+    console.log("onmessage with", msg.data.operation);
+  }
 
   emscLoader.getLib().then(p => {
     const lib = p.lib;
     if (!native.isInitialized()) {
-      console.log("initializing emscripten for then first time with lib");
+      if (RpcFunctions.enableTracing) {
+        console.log("initializing emscripten for then first time with lib");
+      }
       native.initialize(lib);
     }
-
-    console.log("about to execute", msg.data.operation);
+    if (RpcFunctions.enableTracing) {
+      console.log("about to execute", msg.data.operation);
+    }
     const res = f(...msg.data.args);
-    console.log("finished executing", msg.data.operation);
+    if (RpcFunctions.enableTracing) {
+      console.log("finished executing", msg.data.operation);
+    }
     worker.postMessage({ result: res, id: msg.data.id });
   });
 };
diff --git a/src/crypto/emscInterface-test.ts b/src/crypto/emscInterface-test.ts
index 58d83e6f..305e50ff 100644
--- a/src/crypto/emscInterface-test.ts
+++ b/src/crypto/emscInterface-test.ts
@@ -17,8 +17,14 @@
 // tslint:disable:max-line-length
 
 import test from "ava";
+import * as emscLoader from "./emscLoader";
 import * as native from "./emscInterface";
 
+test.before(async () => {
+  const { lib } = await emscLoader.getLib();
+  native.initialize(lib);
+});
+
 test("string hashing", (t) => {
   const x = native.ByteArray.fromStringWithNull("hello taler");
   const h = 
"8RDMADB3YNF3QZBS3V467YZVJAMC2QAQX0TZGVZ6Q5PFRRAJFT70HHN0QF661QR9QWKYMMC7YEMPD679D2RADXCYK8Y669A2A5MKQFR";
@@ -99,7 +105,7 @@ test("withdraw-request", (t) => {
 });
 
 
-test("withdraw-request", (t) => {
+test("currency-conversion", (t) => {
   const a1 = new native.Amount({currency: "KUDOS", value: 1, fraction: 
50000000});
   const a2 = new native.Amount({currency: "KUDOS", value: 1, fraction: 
50000000});
   a1.add(a2);
diff --git a/src/crypto/emscInterface.ts b/src/crypto/emscInterface.ts
index 2ddc15a3..70a85cda 100644
--- a/src/crypto/emscInterface.ts
+++ b/src/crypto/emscInterface.ts
@@ -43,6 +43,9 @@ export function initialize(lib: EmscLib) {
   if (!lib) {
     throw Error("library must be object");
   }
+  if (!lib.ccall) {
+    throw Error("sanity check failed: EmscLib does not have 'ccall'");
+  }
   if (maybeEmscEnv) {
     throw Error("emsc lib already initialized");
   }
diff --git a/src/crypto/emscLoader.js b/src/crypto/emscLoader.js
index 59437da4..25dc6b85 100644
--- a/src/crypto/emscLoader.js
+++ b/src/crypto/emscLoader.js
@@ -25,20 +25,29 @@
  */
 
 let cachedLib = undefined;
+let cachedLibPromise = undefined;
+
+export let enableTracing = false;
 
 /**
  * Load the taler emscripten lib.
  *
  * If in a WebWorker, importScripts is used.  Inside a browser, the module must
  * be globally available.  Inside node, require is used.
+ * 
+ * Returns a Promise<{ lib: EmscLib }>
  */
 export function getLib() {
-  console.log("in getLib");
+  enableTracing && console.log("in getLib");
   if (cachedLib) {
-    console.log("lib is cached");
+    enableTracing && console.log("lib is cached");
     return Promise.resolve({ lib: cachedLib });
   }
+  if (cachedLibPromise) {
+    return cachedLibPromise;
+  }
   if (typeof require !== "undefined") {
+    enableTracing && console.log("trying to load emscripten lib with 
'require'");
     // Make sure that TypeScript doesn't try
     // to check the taler-emscripten-lib.
     const indirectRequire = require;
@@ -49,17 +58,30 @@ export function getLib() {
     const savedImportScripts = g.importScripts;
     delete g.importScripts;
     // Assume that the code is run from the build/ directory.
-    const lib = indirectRequire("../../../emscripten/taler-emscripten-lib.js");
+    const libFn = 
indirectRequire("../../../emscripten/taler-emscripten-lib.js");
+    const lib = libFn();
     g.importScripts = savedImportScripts;
     if (lib) {
-      cachedLib = lib;
-      return Promise.resolve({ lib: cachedLib });
+      if (!lib.ccall) {
+        throw Error("sanity check failed: taler-emscripten lib does not have 
'ccall'");
+      }
+      cachedLibPromise = new Promise((resolve, reject) => {
+        lib.onRuntimeInitialized = () => {
+          cachedLib = lib;
+          cachedLibPromise = undefined;
+          resolve({ lib: cachedLib });
+        };
+      });
+      return cachedLibPromise;
+    } else {
+      // When we're running as a webpack bundle, the above require might
+      // have failed and returned 'undefined', so we try other ways to import.
+      console.log("failed to load emscripten lib with 'require', trying 
alternatives"); 
     }
-    // When we're running as a webpack bundle, the above require might
-    // have failed and returned 'undefined', so we try other ways to import.
   }
 
   if (typeof importScripts !== "undefined") {
+    console.log("trying to load emscripten lib with 'importScripts'");
     self.TalerEmscriptenLib = {};
     importScripts('/emscripten/taler-emscripten-lib.js')
     if (!self.TalerEmscriptenLib) {
diff --git a/src/crypto/nodeWorker.ts b/src/crypto/nodeWorker.ts
index fa942387..b5a2e8b4 100644
--- a/src/crypto/nodeWorker.ts
+++ b/src/crypto/nodeWorker.ts
@@ -92,6 +92,7 @@ export class Worker {
    * Forcibly terminate the worker thread.
    */
   terminate () {
+    console.log("terminating node.js worker");
     this.child.kill("SIGINT");
   }
 }
diff --git a/src/db.ts b/src/db.ts
new file mode 100644
index 00000000..0916ef14
--- /dev/null
+++ b/src/db.ts
@@ -0,0 +1,122 @@
+import { Stores, WALLET_DB_VERSION } from "./dbTypes";
+import { Store, Index } from "./query";
+
+const DB_NAME = "taler";
+
+/**
+ * Return a promise that resolves
+ * to the taler wallet db.
+ */
+export function openTalerDb(
+  idbFactory: IDBFactory,
+  onVersionChange: () => void,
+  onUpgradeUnsupported: (oldVersion: number, newVersion: number) => void,
+): Promise<IDBDatabase> {
+  return new Promise<IDBDatabase>((resolve, reject) => {
+    const req = idbFactory.open(DB_NAME, WALLET_DB_VERSION);
+    req.onerror = e => {
+      console.log("taler database error", e);
+      reject(e);
+    };
+    req.onsuccess = e => {
+      req.result.onversionchange = (evt: IDBVersionChangeEvent) => {
+        console.log(
+          `handling live db version change from ${evt.oldVersion} to ${
+            evt.newVersion
+          }`,
+        );
+        req.result.close();
+        onVersionChange();
+      };
+      resolve(req.result);
+    };
+    req.onupgradeneeded = e => {
+      const db = req.result;
+      console.log(
+        `DB: upgrade needed: oldVersion=${e.oldVersion}, newVersion=${
+          e.newVersion
+        }`,
+      );
+      switch (e.oldVersion) {
+        case 0: // DB does not exist yet
+          for (const n in Stores) {
+            if ((Stores as any)[n] instanceof Store) {
+              const si: Store<any> = (Stores as any)[n];
+              const s = db.createObjectStore(si.name, si.storeParams);
+              for (const indexName in si as any) {
+                if ((si as any)[indexName] instanceof Index) {
+                  const ii: Index<any, any> = (si as any)[indexName];
+                  s.createIndex(ii.indexName, ii.keyPath, ii.options);
+                }
+              }
+            }
+          }
+          break;
+        default:
+          if (e.oldVersion !== WALLET_DB_VERSION) {
+            onUpgradeUnsupported(e.oldVersion, WALLET_DB_VERSION);
+            throw Error("incompatible DB");
+          }
+          break;
+      }
+    };
+  });
+}
+
+export function exportDb(db: IDBDatabase): Promise<any> {
+  const dump = {
+    name: db.name,
+    stores: {} as { [s: string]: any },
+    version: db.version,
+  };
+
+  return new Promise((resolve, reject) => {
+    const tx = db.transaction(Array.from(db.objectStoreNames));
+    tx.addEventListener("complete", () => {
+      resolve(dump);
+    });
+    // tslint:disable-next-line:prefer-for-of
+    for (let i = 0; i < db.objectStoreNames.length; i++) {
+      const name = db.objectStoreNames[i];
+      const storeDump = {} as { [s: string]: any };
+      dump.stores[name] = storeDump;
+      tx.objectStore(name)
+        .openCursor()
+        .addEventListener("success", (e: Event) => {
+          const cursor = (e.target as any).result;
+          if (cursor) {
+            storeDump[cursor.key] = cursor.value;
+            cursor.continue();
+          }
+        });
+    }
+  });
+}
+
+export function importDb(db: IDBDatabase, dump: any): Promise<void> {
+  console.log("importing db", dump);
+  return new Promise<void>((resolve, reject) => {
+    const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite");
+    if (dump.stores) {
+      for (const storeName in dump.stores) {
+        const objects = [];
+        const dumpStore = dump.stores[storeName];
+        for (const key in dumpStore) {
+          objects.push(dumpStore[key]);
+        }
+        console.log(`importing ${objects.length} records into ${storeName}`);
+        const store = tx.objectStore(storeName);
+        for (const obj of objects) {
+          store.put(obj);
+        }
+      }
+    }
+    tx.addEventListener("complete", () => {
+      resolve();
+    });
+  });
+}
+
+export function deleteDb(idbFactory: IDBFactory) {
+  idbFactory.deleteDatabase(DB_NAME);
+}
diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
new file mode 100644
index 00000000..e2b8b54a
--- /dev/null
+++ b/src/headless/taler-wallet-cli.ts
@@ -0,0 +1,335 @@
+import { MemoryBackend, BridgeIDBFactory, shimIndexedDB } from "idb-bridge";
+import { Wallet } from "../wallet";
+import { Notifier, Badge } from "../walletTypes";
+import { openTalerDb, exportDb } from "../db";
+import { HttpRequestLibrary } from "../http";
+import * as amounts from "../amounts";
+import Axios from "axios";
+
+import URI = require("urijs");
+
+import querystring = require("querystring");
+import { CheckPaymentResponse } from "../talerTypes";
+
+const enableTracing = false;
+
+class ConsoleNotifier implements Notifier {
+  notify(): void {
+    // nothing to do.
+  }
+}
+
+class ConsoleBadge implements Badge {
+  startBusy(): void {
+    enableTracing && console.log("NOTIFICATION: busy");
+  }
+  stopBusy(): void {
+    enableTracing && console.log("NOTIFICATION: busy end");
+  }
+  showNotification(): void {
+    enableTracing && console.log("NOTIFICATION: show");
+  }
+  clearNotification(): void {
+    enableTracing && console.log("NOTIFICATION: cleared");
+  }
+}
+
+export class NodeHttpLib implements HttpRequestLibrary {
+  async get(url: string): Promise<import("../http").HttpResponse> {
+    enableTracing && console.log("making GET request to", url);
+    const resp = await Axios({
+      method: "get",
+      url: url,
+      responseType: "json",
+    });
+    enableTracing && console.log("got response", resp.data);
+    enableTracing && console.log("resp type", typeof resp.data);
+    return {
+      responseJson: resp.data,
+      status: resp.status,
+    };
+  }
+
+  async postJson(
+    url: string,
+    body: any,
+  ): Promise<import("../http").HttpResponse> {
+    enableTracing && console.log("making POST request to", url);
+    const resp = await Axios({
+      method: "post",
+      url: url,
+      responseType: "json",
+      data: body,
+    });
+    enableTracing && console.log("got response", resp.data);
+    enableTracing && console.log("resp type", typeof resp.data);
+    return {
+      responseJson: resp.data,
+      status: resp.status,
+    };
+  }
+
+  async postForm(
+    url: string,
+    form: any,
+  ): Promise<import("../http").HttpResponse> {
+    enableTracing && console.log("making POST request to", url);
+    const resp = await Axios({
+      method: "post",
+      url: url,
+      data: querystring.stringify(form),
+      responseType: "json",
+    });
+    enableTracing && console.log("got response", resp.data);
+    enableTracing && console.log("resp type", typeof resp.data);
+    return {
+      responseJson: resp.data,
+      status: resp.status,
+    };
+  }
+}
+
+interface BankUser {
+  username: string;
+  password: string;
+}
+
+function makeId(length: number): string {
+  let result = "";
+  const characters =
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+  for (let i = 0; i < length; i++) {
+    result += characters.charAt(Math.floor(Math.random() * characters.length));
+  }
+  return result;
+}
+
+async function registerBankUser(
+  bankBaseUrl: string,
+  httpLib: HttpRequestLibrary,
+): Promise<BankUser> {
+  const reqUrl = new URI("register").absoluteTo(bankBaseUrl).href();
+  const randId = makeId(8);
+  const bankUser: BankUser = {
+    username: `testuser-${randId}`,
+    password: `testpw-${randId}`,
+  };
+  const result = await httpLib.postForm(reqUrl, bankUser);
+  if (result.status != 200) {
+    throw Error("could not register bank user");
+  }
+  return bankUser;
+}
+
+async function createBankReserve(
+  bankBaseUrl: string,
+  bankUser: BankUser,
+  amount: string,
+  reservePub: string,
+  exchangePaytoUri: string,
+  httpLib: HttpRequestLibrary,
+) {
+  const reqUrl = new URI("taler/withdraw").absoluteTo(bankBaseUrl).href();
+
+  const body = {
+    auth: { type: "basic" },
+    username: bankUser,
+    amount,
+    reserve_pub: reservePub,
+    exchange_wire_detail: exchangePaytoUri,
+  };
+
+  const resp = await Axios({
+    method: "post",
+    url: reqUrl,
+    data: body,
+    responseType: "json",
+    headers: {
+      "X-Taler-Bank-Username": bankUser.username,
+      "X-Taler-Bank-Password": bankUser.password,
+    },
+  });
+
+  if (resp.status != 200) {
+    throw Error("failed to create bank reserve");
+  }
+}
+
+class MerchantBackendConnection {
+  constructor(
+    public merchantBaseUrl: string,
+    public merchantInstance: string,
+    public apiKey: string,
+  ) {}
+
+  async createOrder(
+    amount: string,
+    summary: string,
+    fulfillmentUrl: string,
+  ): Promise<{ orderId: string }> {
+    const reqUrl = new URI("order").absoluteTo(this.merchantBaseUrl).href();
+    const orderReq = {
+      order: {
+        amount,
+        summary,
+        fulfillment_url: fulfillmentUrl,
+        instance: this.merchantInstance,
+      },
+    };
+    const resp = await Axios({
+      method: "post",
+      url: reqUrl,
+      data: orderReq,
+      responseType: "json",
+      headers: {
+        Authorization: `ApiKey ${this.apiKey}`,
+      },
+    });
+    if (resp.status != 200) {
+      throw Error("failed to create bank reserve");
+    }
+    const orderId = resp.data.order_id;
+    if (!orderId) {
+      throw Error("no order id in response");
+    }
+    return { orderId };
+  }
+
+  async checkPayment(orderId: string): Promise<CheckPaymentResponse> {
+    const reqUrl = new URI("check-payment")
+      .absoluteTo(this.merchantBaseUrl)
+      .href();
+    const resp = await Axios({
+      method: "get",
+      url: reqUrl,
+      params: { order_id: orderId, instance: this.merchantInstance },
+      responseType: "json",
+      headers: {
+        Authorization: `ApiKey ${this.apiKey}`,
+      },
+    });
+    if (resp.status != 200) {
+      throw Error("failed to check payment");
+    }
+    return CheckPaymentResponse.checked(resp.data);
+  }
+}
+
+async function main() {
+  const myNotifier = new ConsoleNotifier();
+
+  const myBadge = new ConsoleBadge();
+
+  const myBackend = new MemoryBackend();
+
+  myBackend.enableTracing = false;
+
+  BridgeIDBFactory.enableTracing = false;
+
+  const myBridgeIdbFactory = new BridgeIDBFactory(myBackend);
+  const myIdbFactory: IDBFactory = (myBridgeIdbFactory as any) as IDBFactory;
+
+  const myHttpLib = new NodeHttpLib();
+
+  const myVersionChange = () => {
+    console.error("version change requested, should not happen");
+    throw Error();
+  };
+
+  const myUnsupportedUpgrade = () => {
+    console.error("unsupported database migration");
+    throw Error();
+  };
+
+  shimIndexedDB(myBridgeIdbFactory);
+
+  const exchangeBaseUrl = "https://exchange.test.taler.net/";;
+  const bankBaseUrl = "https://bank.test.taler.net/";;
+
+  const myDb = await openTalerDb(
+    myIdbFactory,
+    myVersionChange,
+    myUnsupportedUpgrade,
+  );
+
+  const myWallet = new Wallet(myDb, myHttpLib, myBadge, myNotifier);
+
+  const reserveResponse = await myWallet.createReserve({
+    amount: amounts.parseOrThrow("TESTKUDOS:10.0"),
+    exchange: exchangeBaseUrl,
+  });
+
+  const bankUser = await registerBankUser(bankBaseUrl, myHttpLib);
+
+  console.log("bank user", bankUser);
+
+  const exchangePaytoUri = await myWallet.getExchangePaytoUri(
+    "https://exchange.test.taler.net/";,
+    ["x-taler-bank"],
+  );
+
+  await createBankReserve(
+    bankBaseUrl,
+    bankUser,
+    "TESTKUDOS:10.0",
+    reserveResponse.reservePub,
+    exchangePaytoUri,
+    myHttpLib,
+  );
+
+  await myWallet.confirmReserve({ reservePub: reserveResponse.reservePub });
+
+  await myWallet.processReserve(reserveResponse.reservePub);
+
+  console.log("process reserve returned");
+
+  const balance = await myWallet.getBalances();
+
+  console.log(JSON.stringify(balance, null, 2));
+
+  const myMerchant = new MerchantBackendConnection(
+    "https://backend.test.taler.net/";,
+    "default",
+    "sandbox",
+  );
+
+  const orderResp = await myMerchant.createOrder(
+    "TESTKUDOS:5",
+    "hello world",
+    "https://example.com/";,
+  );
+
+  console.log("created order with orderId", orderResp.orderId);
+
+  const paymentStatus = await myMerchant.checkPayment(orderResp.orderId);
+
+  console.log("payment status", paymentStatus);
+
+  const contractUrl = paymentStatus.contract_url;
+  if (!contractUrl) {
+    throw Error("no contract URL in payment response");
+  }
+
+  const proposalId = await myWallet.downloadProposal(contractUrl);
+
+  console.log("proposal id", proposalId);
+
+  const checkPayResult = await myWallet.checkPay(proposalId);
+
+  console.log("check pay result", checkPayResult);
+
+  const confirmPayResult = await myWallet.confirmPay(proposalId, undefined);
+
+  console.log("confirmPayResult", confirmPayResult);
+
+  const paymentStatus2 = await myMerchant.checkPayment(orderResp.orderId);
+
+  console.log("payment status after wallet payment:", paymentStatus2);
+
+  myWallet.stop();
+}
+
+main().catch(err => {
+  console.error("Failed with exception:");
+  console.error(err);
+});
diff --git a/src/http.ts b/src/http.ts
index 895b1097..6bdd04e2 100644
--- a/src/http.ts
+++ b/src/http.ts
@@ -24,7 +24,7 @@
  */
 export interface HttpResponse {
   status: number;
-  responseText: string;
+  responseJson: object & any;
 }
 
 
@@ -44,7 +44,7 @@ export interface HttpRequestLibrary {
  * An implementation of the [[HttpRequestLibrary]] using the
  * browser's XMLHttpRequest.
  */
-export class BrowserHttpLib {
+export class BrowserHttpLib implements HttpRequestLibrary {
   private req(method: string,
               url: string,
               options?: any): Promise<HttpResponse> {
@@ -58,8 +58,12 @@ export class BrowserHttpLib {
       }
       myRequest.addEventListener("readystatechange", (e) => {
         if (myRequest.readyState === XMLHttpRequest.DONE) {
+          const responseJson = JSON.parse(myRequest.responseText);
+          if (responseJson === null || typeof responseJson !== "object") {
+            reject(Error("Invalid JSON from HTTP response"));
+          }
           const resp = {
-            responseText: myRequest.responseText,
+            responseJson: responseJson,
             status: myRequest.status,
           };
           resolve(resp);
diff --git a/src/logging.ts b/src/logging.ts
index ca073c10..4e7b60b9 100644
--- a/src/logging.ts
+++ b/src/logging.ts
@@ -21,8 +21,8 @@
 import {
   QueryRoot,
   Store,
-  openPromise,
 } from "./query";
+import { openPromise } from "./promiseUtils";
 
 /**
  * Supported log levels.
diff --git a/src/promiseUtils.ts b/src/promiseUtils.ts
new file mode 100644
index 00000000..eb649471
--- /dev/null
+++ b/src/promiseUtils.ts
@@ -0,0 +1,39 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+ export interface OpenedPromise<T> {
+  promise: Promise<T>;
+  resolve: (val: T) => void;
+  reject: (err: any) => void;
+ }
+
+/**
+ * Get an unresolved promise together with its extracted resolve / reject
+ * function.
+ */
+export function openPromise<T>(): OpenedPromise<T> {
+  let resolve: ((x?: any) => void) | null = null;
+  let reject: ((reason?: any) => void) | null = null;
+  const promise = new Promise<T>((res, rej) => {
+    resolve = res;
+    reject = rej;
+  });
+  if (!(resolve && reject)) {
+    // Never happens, unless JS implementation is broken
+    throw Error();
+  }
+  return { resolve, reject, promise };
+}
\ No newline at end of file
diff --git a/src/query.ts b/src/query.ts
index 68074ec0..7308d9ed 100644
--- a/src/query.ts
+++ b/src/query.ts
@@ -14,13 +14,13 @@
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-
 /**
  * Database query abstractions.
  * @module Query
  * @author Florian Dold
  */
 
+ import { openPromise } from "./promiseUtils";
 
 /**
  * Result of an inner join.
@@ -38,18 +38,17 @@ export interface JoinLeftResult<L, R> {
   right?: R;
 }
 
-
 /**
  * Definition of an object store.
  */
 export class Store<T> {
-  constructor(public name: string,
-              public storeParams?: IDBObjectStoreParameters,
-              public validator?: (v: T) => T) {
-  }
+  constructor(
+    public name: string,
+    public storeParams?: IDBObjectStoreParameters,
+    public validator?: (v: T) => T,
+  ) {}
 }
 
-
 /**
  * Options for an index.
  */
@@ -63,7 +62,6 @@ export interface IndexOptions {
   multiEntry?: boolean;
 }
 
-
 /**
  * Definition of an index.
  */
@@ -78,7 +76,12 @@ export class Index<S extends IDBValidKey, T> {
    */
   options: IndexOptions;
 
-  constructor(s: Store<T>, public indexName: string, public keyPath: string | 
string[], options?: IndexOptions) {
+  constructor(
+    s: Store<T>,
+    public indexName: string,
+    public keyPath: string | string[],
+    options?: IndexOptions,
+  ) {
     const defaultOptions = {
       multiEntry: false,
     };
@@ -91,7 +94,7 @@ export class Index<S extends IDBValidKey, T> {
    * because otherwise the compiler complains.  In iterIndex the
    * key type is pretty useful.
    */
-  protected _dummyKey: S|undefined;
+  protected _dummyKey: S | undefined;
 }
 
 /**
@@ -104,21 +107,29 @@ export interface QueryStream<T> {
    * The left side of the join is extracted via a function from the stream's
    * result, the right side of the join is the key of the index.
    */
-  indexJoin<S, I extends IDBValidKey>(index: Index<I, S>, keyFn: (obj: T) => 
I): QueryStream<JoinResult<T, S>>;
+  indexJoin<S, I extends IDBValidKey>(
+    index: Index<I, S>,
+    keyFn: (obj: T) => I,
+  ): QueryStream<JoinResult<T, S>>;
   /**
    * Join the current query with values from an index, and keep values in the
    * current stream that don't have a match.  The left side of the join is
    * extracted via a function from the stream's result, the right side of the
    * join is the key of the index.
    */
-  indexJoinLeft<S, I extends IDBValidKey>(index: Index<I, S>,
-                                          keyFn: (obj: T) => I): 
QueryStream<JoinLeftResult<T, S>>;
+  indexJoinLeft<S, I extends IDBValidKey>(
+    index: Index<I, S>,
+    keyFn: (obj: T) => I,
+  ): QueryStream<JoinLeftResult<T, S>>;
   /**
    * Join the current query with values from another object store.
    * The left side of the join is extracted via a function over the current 
query,
    * the right side of the join is the key of the object store.
    */
-  keyJoin<S, I extends IDBValidKey>(store: Store<S>, keyFn: (obj: T) => I): 
QueryStream<JoinResult<T, S>>;
+  keyJoin<S, I extends IDBValidKey>(
+    store: Store<S>,
+    keyFn: (obj: T) => I,
+  ): QueryStream<JoinResult<T, S>>;
 
   /**
    * Only keep elements in the result stream for which the predicate returns
@@ -166,7 +177,6 @@ export interface QueryStream<T> {
   run(): Promise<void>;
 }
 
-
 /**
  * Query result that consists of at most one value.
  */
@@ -184,20 +194,25 @@ export interface QueryValue<T> {
    * branch).  This is necessary since IndexedDB does not allow long-lived
    * transactions.
    */
-  cond<R>(f: (x: T) => boolean, onTrue: (r: QueryRoot) => R, onFalse: (r: 
QueryRoot) => R): Promise<void>;
+  cond<R>(
+    f: (x: T) => boolean,
+    onTrue: (r: QueryRoot) => R,
+    onFalse: (r: QueryRoot) => R,
+  ): Promise<void>;
 }
 
-
 abstract class BaseQueryValue<T> implements QueryValue<T> {
-
-  constructor(public root: QueryRoot) {
-  }
+  constructor(public root: QueryRoot) {}
 
   map<S>(f: (x: T) => S): QueryValue<S> {
     return new MapQueryValue<T, S>(this, f);
   }
 
-  cond<R>(f: (x: T) => boolean, onTrue: (r: QueryRoot) => R, onFalse: (r: 
QueryRoot) => R): Promise<void> {
+  cond<R>(
+    f: (x: T) => boolean,
+    onTrue: (r: QueryRoot) => R,
+    onFalse: (r: QueryRoot) => R,
+  ): Promise<void> {
     return new Promise<void>((resolve, reject) => {
       this.subscribeOne((v, tx) => {
         if (f(v)) {
@@ -228,7 +243,7 @@ class FirstQueryValue<T> extends BaseQueryValue<T> {
         return;
       }
       if (isDone) {
-          f(undefined, tx);
+        f(undefined, tx);
       } else {
         f(value, tx);
       }
@@ -247,37 +262,16 @@ class MapQueryValue<T, S> extends BaseQueryValue<S> {
   }
 }
 
-
 /**
  * Exception that should be thrown by client code to abort a transaction.
  */
 export const AbortTransaction = Symbol("abort_transaction");
 
-/**
- * Get an unresolved promise together with its extracted resolve / reject
- * function.
- */
-export function openPromise<T>(): any {
-  let resolve: ((x?: any) => void) | null = null;
-  let reject: ((reason?: any) => void) | null = null;
-  const promise = new Promise<T>((res, rej) => {
-    resolve = res;
-    reject = rej;
-  });
-  if (!(resolve && reject)) {
-    // Never happens, unless JS implementation is broken
-    throw Error();
-  }
-  return {resolve, reject, promise};
-}
-
-
 abstract class QueryStreamBase<T> implements QueryStream<T> {
-  abstract subscribe(f: (isDone: boolean,
-                         value: any,
-                         tx: IDBTransaction) => void): void;
-  constructor(public root: QueryRoot) {
-  }
+  abstract subscribe(
+    f: (isDone: boolean, value: any, tx: IDBTransaction) => void,
+  ): void;
+  constructor(public root: QueryRoot) {}
 
   first(): QueryValue<T> {
     return new FirstQueryValue(this);
@@ -291,20 +285,36 @@ abstract class QueryStreamBase<T> implements 
QueryStream<T> {
     return new QueryStreamMap(this, f);
   }
 
-  indexJoin<S, I extends IDBValidKey>(index: Index<I, S>,
-                                      keyFn: (obj: T) => I): 
QueryStream<JoinResult<T, S>> {
+  indexJoin<S, I extends IDBValidKey>(
+    index: Index<I, S>,
+    keyFn: (obj: T) => I,
+  ): QueryStream<JoinResult<T, S>> {
     this.root.addStoreAccess(index.storeName, false);
-    return new QueryStreamIndexJoin<T, S>(this, index.storeName, 
index.indexName, keyFn);
-  }
-
-  indexJoinLeft<S, I extends IDBValidKey>(index: Index<I, S>,
-                                          keyFn: (obj: T) => I): 
QueryStream<JoinLeftResult<T, S>> {
+    return new QueryStreamIndexJoin<T, S>(
+      this,
+      index.storeName,
+      index.indexName,
+      keyFn,
+    );
+  }
+
+  indexJoinLeft<S, I extends IDBValidKey>(
+    index: Index<I, S>,
+    keyFn: (obj: T) => I,
+  ): QueryStream<JoinLeftResult<T, S>> {
     this.root.addStoreAccess(index.storeName, false);
-    return new QueryStreamIndexJoinLeft<T, S>(this, index.storeName, 
index.indexName, keyFn);
-  }
-
-  keyJoin<S, I extends IDBValidKey>(store: Store<S>,
-                                    keyFn: (obj: T) => I): 
QueryStream<JoinResult<T, S>> {
+    return new QueryStreamIndexJoinLeft<T, S>(
+      this,
+      index.storeName,
+      index.indexName,
+      keyFn,
+    );
+  }
+
+  keyJoin<S, I extends IDBValidKey>(
+    store: Store<S>,
+    keyFn: (obj: T) => I,
+  ): QueryStream<JoinResult<T, S>> {
     this.root.addStoreAccess(store.name, false);
     return new QueryStreamKeyJoin<T, S>(this, store.name, keyFn);
   }
@@ -314,7 +324,7 @@ abstract class QueryStreamBase<T> implements QueryStream<T> 
{
   }
 
   toArray(): Promise<T[]> {
-    const {resolve, promise} = openPromise();
+    const { resolve, promise } = openPromise<T[]>();
     const values: T[] = [];
 
     this.subscribe((isDone, value) => {
@@ -326,12 +336,12 @@ abstract class QueryStreamBase<T> implements 
QueryStream<T> {
     });
 
     return Promise.resolve()
-                  .then(() => this.root.finish())
-                  .then(() => promise);
+      .then(() => this.root.finish())
+      .then(() => promise);
   }
 
   fold<A>(f: (x: T, acc: A) => A, init: A): Promise<A> {
-    const {resolve, promise} = openPromise();
+    const { resolve, promise } = openPromise<A>();
     let acc = init;
 
     this.subscribe((isDone, value) => {
@@ -343,12 +353,12 @@ abstract class QueryStreamBase<T> implements 
QueryStream<T> {
     });
 
     return Promise.resolve()
-                  .then(() => this.root.finish())
-                  .then(() => promise);
+      .then(() => this.root.finish())
+      .then(() => promise);
   }
 
   forEach(f: (x: T) => void): Promise<void> {
-    const {resolve, promise} = openPromise();
+    const { resolve, promise } = openPromise<void>();
 
     this.subscribe((isDone, value) => {
       if (isDone) {
@@ -359,12 +369,12 @@ abstract class QueryStreamBase<T> implements 
QueryStream<T> {
     });
 
     return Promise.resolve()
-                  .then(() => this.root.finish())
-                  .then(() => promise);
+      .then(() => this.root.finish())
+      .then(() => promise);
   }
 
   run(): Promise<void> {
-    const {resolve, promise} = openPromise();
+    const { resolve, promise } = openPromise<void>();
 
     this.subscribe((isDone, value) => {
       if (isDone) {
@@ -374,8 +384,8 @@ abstract class QueryStreamBase<T> implements QueryStream<T> 
{
     });
 
     return Promise.resolve()
-                  .then(() => this.root.finish())
-                  .then(() => promise);
+      .then(() => this.root.finish())
+      .then(() => promise);
   }
 }
 
@@ -401,7 +411,6 @@ class QueryStreamFilter<T> extends QueryStreamBase<T> {
   }
 }
 
-
 class QueryStreamFlatMap<T, S> extends QueryStreamBase<S> {
   constructor(public s: QueryStreamBase<T>, public flatMapFn: (v: T) => S[]) {
     super(s.root);
@@ -421,7 +430,6 @@ class QueryStreamFlatMap<T, S> extends QueryStreamBase<S> {
   }
 }
 
-
 class QueryStreamMap<S, T> extends QueryStreamBase<T> {
   constructor(public s: QueryStreamBase<S>, public mapFn: (v: S) => T) {
     super(s.root);
@@ -439,10 +447,13 @@ class QueryStreamMap<S, T> extends QueryStreamBase<T> {
   }
 }
 
-
 class QueryStreamIndexJoin<T, S> extends QueryStreamBase<JoinResult<T, S>> {
-  constructor(public s: QueryStreamBase<T>, public storeName: string, public 
indexName: string,
-              public key: any) {
+  constructor(
+    public s: QueryStreamBase<T>,
+    public storeName: string,
+    public indexName: string,
+    public key: any,
+  ) {
     super(s.root);
   }
 
@@ -457,7 +468,7 @@ class QueryStreamIndexJoin<T, S> extends 
QueryStreamBase<JoinResult<T, S>> {
       req.onsuccess = () => {
         const cursor = req.result;
         if (cursor) {
-          f(false, {left: value, right: cursor.value}, tx);
+          f(false, { left: value, right: cursor.value }, tx);
           cursor.continue();
         }
       };
@@ -465,10 +476,15 @@ class QueryStreamIndexJoin<T, S> extends 
QueryStreamBase<JoinResult<T, S>> {
   }
 }
 
-
-class QueryStreamIndexJoinLeft<T, S> extends QueryStreamBase<JoinLeftResult<T, 
S>> {
-  constructor(public s: QueryStreamBase<T>, public storeName: string, public 
indexName: string,
-              public key: any) {
+class QueryStreamIndexJoinLeft<T, S> extends QueryStreamBase<
+  JoinLeftResult<T, S>
+> {
+  constructor(
+    public s: QueryStreamBase<T>,
+    public storeName: string,
+    public indexName: string,
+    public key: any,
+  ) {
     super(s.root);
   }
 
@@ -485,11 +501,11 @@ class QueryStreamIndexJoinLeft<T, S> extends 
QueryStreamBase<JoinLeftResult<T, S
         const cursor = req.result;
         if (cursor) {
           gotMatch = true;
-          f(false, {left: value, right: cursor.value}, tx);
+          f(false, { left: value, right: cursor.value }, tx);
           cursor.continue();
         } else {
           if (!gotMatch) {
-            f(false, {left: value}, tx);
+            f(false, { left: value }, tx);
           }
         }
       };
@@ -497,10 +513,12 @@ class QueryStreamIndexJoinLeft<T, S> extends 
QueryStreamBase<JoinLeftResult<T, S
   }
 }
 
-
 class QueryStreamKeyJoin<T, S> extends QueryStreamBase<JoinResult<T, S>> {
-  constructor(public s: QueryStreamBase<T>, public storeName: string,
-              public key: any) {
+  constructor(
+    public s: QueryStreamBase<T>,
+    public storeName: string,
+    public key: any,
+  ) {
     super(s.root);
   }
 
@@ -515,7 +533,7 @@ class QueryStreamKeyJoin<T, S> extends 
QueryStreamBase<JoinResult<T, S>> {
       req.onsuccess = () => {
         const cursor = req.result;
         if (cursor) {
-          f(false, {left: value, right: cursor.value}, tx);
+          f(false, { left: value, right: cursor.value }, tx);
           cursor.continue();
         } else {
           f(true, undefined, tx);
@@ -525,7 +543,6 @@ class QueryStreamKeyJoin<T, S> extends 
QueryStreamBase<JoinResult<T, S>> {
   }
 }
 
-
 class IterQueryStream<T> extends QueryStreamBase<T> {
   private storeName: string;
   private options: any;
@@ -538,11 +555,10 @@ class IterQueryStream<T> extends QueryStreamBase<T> {
     this.subscribers = [];
 
     const doIt = (tx: IDBTransaction) => {
-      const {indexName = void 0, only = void 0} = this.options;
+      const { indexName = void 0, only = void 0 } = this.options;
       let s: any;
       if (indexName !== void 0) {
-        s = tx.objectStore(this.storeName)
-              .index(this.options.indexName);
+        s = tx.objectStore(this.storeName).index(this.options.indexName);
       } else {
         s = tx.objectStore(this.storeName);
       }
@@ -574,13 +590,12 @@ class IterQueryStream<T> extends QueryStreamBase<T> {
   }
 }
 
-
 /**
  * Root wrapper around an IndexedDB for queries with a fluent interface.
  */
 export class QueryRoot {
-  private work: Array<((t: IDBTransaction) => void)> = [];
-  private stores = new Set();
+  private work: Array<(t: IDBTransaction) => void> = [];
+  private stores: Set<string> = new Set();
   private kickoffPromise: Promise<void>;
 
   /**
@@ -595,13 +610,12 @@ export class QueryRoot {
 
   private keys: { [keyName: string]: IDBValidKey } = {};
 
-  constructor(public db: IDBDatabase) {
-  }
+  constructor(public db: IDBDatabase) {}
 
   /**
    * Get a named key that was created during the query.
    */
-  key(keyName: string): IDBValidKey|undefined {
+  key(keyName: string): IDBValidKey | undefined {
     return this.keys[keyName];
   }
 
@@ -626,7 +640,7 @@ export class QueryRoot {
    */
   count<T>(store: Store<T>): Promise<number> {
     this.checkFinished();
-    const {resolve, promise} = openPromise();
+    const { resolve, promise } = openPromise<number>();
 
     const doCount = (tx: IDBTransaction) => {
       const s = tx.objectStore(store.name);
@@ -638,15 +652,17 @@ export class QueryRoot {
 
     this.addWork(doCount, store.name, false);
     return Promise.resolve()
-                  .then(() => this.finish())
-                  .then(() => promise);
-
+      .then(() => this.finish())
+      .then(() => promise);
   }
 
   /**
    * Delete all objects in a store that match a predicate.
    */
-  deleteIf<T>(store: Store<T>, predicate: (x: T, n: number) => boolean): 
QueryRoot {
+  deleteIf<T>(
+    store: Store<T>,
+    predicate: (x: T, n: number) => boolean,
+  ): QueryRoot {
     this.checkFinished();
     const doDeleteIf = (tx: IDBTransaction) => {
       const s = tx.objectStore(store.name);
@@ -666,8 +682,10 @@ export class QueryRoot {
     return this;
   }
 
-  iterIndex<S extends IDBValidKey, T>(index: Index<S, T>,
-                                      only?: S): QueryStream<T> {
+  iterIndex<S extends IDBValidKey, T>(
+    index: Index<S, T>,
+    only?: S,
+  ): QueryStream<T> {
     this.checkFinished();
     this.stores.add(index.storeName);
     this.scheduleFinish();
@@ -688,7 +706,7 @@ export class QueryRoot {
       const req = tx.objectStore(store.name).put(val);
       if (keyName) {
         req.onsuccess = () => {
-            this.keys[keyName] = req.result;
+          this.keys[keyName] = req.result;
         };
       }
     };
@@ -702,7 +720,7 @@ export class QueryRoot {
    */
   putOrGetExisting<T>(store: Store<T>, val: T, key: IDBValidKey): Promise<T> {
     this.checkFinished();
-    const {resolve, promise} = openPromise();
+    const { resolve, promise } = openPromise<T>();
     const doPutOrGet = (tx: IDBTransaction) => {
       const objstore = tx.objectStore(store.name);
       const req = objstore.get(key);
@@ -722,10 +740,9 @@ export class QueryRoot {
     return promise;
   }
 
-
   putWithResult<T>(store: Store<T>, val: T): Promise<IDBValidKey> {
     this.checkFinished();
-    const {resolve, promise} = openPromise();
+    const { resolve, promise } = openPromise<IDBValidKey>();
     const doPutWithResult = (tx: IDBTransaction) => {
       const req = tx.objectStore(store.name).put(val);
       req.onsuccess = () => {
@@ -735,18 +752,17 @@ export class QueryRoot {
     };
     this.addWork(doPutWithResult, store.name, true);
     return Promise.resolve()
-                  .then(() => this.finish())
-                  .then(() => promise);
+      .then(() => this.finish())
+      .then(() => promise);
   }
 
-
   /**
    * Update objects inside a transaction.
    *
    * If the mutation function throws AbortTransaction, the whole transaction 
will be aborted.
    * If the mutation function returns undefined or null, no modification will 
be made.
    */
-  mutate<T>(store: Store<T>, key: any, f: (v: T) => T|undefined): QueryRoot {
+  mutate<T>(store: Store<T>, key: any, f: (v: T) => T | undefined): QueryRoot {
     this.checkFinished();
     const doPut = (tx: IDBTransaction) => {
       const req = tx.objectStore(store.name).openCursor(IDBKeyRange.only(key));
@@ -754,7 +770,7 @@ export class QueryRoot {
         const cursor = req.result;
         if (cursor) {
           const value = cursor.value;
-          let modifiedValue: T|undefined;
+          let modifiedValue: T | undefined;
           try {
             modifiedValue = f(value);
           } catch (e) {
@@ -776,7 +792,6 @@ export class QueryRoot {
     return this;
   }
 
-
   /**
    * Add all object from an iterable to the given object store.
    */
@@ -810,13 +825,13 @@ export class QueryRoot {
   /**
    * Get one object from a store by its key.
    */
-  get<T>(store: Store<T>, key: any): Promise<T|undefined> {
+  get<T>(store: Store<T>, key: any): Promise<T | undefined> {
     this.checkFinished();
     if (key === void 0) {
       throw Error("key must not be undefined");
     }
 
-    const {resolve, promise} = openPromise();
+    const { resolve, promise } = openPromise<T | undefined>();
 
     const doGet = (tx: IDBTransaction) => {
       const req = tx.objectStore(store.name).get(key);
@@ -827,8 +842,8 @@ export class QueryRoot {
 
     this.addWork(doGet, store.name, false);
     return Promise.resolve()
-                  .then(() => this.finish())
-                  .then(() => promise);
+      .then(() => this.finish())
+      .then(() => promise);
   }
 
   /**
@@ -839,7 +854,7 @@ export class QueryRoot {
   getMany<T>(store: Store<T>, keys: any[]): Promise<T[]> {
     this.checkFinished();
 
-    const { resolve, promise } = openPromise();
+    const { resolve, promise } = openPromise<T[]>();
     const results: T[] = [];
 
     const doGetMany = (tx: IDBTransaction) => {
@@ -859,26 +874,29 @@ export class QueryRoot {
 
     this.addWork(doGetMany, store.name, false);
     return Promise.resolve()
-                  .then(() => this.finish())
-                  .then(() => promise);
+      .then(() => this.finish())
+      .then(() => promise);
   }
 
   /**
    * Get one object from a store by its key.
    */
-  getIndexed<I extends IDBValidKey, T>(index: Index<I, T>,
-                                       key: I): Promise<T|undefined> {
+  getIndexed<I extends IDBValidKey, T>(
+    index: Index<I, T>,
+    key: I,
+  ): Promise<T | undefined> {
     this.checkFinished();
     if (key === void 0) {
       throw Error("key must not be undefined");
     }
 
-    const {resolve, promise} = openPromise<void>();
+    const { resolve, promise } = openPromise<T | undefined>();
 
     const doGetIndexed = (tx: IDBTransaction) => {
-      const req = tx.objectStore(index.storeName)
-                    .index(index.indexName)
-                    .get(key);
+      const req = tx
+        .objectStore(index.storeName)
+        .index(index.indexName)
+        .get(key);
       req.onsuccess = () => {
         resolve(req.result);
       };
@@ -886,8 +904,8 @@ export class QueryRoot {
 
     this.addWork(doGetIndexed, index.storeName, false);
     return Promise.resolve()
-                  .then(() => this.finish())
-                  .then(() => promise);
+      .then(() => this.finish())
+      .then(() => promise);
   }
 
   private scheduleFinish() {
@@ -917,10 +935,12 @@ export class QueryRoot {
         resolve();
       };
       tx.onabort = () => {
-        console.warn(`aborted ${mode} transaction on stores [${[... 
this.stores]}]`);
+        console.warn(
+          `aborted ${mode} transaction on stores [${[...this.stores]}]`,
+        );
         reject(Error("transaction aborted"));
       };
-      tx.onerror = (e) => {
+      tx.onerror = e => {
         console.warn(`error in transaction`, (e.target as any).error);
       };
       for (const w of this.work) {
@@ -946,9 +966,11 @@ export class QueryRoot {
   /**
    * Low-level function to add a task to the internal work queue.
    */
-  addWork(workFn: (t: IDBTransaction) => void,
-          storeName?: string,
-          isWrite?: boolean) {
+  addWork(
+    workFn: (t: IDBTransaction) => void,
+    storeName?: string,
+    isWrite?: boolean,
+  ) {
     this.work.push(workFn);
     if (storeName) {
       this.addStoreAccess(storeName, isWrite);
diff --git a/src/talerTypes.ts b/src/talerTypes.ts
index db49b074..9176daf7 100644
--- a/src/talerTypes.ts
+++ b/src/talerTypes.ts
@@ -852,7 +852,7 @@ export class WireFeesJson {
 }
 
 
-@Checkable.Class()
+@Checkable.Class({extra: true})
 export class AccountInfo {
   @Checkable.String()
   url: string;
@@ -905,3 +905,30 @@ export class Proposal {
    */
   static checked: (obj: any) => Proposal;
 }
+
+/**
+ * Response from the internal merchant API.
+ */
+@Checkable.Class({extra: true})
+export class CheckPaymentResponse {
+  @Checkable.Boolean()
+  paid: boolean;
+
+  @Checkable.Optional(Checkable.Boolean())
+  refunded: boolean | undefined;
+
+  @Checkable.Optional(Checkable.String())
+  refunded_amount: string | undefined;
+
+  @Checkable.Optional(Checkable.Value(() => ContractTerms))
+  contract_terms: ContractTerms | undefined;
+
+  @Checkable.Optional(Checkable.String())
+  contract_url: string | undefined;
+
+  /**
+   * Verify that a value matches the schema of this class and convert it into a
+   * member.
+   */
+  static checked: (obj: any) => CheckPaymentResponse;
+}
\ No newline at end of file
diff --git a/src/timer.ts b/src/timer.ts
index ea7d3447..d3bb5d48 100644
--- a/src/timer.ts
+++ b/src/timer.ts
@@ -33,7 +33,7 @@ class IntervalHandle {
   }
 
   clear() {
-    clearTimeout(this.h);
+    clearInterval(this.h);
   }
 }
 
diff --git a/src/wallet.ts b/src/wallet.ts
index 0dfb7755..6d4eeb26 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -105,6 +105,7 @@ import {
   WalletBalance,
   WalletBalanceEntry,
 } from "./walletTypes";
+import { openPromise } from "./promiseUtils";
 
 interface SpeculativePayData {
   payCoinInfo: PayCoinInfo;
@@ -327,6 +328,7 @@ export class Wallet {
    * IndexedDB database used by the wallet.
    */
   db: IDBDatabase;
+  private enableTracing = false;
   private http: HttpRequestLibrary;
   private badge: Badge;
   private notifier: Notifier;
@@ -337,6 +339,12 @@ export class Wallet {
   private speculativePayData: SpeculativePayData | undefined;
   private cachedNextUrl: { [fulfillmentUrl: string]: NextUrlResult } = {};
   private activeTipOperations: { [s: string]: Promise<TipRecord> } = {};
+  private activeProcessReserveOperations: {
+    [reservePub: string]: Promise<void>;
+  } = {};
+  private activeProcessPreCoinOperations: {
+    [preCoinPub: string]: Promise<void>;
+  } = {};
 
   /**
    * Set of identifiers for running operations.
@@ -426,14 +434,14 @@ export class Wallet {
       .iter(Stores.reserves)
       .forEach(reserve => {
         console.log("resuming reserve", reserve.reserve_pub);
-        this.processReserve(reserve);
+        this.processReserve(reserve.reserve_pub);
       });
 
     this.q()
       .iter(Stores.precoins)
       .forEach(preCoin => {
         console.log("resuming precoin");
-        this.processPreCoin(preCoin);
+        this.processPreCoin(preCoin.coinPub);
       });
 
     this.q()
@@ -1073,151 +1081,184 @@ export class Wallet {
    * First fetch information requred to withdraw from the reserve,
    * then deplete the reserve, withdrawing coins until it is empty.
    */
-  private async processReserve(
-    reserveRecord: ReserveRecord,
-    retryDelayMs: number = 250,
-  ): Promise<void> {
-    const opId = "reserve-" + reserveRecord.reserve_pub;
+  async processReserve(reservePub: string): Promise<void> {
+    const activeOperation = this.activeProcessReserveOperations[reservePub];
+
+    if (activeOperation) {
+      return activeOperation;
+    }
+
+    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) => {
+      try {
+        const reserve = await this.updateReserve(reservePub);
+        await this.depleteReserve(reserve);
+        op.resolve();
+      } catch (e) {
+        // random, exponential backoff truncated at 3 minutes
+        const nextDelay = Math.min(
+          2 * retryDelayMs + retryDelayMs * Math.random(),
+          3000 * 60,
+        );
+        console.warn(
+          `Failed to deplete reserve, trying again in ${retryDelayMs} ms`,
+        );
+        this.timerGroup.after(retryDelayMs, () =>
+          processReserveInternal(nextDelay),
+        );
+      }
+    };
+
     try {
-      const reserve = await this.updateReserve(reserveRecord.reserve_pub);
-      await this.depleteReserve(reserve);
-    } catch (e) {
-      // random, exponential backoff truncated at 3 minutes
-      const nextDelay = Math.min(
-        2 * retryDelayMs + retryDelayMs * Math.random(),
-        3000 * 60,
-      );
-      console.warn(
-        `Failed to deplete reserve, trying again in ${retryDelayMs} ms`,
-      );
-      this.timerGroup.after(retryDelayMs, () =>
-        this.processReserve(reserveRecord, nextDelay),
-      );
+      processReserveInternal();
+      this.activeProcessReserveOperations[reservePub] = op.promise;
+      await op.promise;
     } finally {
       this.stopOperation(opId);
+      delete this.activeProcessReserveOperations[reservePub];
     }
   }
 
   /**
    * Given a planchet, withdraw a coin from the exchange.
    */
-  private async processPreCoin(
-    preCoin: PreCoinRecord,
-    retryDelayMs = 200,
-  ): Promise<void> {
-    // Throttle concurrent executions of this function, so we don't withdraw 
too many coins at once.
-    if (
-      this.processPreCoinConcurrent >= 4 ||
-      this.processPreCoinThrottle[preCoin.exchangeBaseUrl]
-    ) {
-      console.log("delaying processPreCoin");
-      this.timerGroup.after(retryDelayMs, () =>
-        this.processPreCoin(preCoin, Math.min(retryDelayMs * 2, 5 * 60 * 
1000)),
-      );
-      return;
+  private async processPreCoin(preCoinPub: string): Promise<void> {
+    const activeOperation = this.activeProcessPreCoinOperations[preCoinPub];
+    if (activeOperation) {
+      return activeOperation;
     }
-    console.log("executing processPreCoin", preCoin);
-    this.processPreCoinConcurrent++;
-    try {
-      const exchange = await this.q().get(
-        Stores.exchanges,
-        preCoin.exchangeBaseUrl,
-      );
-      if (!exchange) {
-        console.error("db inconsistent: exchange for precoin not found");
+
+    const op = openPromise<void>();
+
+    const processPreCoinInternal = async (retryDelayMs: number = 200) => {
+      const preCoin = await this.q().get(Stores.precoins, preCoinPub);
+      if (!preCoin) {
+        console.log("processPreCoin: preCoinPub not found");
         return;
       }
-      const denom = await this.q().get(Stores.denominations, [
-        preCoin.exchangeBaseUrl,
-        preCoin.denomPub,
-      ]);
-      if (!denom) {
-        console.error("db inconsistent: denom for precoin 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]
+      ) {
+        this.enableTracing && console.log("delaying processPreCoin");
+        this.timerGroup.after(retryDelayMs, () =>
+          processPreCoinInternal(Math.min(retryDelayMs * 2, 5 * 60 * 1000)),
+        );
+        return op.promise;
       }
 
-      const coin = await this.withdrawExecute(preCoin);
-      console.log("processPreCoin: got coin", coin);
+      //console.log("executing processPreCoin", preCoin);
+      this.processPreCoinConcurrent++;
 
-      const mutateReserve = (r: ReserveRecord) => {
-        console.log(
-          `before committing coin: current ${amountToPretty(
-            r.current_amount!,
-          )}, precoin: ${amountToPretty(r.precoin_amount)})}`,
-        );
-
-        const x = Amounts.sub(
-          r.precoin_amount,
-          preCoin.coinValue,
-          denom.feeWithdraw,
+      try {
+        const exchange = await this.q().get(
+          Stores.exchanges,
+          preCoin.exchangeBaseUrl,
         );
-        if (x.saturated) {
-          console.error("database inconsistent");
-          throw AbortTransaction;
+        if (!exchange) {
+          console.error("db inconsistent: exchange for precoin not found");
+          return;
+        }
+        const denom = await this.q().get(Stores.denominations, [
+          preCoin.exchangeBaseUrl,
+          preCoin.denomPub,
+        ]);
+        if (!denom) {
+          console.error("db inconsistent: denom for precoin not found");
+          return;
         }
-        r.precoin_amount = x.amount;
-        return r;
-      };
 
-      await this.q()
-        .mutate(Stores.reserves, preCoin.reservePub, mutateReserve)
-        .delete(Stores.precoins, coin.coinPub)
-        .add(Stores.coins, coin)
-        .finish();
+        const coin = await this.withdrawExecute(preCoin);
 
-      if (coin.status === CoinStatus.TainedByTip) {
-        const tip = await this.q().getIndexed(
-          Stores.tips.coinPubIndex,
-          coin.coinPub,
-        );
-        if (!tip) {
-          throw Error(
-            `inconsistent DB: tip for coin pub ${coin.coinPub} not found.`,
+        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;
+        };
 
-        if (tip.accepted) {
-          console.log("untainting already accepted tip");
-          // Transactionally set coin to fresh.
-          const mutateCoin = (c: CoinRecord) => {
-            if (c.status === CoinStatus.TainedByTip) {
-              c.status = CoinStatus.Fresh;
-            }
-            return c;
-          };
-          await this.q().mutate(Stores.coins, coin.coinPub, mutateCoin);
-          // Show notifications only for accepted tips
+        await this.q()
+          .mutate(Stores.reserves, preCoin.reservePub, mutateReserve)
+          .delete(Stores.precoins, coin.coinPub)
+          .add(Stores.coins, coin)
+          .finish();
+
+        if (coin.status === CoinStatus.TainedByTip) {
+          const tip = await this.q().getIndexed(
+            Stores.tips.coinPubIndex,
+            coin.coinPub,
+          );
+          if (!tip) {
+            throw Error(
+              `inconsistent DB: tip for coin pub ${coin.coinPub} not found.`,
+            );
+          }
+
+          if (tip.accepted) {
+            console.log("untainting already accepted tip");
+            // Transactionally set coin to fresh.
+            const mutateCoin = (c: CoinRecord) => {
+              if (c.status === CoinStatus.TainedByTip) {
+                c.status = CoinStatus.Fresh;
+              }
+              return c;
+            };
+            await this.q().mutate(Stores.coins, coin.coinPub, mutateCoin);
+            // Show notifications only for accepted tips
+            this.badge.showNotification();
+          }
+        } else {
           this.badge.showNotification();
         }
-      } else {
-        this.badge.showNotification();
-      }
 
-      this.notifier.notify();
-    } 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, () =>
-        this.processPreCoin(preCoin, nextRetryDelayMs),
-      );
+        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 currentThrottle =
-        this.processPreCoinThrottle[preCoin.exchangeBaseUrl] || 0;
-      this.processPreCoinThrottle[preCoin.exchangeBaseUrl] =
-        currentThrottle + 1;
-      this.timerGroup.after(retryDelayMs, () => {
-        this.processPreCoinThrottle[preCoin.exchangeBaseUrl]--;
-      });
+        const currentThrottle =
+          this.processPreCoinThrottle[preCoin.exchangeBaseUrl] || 0;
+        this.processPreCoinThrottle[preCoin.exchangeBaseUrl] =
+          currentThrottle + 1;
+        this.timerGroup.after(retryDelayMs, () => {
+          this.processPreCoinThrottle[preCoin.exchangeBaseUrl]--;
+        });
+      } finally {
+        this.processPreCoinConcurrent--;
+      }
+    };
+
+    try {
+      this.activeProcessPreCoinOperations[preCoinPub] = op.promise;
+      await processPreCoinInternal();
+      return op.promise;
     } finally {
-      this.processPreCoinConcurrent--;
+      delete this.activeProcessPreCoinOperations[preCoinPub];
     }
   }
 
@@ -1332,7 +1373,7 @@ export class Wallet {
       .finish();
     this.notifier.notify();
 
-    this.processReserve(reserve);
+    this.processReserve(reserve.reserve_pub);
   }
 
   private async withdrawExecute(pc: PreCoinRecord): Promise<CoinRecord> {
@@ -1350,7 +1391,7 @@ export class Wallet {
         status: resp.status,
       });
     }
-    const r = JSON.parse(resp.responseText);
+    const r = resp.responseJson;
     const denomSig = await this.cryptoApi.rsaUnblind(
       r.ev_sig,
       pc.blindingKey,
@@ -1423,20 +1464,22 @@ export class Wallet {
           r.timestamp_depleted = new Date().getTime();
         }
 
-        console.log(
-          `after creating precoin: current ${amountToPretty(
-            r.current_amount,
-          )}, precoin: ${amountToPretty(r.precoin_amount)})}`,
-        );
-
         return r;
       }
 
       const preCoin = await this.cryptoApi.createPreCoin(denom, reserve);
-      await this.q()
-        .put(Stores.precoins, preCoin)
-        .mutate(Stores.reserves, reserve.reserve_pub, mutateReserve);
-      await this.processPreCoin(preCoin);
+      // This will fail and throw an exception if the remaining amount in the
+      // reserve is too low to create a pre-coin.
+      try {
+        await this.q()
+          .put(Stores.precoins, preCoin)
+          .mutate(Stores.reserves, reserve.reserve_pub, mutateReserve)
+          .finish();
+      } catch (e) {
+        console.log("can't create pre-coin:", e.name, e.message);
+        return;
+      }
+      await this.processPreCoin(preCoin.coinPub);
     });
 
     await Promise.all(ps);
@@ -1462,7 +1505,7 @@ export class Wallet {
     if (resp.status !== 200) {
       throw Error();
     }
-    const reserveInfo = ReserveStatus.checked(JSON.parse(resp.responseText));
+    const reserveInfo = ReserveStatus.checked(resp.responseJson);
     if (!reserveInfo) {
       throw Error();
     }
@@ -1486,7 +1529,7 @@ export class Wallet {
       throw Error("/wire request failed");
     }
 
-    const wiJson = JSON.parse(resp.responseText);
+    const wiJson = resp.responseJson;
     if (!wiJson) {
       throw Error("/wire response malformed");
     }
@@ -1745,6 +1788,20 @@ export class Wallet {
     return ret;
   }
 
+  async getExchangePaytoUri(
+    exchangeBaseUrl: string,
+    supportedTargetTypes: string[],
+  ): Promise<string> {
+    const wireInfo = await this.getWireInfo(exchangeBaseUrl);
+    for (let account of wireInfo.accounts) {
+      const paytoUri = new URI(account.url);
+      if (supportedTargetTypes.includes(paytoUri.authority())) {
+        return account.url;
+      }
+    }
+    throw Error("no matching exchange account found");
+  }
+
   /**
    * Update or add exchange DB entry by fetching the /keys information.
    * Optionally link the reserve entry to the new or existing
@@ -1757,9 +1814,7 @@ export class Wallet {
     if (keysResp.status !== 200) {
       throw Error("/keys request failed");
     }
-    const exchangeKeysJson = KeysJson.checked(
-      JSON.parse(keysResp.responseText),
-    );
+    const exchangeKeysJson = KeysJson.checked(keysResp.responseJson);
     const exchangeWire = await this.getWireInfo(baseUrl);
     return this.updateExchangeFromJson(baseUrl, exchangeKeysJson, 
exchangeWire);
   }
@@ -1810,8 +1865,6 @@ export class Wallet {
       throw Error("exchange doesn't offer any denominations");
     }
 
-    console.log("updating exchange with wireMethodDetails", wireMethodDetails);
-
     const r = await this.q().get<ExchangeRecord>(Stores.exchanges, baseUrl);
 
     let exchangeInfo: ExchangeRecord;
@@ -2291,18 +2344,14 @@ export class Wallet {
     console.log("melt request:", meltReq);
     const resp = await this.http.postJson(reqUrl.href(), meltReq);
 
-    console.log("melt response:", resp.responseText);
+    console.log("melt response:", resp.responseJson);
 
     if (resp.status !== 200) {
-      console.error(resp.responseText);
+      console.error(resp.responseJson);
       throw Error("refresh failed");
     }
 
-    const respJson = JSON.parse(resp.responseText);
-
-    if (!respJson) {
-      throw Error("exchange responded with garbage");
-    }
+    const respJson = resp.responseJson;
 
     const norevealIndex = respJson.noreveal_index;
 
@@ -2376,7 +2425,7 @@ export class Wallet {
       return;
     }
 
-    const respJson = JSON.parse(resp.responseText);
+    const respJson = resp.responseJson;
 
     if (!respJson.ev_sigs || !Array.isArray(respJson.ev_sigs)) {
       console.log("/refresh/reveal did not contain ev_sigs");
@@ -2647,9 +2696,7 @@ export class Wallet {
     if (resp.status !== 200) {
       throw Error();
     }
-    const paybackConfirmation = PaybackConfirmation.checked(
-      JSON.parse(resp.responseText),
-    );
+    const paybackConfirmation = PaybackConfirmation.checked(resp.responseJson);
     if (paybackConfirmation.reserve_pub !== coin.reservePub) {
       throw Error(`Coin's reserve doesn't match reserve on payback`);
     }
@@ -2710,6 +2757,7 @@ export class Wallet {
    */
   stop() {
     this.timerGroup.stopCurrentAndFutureTimers();
+    this.cryptoApi.stop();
   }
 
   async getSenderWireInfos(): Promise<SenderWireInfos> {
@@ -2727,14 +2775,14 @@ export class Wallet {
       exchangeWireTypes[e] = Array.from(m[e]);
     });
 
-    const senderWiresSet = new Set();
+    const senderWiresSet: Set<string> = new Set();
     await this.q()
       .iter(Stores.senderWires)
       .map(x => {
         senderWiresSet.add(x.paytoUri);
       })
       .run();
-    const senderWires = Array.from(senderWiresSet);
+    const senderWires: string[] = Array.from(senderWiresSet);
 
     return {
       exchangeWireTypes,
@@ -2857,7 +2905,7 @@ export class Wallet {
         console.error("deposit failed due to status code", resp);
         continue;
       }
-      const respJson = JSON.parse(resp.responseText);
+      const respJson = resp.responseJson;
       if (respJson.status !== "DEPOSIT_OK") {
         console.error("deposit failed", resp);
         continue;
@@ -3194,7 +3242,7 @@ export class Wallet {
         withdrawSig: response.reserve_sigs[i].reserve_sig,
       };
       await this.q().put(Stores.precoins, preCoin);
-      this.processPreCoin(preCoin);
+      this.processPreCoin(preCoin.coinPub);
     }
 
     tipRecord.pickedUp = true;
diff --git a/src/walletTypes.ts b/src/walletTypes.ts
index 73a72bbb..f9d753f2 100644
--- a/src/walletTypes.ts
+++ b/src/walletTypes.ts
@@ -303,7 +303,7 @@ export interface SenderWireInfos {
   /**
    * Sender wire information stored in the wallet.
    */
-  senderWires: object[];
+  senderWires: string[];
 }
 
 
diff --git a/src/webex/pages/return-coins.tsx b/src/webex/pages/return-coins.tsx
index 278f8af9..b5d53c31 100644
--- a/src/webex/pages/return-coins.tsx
+++ b/src/webex/pages/return-coins.tsx
@@ -55,7 +55,7 @@ interface ReturnSelectionItemProps extends 
ReturnSelectionListProps {
 
 interface ReturnSelectionItemState {
   selectedValue: string;
-  supportedWires: object[];
+  supportedWires: string[];
   selectedWire: string;
   currency: string;
 }
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index f2dbd68b..0baa7d41 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -26,10 +26,6 @@
  */
 import { BrowserHttpLib } from "../http";
 import * as logging from "../logging";
-import {
-  Index,
-  Store,
-} from "../query";
 
 import { AmountJson } from "../amounts";
 
@@ -48,10 +44,11 @@ import { isFirefox } from "./compat";
 
 import {
   PurchaseRecord,
-  Stores,
   WALLET_DB_VERSION,
 } from "../dbTypes";
 
+import { openTalerDb, exportDb, importDb, deleteDb } from "../db";
+
 
 import { ChromeBadge } from "./chromeBadge";
 import { MessageType } from "./messages";
@@ -62,9 +59,6 @@ import Port = chrome.runtime.Port;
 import MessageSender = chrome.runtime.MessageSender;
 import { TipToken } from "../talerTypes";
 
-
-const DB_NAME = "taler";
-
 const NeedsWallet = Symbol("NeedsWallet");
 
 function handleMessage(sender: MessageSender,
@@ -104,7 +98,7 @@ function handleMessage(sender: MessageSender,
           tx.objectStore(db.objectStoreNames[i]).clear();
         }
       }
-      deleteDb();
+      deleteDb(indexedDB);
       setBadgeText({ text: "" });
       console.log("reset done");
       if (!currentWallet) {
@@ -688,6 +682,17 @@ let currentWallet: Wallet|undefined;
  */
 let oldDbVersion: number|undefined;
 
+function handleUpgradeUnsupported(oldDbVersion: number, newDbVersion: number) {
+  console.log("DB migration not supported");
+  chrome.tabs.create({
+    url: chrome.extension.getURL(
+      "/src/webex/pages/reset-required.html",
+    ),
+  });
+  setBadgeText({ text: "err" });
+  chrome.browserAction.setBadgeBackgroundColor({ color: "#F00" });
+}
+
 
 async function reinitWallet() {
   if (currentWallet) {
@@ -698,7 +703,7 @@ async function reinitWallet() {
   const badge = new ChromeBadge();
   let db: IDBDatabase;
   try {
-    db = await openTalerDb();
+    db = await openTalerDb(indexedDB, reinitWallet, handleUpgradeUnsupported);
   } catch (e) {
     console.error("could not open database", e);
     return;
@@ -867,120 +872,3 @@ export async function wxMain() {
     }
   }, { urls: ["<all_urls>"] }, ["responseHeaders", "blocking"]);
 }
-
-
-/**
- * Return a promise that resolves
- * to the taler wallet db.
- */
-function openTalerDb(): Promise<IDBDatabase> {
-  return new Promise<IDBDatabase>((resolve, reject) => {
-    const req = indexedDB.open(DB_NAME, WALLET_DB_VERSION);
-    req.onerror = (e) => {
-      console.log("taler database error", e);
-      reject(e);
-    };
-    req.onsuccess = (e) => {
-      req.result.onversionchange = (evt: IDBVersionChangeEvent) => {
-        console.log(`handling live db version change from ${evt.oldVersion} to 
${evt.newVersion}`);
-        req.result.close();
-        reinitWallet();
-      };
-      resolve(req.result);
-    };
-    req.onupgradeneeded = (e) => {
-      const db = req.result;
-      console.log(`DB: upgrade needed: oldVersion=${e.oldVersion}, 
newVersion=${e.newVersion}`);
-      switch (e.oldVersion) {
-        case 0: // DB does not exist yet
-
-          for (const n in Stores) {
-            if ((Stores as any)[n] instanceof Store) {
-              const si: Store<any> = (Stores as any)[n];
-              const s = db.createObjectStore(si.name, si.storeParams);
-              for (const indexName in (si as any)) {
-                if ((si as any)[indexName] instanceof Index) {
-                  const ii: Index<any, any> = (si as any)[indexName];
-                  s.createIndex(ii.indexName, ii.keyPath, ii.options);
-                }
-              }
-            }
-          }
-          break;
-        default:
-          if (e.oldVersion !== WALLET_DB_VERSION) {
-            oldDbVersion = e.oldVersion;
-            chrome.tabs.create({
-              url: 
chrome.extension.getURL("/src/webex/pages/reset-required.html"),
-            });
-            setBadgeText({text: "err"});
-            chrome.browserAction.setBadgeBackgroundColor({color: "#F00"});
-            throw Error("incompatible DB");
-          }
-          break;
-      }
-    };
-  });
-}
-
-
-function exportDb(db: IDBDatabase): Promise<any> {
-  const dump = {
-    name: db.name,
-    stores: {} as {[s: string]: any},
-    version: db.version,
-  };
-
-  return new Promise((resolve, reject) => {
-
-    const tx = db.transaction(Array.from(db.objectStoreNames));
-    tx.addEventListener("complete", () => {
-      resolve(dump);
-    });
-    // tslint:disable-next-line:prefer-for-of
-    for (let i = 0; i < db.objectStoreNames.length; i++) {
-      const name = db.objectStoreNames[i];
-      const storeDump = {} as {[s: string]: any};
-      dump.stores[name] = storeDump;
-      tx.objectStore(name)
-        .openCursor()
-        .addEventListener("success", (e: Event) => {
-          const cursor = (e.target as any).result;
-          if (cursor) {
-            storeDump[cursor.key] = cursor.value;
-            cursor.continue();
-          }
-        });
-    }
-  });
-}
-
-
-function importDb(db: IDBDatabase, dump: any): Promise<void> {
-  console.log("importing db", dump);
-  return new Promise<void>((resolve, reject) => {
-    const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite");
-    if (dump.stores) {
-      for (const storeName in dump.stores) {
-        const objects = [];
-        const dumpStore = dump.stores[storeName];
-        for (const key in dumpStore) {
-          objects.push(dumpStore[key]);
-        }
-        console.log(`importing ${objects.length} records into ${storeName}`);
-        const store = tx.objectStore(storeName);
-        for (const obj of objects) {
-          store.put(obj);
-        }
-      }
-    }
-    tx.addEventListener("complete", () => {
-      resolve();
-    });
-  });
-}
-
-
-function deleteDb() {
-  indexedDB.deleteDatabase(DB_NAME);
-}
diff --git a/tsconfig.json b/tsconfig.json
index 081140fa..4a2f09e8 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -19,7 +19,7 @@
     "noImplicitAny": true,
     "allowJs": true,
     "checkJs": true,
-    "noUnusedLocals": true
+    "incremental": true
   },
   "files": [
     "decl/chrome/chrome.d.ts",
@@ -37,7 +37,9 @@
     "src/crypto/nodeWorker.ts",
     "src/crypto/nodeWorkerEntry.ts",
     "src/crypto/startWorker.js",
+    "src/db.ts",
     "src/dbTypes.ts",
+    "src/headless/taler-wallet-cli.ts",
     "src/helpers-test.ts",
     "src/helpers.ts",
     "src/http.ts",
@@ -46,6 +48,7 @@
     "src/libtoolVersion-test.ts",
     "src/libtoolVersion.ts",
     "src/logging.ts",
+    "src/promiseUtils.ts",
     "src/query.ts",
     "src/talerTypes.ts",
     "src/timer.ts",
diff --git a/yarn.lock b/yarn.lock
index f624383b..79c46857 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1057,13 +1057,13 @@ awesome-typescript-loader@^5.2.1:
     source-map-support "^0.5.3"
     webpack-log "^1.2.0"
 
-axios@^0.18.0:
-  version "0.18.0"
-  resolved 
"https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102";
-  integrity sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=
+axios@^0.19.0:
+  version "0.19.0"
+  resolved 
"https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8";
+  integrity 
sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==
   dependencies:
-    follow-redirects "^1.3.0"
-    is-buffer "^1.1.5"
+    follow-redirects "1.5.10"
+    is-buffer "^2.0.2"
 
 babel-code-frame@^6.22.0:
   version "6.26.0"
@@ -1671,6 +1671,11 @@ commander@^2.12.1, commander@^2.18.0, commander@^2.19.0, 
commander@~2.19.0:
   resolved 
"https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a";
   integrity 
sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==
 
+commander@^2.20.0:
+  version "2.20.0"
+  resolved 
"https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422";
+  integrity 
sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
+
 common-path-prefix@^1.0.0:
   version "1.0.0"
   resolved 
"https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-1.0.0.tgz#cd52f6f0712e0baab97d6f9732874f22f47752c0";
@@ -1970,12 +1975,12 @@ debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3:
   dependencies:
     ms "2.0.0"
 
-debug@^3.2.6:
-  version "3.2.6"
-  resolved 
"https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b";
-  integrity 
sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
+debug@=3.1.0:
+  version "3.1.0"
+  resolved 
"https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261";
+  integrity 
sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
   dependencies:
-    ms "^2.1.1"
+    ms "2.0.0"
 
 debug@^4.1.0, debug@^4.1.1:
   version "4.1.1"
@@ -2645,12 +2650,12 @@ flush-write-stream@^1.0.0, flush-write-stream@^1.0.2:
     inherits "^2.0.3"
     readable-stream "^2.3.6"
 
-follow-redirects@^1.3.0:
-  version "1.7.0"
-  resolved 
"https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76";
-  integrity 
sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==
+follow-redirects@1.5.10:
+  version "1.5.10"
+  resolved 
"https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a";
+  integrity 
sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
   dependencies:
-    debug "^3.2.6"
+    debug "=3.1.0"
 
 for-in@^1.0.1, for-in@^1.0.2:
   version "1.0.2"
@@ -3338,6 +3343,11 @@ is-buffer@^1.1.5:
   resolved 
"https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be";
   integrity 
sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
 
+is-buffer@^2.0.2:
+  version "2.0.3"
+  resolved 
"https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725";
+  integrity 
sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==
+
 is-ci@^1.0.10:
   version "1.2.1"
   resolved 
"https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c";
@@ -5755,6 +5765,14 @@ source-map-support@^0.5.11, source-map-support@^0.5.3, 
source-map-support@~0.5.1
     buffer-from "^1.0.0"
     source-map "^0.6.0"
 
+source-map-support@^0.5.12:
+  version "0.5.12"
+  resolved 
"https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599";
+  integrity 
sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==
+  dependencies:
+    buffer-from "^1.0.0"
+    source-map "^0.6.0"
+
 source-map-url@^0.4.0:
   version "0.4.0"
   resolved 
"https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3";
@@ -6323,9 +6341,9 @@ typescript@3.2.x:
   integrity 
sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg==
 
 typescript@^3.3.4000:
-  version "3.3.4000"
-  resolved 
"https://registry.yarnpkg.com/typescript/-/typescript-3.3.4000.tgz#76b0f89cfdbf97827e1112d64f283f1151d6adf0";
-  integrity 
sha512-jjOcCZvpkl2+z7JFn0yBOoLQyLoIkNZAs/fYJkUG6VKy6zLPHJGfQJYFHzibB6GJaF/8QrcECtlQ5cpvRHSMEA==
+  version "3.5.3"
+  resolved 
"https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977";
+  integrity 
sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==
 
 uglify-js@^3.0.27, uglify-js@^3.1.4:
   version "3.5.2"

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



reply via email to

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