gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 02/04: WIP: simplify DB queries and error handling


From: gnunet
Subject: [taler-wallet-core] 02/04: WIP: simplify DB queries and error handling
Date: Wed, 20 Nov 2019 20:02:57 +0100

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

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

commit 553da649902f71d5ca34c9a6289ab6b1ef0ba7cb
Author: Florian Dold <address@hidden>
AuthorDate: Wed Nov 20 19:48:43 2019 +0100

    WIP: simplify DB queries and error handling
---
 package.json                              |    2 +-
 packages/idb-bridge/.vscode/settings.json |    3 +-
 src/dbTypes.ts                            |   90 +-
 src/headless/clk.ts                       |   64 +-
 src/headless/taler-wallet-cli.ts          |  108 ++-
 src/helpers.ts                            |   14 +
 src/logging.ts                            |  351 --------
 src/query.ts                              | 1112 ++++++-----------------
 src/wallet.ts                             | 1400 +++++++++++++----------------
 src/walletTypes.ts                        |   58 +-
 src/webex/messages.ts                     |   16 -
 src/webex/pages/error.html                |   18 -
 src/webex/pages/error.tsx                 |  129 ---
 src/webex/pages/logs.html                 |   27 -
 src/webex/pages/logs.tsx                  |   86 --
 src/webex/renderHtml.tsx                  |    6 +-
 src/webex/wxApi.ts                        |   32 -
 src/webex/wxBackend.ts                    |   42 -
 tsconfig.json                             |    3 -
 webpack.config.js                         |    2 -
 yarn.lock                                 |    8 +-
 21 files changed, 1131 insertions(+), 2440 deletions(-)

diff --git a/package.json b/package.json
index 8e7a5b35..d8511c40 100644
--- a/package.json
+++ b/package.json
@@ -64,7 +64,7 @@
     "@types/urijs": "^1.19.3",
     "axios": "^0.19.0",
     "commander": "^3.0.1",
-    "idb-bridge": "^0.0.10",
+    "idb-bridge": "^0.0.11",
     "qrcode-generator": "^1.4.3",
     "source-map-support": "^0.5.12",
     "urijs": "^1.18.10"
diff --git a/packages/idb-bridge/.vscode/settings.json 
b/packages/idb-bridge/.vscode/settings.json
index ff30c446..ec71f9aa 100644
--- a/packages/idb-bridge/.vscode/settings.json
+++ b/packages/idb-bridge/.vscode/settings.json
@@ -1,3 +1,4 @@
 {
-    "editor.tabSize": 2
+    "editor.tabSize": 2,
+    "typescript.tsdk": "node_modules/typescript/lib"
 }
\ No newline at end of file
diff --git a/src/dbTypes.ts b/src/dbTypes.ts
index 28893b8e..0d54069e 100644
--- a/src/dbTypes.ts
+++ b/src/dbTypes.ts
@@ -36,6 +36,7 @@ import {
 } from "./talerTypes";
 
 import { Index, Store } from "./query";
+import { Timestamp, OperationError } from "./walletTypes";
 
 /**
  * Current database version, should be incremented
@@ -310,13 +311,10 @@ export class DenominationRecord {
 }
 
 /**
- * Exchange record as stored in the wallet's database.
+ * Details about the exchange that we only know after
+ * querying /keys and /wire.
  */
-export interface ExchangeRecord {
-  /**
-   * Base url of the exchange.
-   */
-  baseUrl: string;
+export interface ExchangeDetails {
   /**
    * Master public key of the exchange.
    */
@@ -331,22 +329,60 @@ export interface ExchangeRecord {
    */
   currency: string;
 
+  /**
+   * Last observed protocol version.
+   */
+  protocolVersion: string;
+
   /**
    * Timestamp for last update.
    */
-  lastUpdateTime: number;
+  lastUpdateTime: Timestamp;
+}
+
+export enum ExchangeUpdateStatus {
+  NONE = "none",
+  FETCH_KEYS = "fetch_keys",
+  FETCH_WIRE = "fetch_wire",
+}
+
+export interface ExchangeBankAccount {
+  url: string;
+}
 
+export interface ExchangeWireInfo {
+  feesForType: { [wireMethod: string]: WireFee[] };
+  accounts: ExchangeBankAccount[];
+}
+
+/**
+ * Exchange record as stored in the wallet's database.
+ */
+export interface ExchangeRecord {
   /**
-   * When did we actually use this exchange last (in milliseconds).  If we
-   * never used the exchange for anything but just updated its info, this is
-   * set to 0.  (Currently only updated when reserves are created.)
+   * Base url of the exchange.
    */
-  lastUsedTime: number;
+  baseUrl: string;
 
   /**
-   * Last observed protocol version.
+   * Details, once known.
    */
-  protocolVersion?: string;
+  details: ExchangeDetails | undefined;
+
+  /**
+   * Mapping from wire method type to the wire fee.
+   */
+  wireInfo: ExchangeWireInfo | undefined;
+
+  /**
+   * Time when the update to the exchange has been started or
+   * undefined if no update is in progress.
+   */
+  updateStarted: Timestamp | undefined;
+
+  updateStatus: ExchangeUpdateStatus;
+
+  lastError?: OperationError;
 }
 
 /**
@@ -554,21 +590,6 @@ export class ProposalDownloadRecord {
   static checked: (obj: any) => ProposalDownloadRecord;
 }
 
-/**
- * Wire fees for an exchange.
- */
-export interface ExchangeWireFeesRecord {
-  /**
-   * Base URL of the exchange.
-   */
-  exchangeBaseUrl: string;
-
-  /**
-   * Mapping from wire method type to the wire fee.
-   */
-  feesForType: { [wireMethod: string]: WireFee[] };
-}
-
 /**
  * Status of a tip we got from a merchant.
  */
@@ -931,12 +952,6 @@ export namespace Stores {
     constructor() {
       super("exchanges", { keyPath: "baseUrl" });
     }
-
-    pubKeyIndex = new Index<string, ExchangeRecord>(
-      this,
-      "pubKeyIndex",
-      "masterPublicKey",
-    );
   }
 
   class CoinsStore extends Store<CoinRecord> {
@@ -1034,12 +1049,6 @@ export namespace Stores {
     }
   }
 
-  class ExchangeWireFeesStore extends Store<ExchangeWireFeesRecord> {
-    constructor() {
-      super("exchangeWireFees", { keyPath: "exchangeBaseUrl" });
-    }
-  }
-
   class ReservesStore extends Store<ReserveRecord> {
     constructor() {
       super("reserves", { keyPath: "reserve_pub" });
@@ -1094,7 +1103,6 @@ export namespace Stores {
   export const config = new ConfigStore();
   export const currencies = new CurrenciesStore();
   export const denominations = new DenominationsStore();
-  export const exchangeWireFees = new ExchangeWireFeesStore();
   export const exchanges = new ExchangeStore();
   export const precoins = new Store<PreCoinRecord>("precoins", {
     keyPath: "coinPub",
diff --git a/src/headless/clk.ts b/src/headless/clk.ts
index 642a1bef..f66d609e 100644
--- a/src/headless/clk.ts
+++ b/src/headless/clk.ts
@@ -20,7 +20,6 @@
 import process = require("process");
 import path = require("path");
 import readline = require("readline");
-import { symlinkSync } from "fs";
 
 class Converter<T> {}
 
@@ -54,6 +53,7 @@ interface ArgumentDef {
   name: string;
   conv: Converter<any>;
   args: ArgumentArgs<any>;
+  required: boolean;
 }
 
 interface SubcommandDef {
@@ -181,7 +181,7 @@ export class CommandGroup<GN extends keyof any, TG> {
     return this as any;
   }
 
-  argument<N extends keyof any, V>(
+  requiredArgument<N extends keyof any, V>(
     name: N,
     conv: Converter<V>,
     args: ArgumentArgs<V> = {},
@@ -190,6 +190,22 @@ export class CommandGroup<GN extends keyof any, TG> {
       args: args,
       conv: conv,
       name: name as string,
+      required: true,
+    };
+    this.arguments.push(argDef);
+    return this as any;
+  }
+
+  maybeArgument<N extends keyof any, V>(
+    name: N,
+    conv: Converter<V>,
+    args: ArgumentArgs<V> = {},
+  ): CommandGroup<GN, TG & SubRecord<GN, N, V | undefined>> {
+    const argDef: ArgumentDef = {
+      args: args,
+      conv: conv,
+      name: name as string,
+      required: false,
     };
     this.arguments.push(argDef);
     return this as any;
@@ -401,10 +417,25 @@ export class CommandGroup<GN extends keyof any, TG> {
           process.exit(-1);
           throw Error("not reached");
         }
+        myArgs[d.name] = unparsedArgs[i];
         posArgIndex++;
       }
     }
 
+    for (let i = posArgIndex; i < this.arguments.length; i++) {
+      const d = this.arguments[i];
+      const n = this.name ?? progname;
+      if (d.required) {
+        if (d.args.default !== undefined) {
+          myArgs[d.name] = d.args.default;
+        } else {
+          console.error(`error: missing positional argument '${d.name}' for 
${n}`);
+          process.exit(-1);
+          throw Error("not reached");
+        }
+      }
+    }
+
     for (let option of this.options) {
       if (option.isFlag == false && option.required == true) {
         if (!foundOptions[option.name]) {
@@ -433,9 +464,7 @@ export class CommandGroup<GN extends keyof any, TG> {
         unparsedArgs.slice(i + 1),
         parsedArgs,
       );
-    }
-
-    if (this.myAction) {
+    } else if (this.myAction) {
       this.myAction(parsedArgs);
     } else {
       this.printHelp(progname, parents);
@@ -513,18 +542,35 @@ export class Program<PN extends keyof any, T> {
   }
 
   /**
-   * Add a positional argument to the program.
+   * Add a required positional argument to the program.
    */
-  argument<N extends keyof any, V>(
+  requiredArgument<N extends keyof any, V>(
     name: N,
     conv: Converter<V>,
     args: ArgumentArgs<V> = {},
   ): Program<N, T & SubRecord<PN, N, V>> {
-    this.mainCommand.argument(name, conv, args);
+    this.mainCommand.requiredArgument(name, conv, args);
+    return this as any;
+  }
+
+  /**
+   * Add an optional argument to the program.
+   */
+  maybeArgument<N extends keyof any, V>(
+    name: N,
+    conv: Converter<V>,
+    args: ArgumentArgs<V> = {},
+  ): Program<N, T & SubRecord<PN, N, V | undefined>> {
+    this.mainCommand.maybeArgument(name, conv, args);
     return this as any;
   }
 }
 
+export type GetArgType<T> =
+  T extends Program<any, infer AT> ? AT :
+  T extends CommandGroup<any, infer AT> ? AT :
+  any;
+
 export function program<PN extends keyof any>(
   argKey: PN,
   args: ProgramArgs = {},
@@ -532,6 +578,8 @@ export function program<PN extends keyof any>(
   return new Program(argKey as string, args);
 }
 
+
+
 export function prompt(question: string): Promise<string> {
   const stdinReadline = readline.createInterface({
     input: process.stdin,
diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
index 41f68319..06235d0b 100644
--- a/src/headless/taler-wallet-cli.ts
+++ b/src/headless/taler-wallet-cli.ts
@@ -97,6 +97,28 @@ const walletCli = clk
     help: "Enable verbose output.",
   });
 
+type WalletCliArgsType = clk.GetArgType<typeof walletCli>;
+
+async function withWallet<T>(
+  walletCliArgs: WalletCliArgsType,
+  f: (w: Wallet) => Promise<T>,
+): Promise<T> {
+  applyVerbose(walletCliArgs.wallet.verbose);
+  const wallet = await getDefaultNodeWallet({
+    persistentStoragePath: walletDbPath,
+  });
+  try {
+    await wallet.fillDefaults();
+    const ret = await f(wallet);
+    return ret;
+  } catch (e) {
+    console.error("caught exception:", e);
+    process.exit(1);
+  } finally {
+    wallet.stop();
+  }
+}
+
 walletCli
   .subcommand("testPayCmd", "test-pay", { help: "create contract and pay" })
   .requiredOption("amount", ["-a", "--amount"], clk.STRING)
@@ -135,15 +157,11 @@ walletCli
 walletCli
   .subcommand("", "balance", { help: "Show wallet balance." })
   .action(async args => {
-    applyVerbose(args.wallet.verbose);
     console.log("balance command called");
-    const wallet = await getDefaultNodeWallet({
-      persistentStoragePath: walletDbPath,
+    withWallet(args, async (wallet) => {
+      const balance = await wallet.getBalances();
+      console.log(JSON.stringify(balance, undefined, 2));
     });
-    console.log("got wallet");
-    const balance = await wallet.getBalances();
-    console.log(JSON.stringify(balance, undefined, 2));
-    process.exit(0);
   });
 
 walletCli
@@ -153,29 +171,19 @@ walletCli
   .requiredOption("limit", ["--limit"], clk.STRING)
   .requiredOption("contEvt", ["--continue-with"], clk.STRING)
   .action(async args => {
-    applyVerbose(args.wallet.verbose);
-    console.log("history command called");
-    const wallet = await getDefaultNodeWallet({
-      persistentStoragePath: walletDbPath,
+    withWallet(args, async (wallet) => {
+      const history = await wallet.getHistory();
+      console.log(JSON.stringify(history, undefined, 2));
     });
-    console.log("got wallet");
-    const history = await wallet.getHistory();
-    console.log(JSON.stringify(history, undefined, 2));
-    process.exit(0);
   });
 
 walletCli
   .subcommand("", "pending", { help: "Show pending operations." })
   .action(async args => {
-    applyVerbose(args.wallet.verbose);
-    console.log("history command called");
-    const wallet = await getDefaultNodeWallet({
-      persistentStoragePath: walletDbPath,
+    withWallet(args, async (wallet) => {
+      const pending = await wallet.getPendingOperations();
+      console.log(JSON.stringify(pending, undefined, 2));
     });
-    console.log("got wallet");
-    const pending = await wallet.getPendingOperations();
-    console.log(JSON.stringify(pending, undefined, 2));
-    process.exit(0);
   });
 
 async function asyncSleep(milliSeconds: number): Promise<void> {
@@ -184,6 +192,16 @@ async function asyncSleep(milliSeconds: number): 
Promise<void> {
   });
 }
 
+walletCli
+  .subcommand("runPendingOpt", "run-pending", {
+    help: "Run pending operations."
+  })
+  .action(async (args) => {
+    withWallet(args, async (wallet) => {
+      await wallet.processPending();
+    });
+  });
+
 walletCli
   .subcommand("testMerchantQrcodeCmd", "test-merchant-qrcode")
   .requiredOption("amount", ["-a", "--amount"], clk.STRING, {
@@ -279,7 +297,7 @@ walletCli
 
 walletCli
   .subcommand("withdrawUriCmd", "withdraw-uri")
-  .argument("withdrawUri", clk.STRING)
+  .requiredArgument("withdrawUri", clk.STRING)
   .action(async args => {
     applyVerbose(args.wallet.verbose);
     const cmdArgs = args.withdrawUriCmd;
@@ -318,7 +336,7 @@ walletCli
 
 walletCli
   .subcommand("tipUriCmd", "tip-uri")
-  .argument("uri", clk.STRING)
+  .requiredArgument("uri", clk.STRING)
   .action(async args => {
     applyVerbose(args.wallet.verbose);
     const tipUri = args.tipUriCmd.uri;
@@ -334,7 +352,7 @@ walletCli
 
 walletCli
   .subcommand("refundUriCmd", "refund-uri")
-  .argument("uri", clk.STRING)
+  .requiredArgument("uri", clk.STRING)
   .action(async args => {
     applyVerbose(args.wallet.verbose);
     const refundUri = args.refundUriCmd.uri;
@@ -346,20 +364,38 @@ walletCli
     wallet.stop();
   });
 
-const exchangesCli = walletCli
-  .subcommand("exchangesCmd", "exchanges", {
-    help: "Manage exchanges."
-  });
-
-exchangesCli.subcommand("exchangesListCmd", "list", {
-  help: "List known exchanges."
+const exchangesCli = walletCli.subcommand("exchangesCmd", "exchanges", {
+  help: "Manage exchanges.",
 });
 
-exchangesCli.subcommand("exchangesListCmd", "update");
+exchangesCli
+  .subcommand("exchangesListCmd", "list", {
+    help: "List known exchanges.",
+  })
+  .action(async args => {
+    console.log("Listing exchanges ...");
+    withWallet(args, async (wallet) => {
+      const exchanges = await wallet.getExchanges();
+      console.log("exchanges", exchanges);
+    });
+  });
+
+exchangesCli
+  .subcommand("exchangesUpdateCmd", "update", {
+    help: "Update or add an exchange by base URL.",
+  })
+  .requiredArgument("url", clk.STRING, {
+    help: "Base URL of the exchange.",
+  })
+  .action(async args => {
+    withWallet(args, async (wallet) => {
+      const res = await 
wallet.updateExchangeFromUrl(args.exchangesUpdateCmd.url);
+    });
+  });
 
 walletCli
   .subcommand("payUriCmd", "pay-uri")
-  .argument("url", clk.STRING)
+  .requiredArgument("url", clk.STRING)
   .flag("autoYes", ["-y", "--yes"])
   .action(async args => {
     applyVerbose(args.wallet.verbose);
@@ -374,7 +410,7 @@ walletCli
   });
 
 const testCli = walletCli.subcommand("testingArgs", "testing", {
-  help: "Subcommands for testing GNU Taler deployments."
+  help: "Subcommands for testing GNU Taler deployments.",
 });
 
 testCli
diff --git a/src/helpers.ts b/src/helpers.ts
index 7cd17649..a063db16 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -25,6 +25,7 @@ import { AmountJson } from "./amounts";
 import * as Amounts from "./amounts";
 
 import URI = require("urijs");
+import { Timestamp } from "./walletTypes";
 
 /**
  * Show an amount in a form suitable for the user.
@@ -125,6 +126,19 @@ export function getTalerStampSec(stamp: string): number | 
null {
   return parseInt(m[1], 10);
 }
 
+/**
+ * Extract a timestamp from a Taler timestamp string.
+ */
+export function extractTalerStamp(stamp: string): Timestamp | undefined {
+  const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/);
+  if (!m || !m[1]) {
+    return undefined;
+  }
+  return {
+    t_ms: parseInt(m[1], 10) * 1000,
+  };
+}
+
 /**
  * Check if a timestamp is in the right format.
  */
diff --git a/src/logging.ts b/src/logging.ts
deleted file mode 100644
index 4e7b60b9..00000000
--- a/src/logging.ts
+++ /dev/null
@@ -1,351 +0,0 @@
-/*
- This file is part of TALER
- (C) 2016 Inria
-
- 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/>
- */
-
-/**
- * Configurable logging.  Allows to log persistently to a database.
- */
-
-import {
-  QueryRoot,
-  Store,
-} from "./query";
-import { openPromise } from "./promiseUtils";
-
-/**
- * Supported log levels.
- */
-export type Level = "error" | "debug" | "info" | "warn";
-
-// Right now, our debug/info/warn/debug loggers just use the console based
-// loggers.  This might change in the future.
-
-function makeInfo() {
-  return console.info.bind(console, "%o");
-}
-
-function makeWarn() {
-  return console.warn.bind(console, "%o");
-}
-
-function makeError() {
-  return console.error.bind(console, "%o");
-}
-
-function makeDebug() {
-  return console.log.bind(console, "%o");
-}
-
-/**
- * Log a message using the configurable logger.
- */
-export async function log(msg: string, level: Level = "info"): Promise<void> {
-  const ci = getCallInfo(2);
-  return record(level, msg, undefined, ci.file, ci.line, ci.column);
-}
-
-function getCallInfo(level: number) {
-  // see https://github.com/v8/v8/wiki/Stack-Trace-API
-  const stack = Error().stack;
-  if (!stack) {
-    return unknownFrame;
-  }
-  const lines = stack.split("\n");
-  return parseStackLine(lines[level + 1]);
-}
-
-interface Frame {
-  column?: number;
-  file?: string;
-  line?: number;
-  method?: string;
-}
-
-const unknownFrame: Frame = {
-  column: 0,
-  file: "(unknown)",
-  line: 0,
-  method: "(unknown)",
-};
-
-/**
- * Adapted from https://github.com/errwischt/stacktrace-parser.
- */
-function parseStackLine(stackLine: string): Frame {
-  // tslint:disable-next-line:max-line-length
-  const chrome = /^\s*at (?:(?:(?:Anonymous function)?|((?:\[object 
object\])?\S+(?: \[as \S+\])?)) 
)?\(?((?:file|http|https):.*?):(\d+)(?::(\d+))?\)?\s*$/i;
-  const gecko = /^(?:\s*([^@]*)(?:\((.*?)\))?@)?(\S.*?):(\d+)(?::(\d+))?\s*$/i;
-  const node  = /^\s*at (?:((?:\[object object\])?\S+(?: \[as \S+\])?) 
)?\(?(.*?):(\d+)(?::(\d+))?\)?\s*$/i;
-  let parts;
-
-  parts = gecko.exec(stackLine);
-  if (parts) {
-    const f: Frame = {
-        column: parts[5] ? +parts[5] : undefined,
-        file: parts[3],
-        line: +parts[4],
-        method: parts[1] || "(unknown)",
-    };
-    return f;
-  }
-
-  parts = chrome.exec(stackLine);
-  if (parts) {
-    const f: Frame = {
-        column: parts[4] ? +parts[4] : undefined,
-        file: parts[2],
-        line: +parts[3],
-        method: parts[1] || "(unknown)",
-    };
-    return f;
-  }
-
-  parts = node.exec(stackLine);
-  if (parts) {
-    const f: Frame = {
-        column: parts[4] ? +parts[4] : undefined,
-        file: parts[2],
-        line: +parts[3],
-        method: parts[1] || "(unknown)",
-    };
-    return f;
-  }
-
-  return unknownFrame;
-}
-
-
-let db: IDBDatabase|undefined;
-
-/**
- * A structured log entry as stored in the database.
- */
-export interface LogEntry {
-  /**
-   * Soure code column where the error occured.
-   */
-  col?: number;
-  /**
-   * Additional detail for the log statement.
-   */
-  detail?: string;
-  /**
-   * Id of the log entry, used as primary
-   * key for the database.
-   */
-  id?: number;
-  /**
-   * Log level, see [[Level}}.
-   */
-  level: string;
-  /**
-   * Line where the log was created from.
-   */
-  line?: number;
-  /**
-   * The actual log message.
-   */
-  msg: string;
-  /**
-   * The source file where the log enctry
-   * was created from.
-   */
-  source?: string;
-  /**
-   * Time when the log entry was created.
-   */
-  timestamp: number;
-}
-
-/**
- * Get all logs.  Only use for debugging, since this returns all logs ever made
- * at once without pagination.
- */
-export async function getLogs(): Promise<LogEntry[]> {
-  if (!db) {
-    db = await openLoggingDb();
-  }
-  return await new QueryRoot(db).iter(logsStore).toArray();
-}
-
-/**
- * The barrier ensures that only one DB write is scheduled against the log db
- * at the same time, so that the DB can stay responsive.  This is a bit of a
- * design problem with IndexedDB, it doesn't guarantee fairness.
- */
-let barrier: any;
-
-/**
- * Record an exeption in the log.
- */
-export async function recordException(msg: string, e: any): Promise<void> {
-  let stack: string|undefined;
-  let frame: Frame|undefined;
-  try {
-    stack = e.stack;
-    if (stack) {
-      const lines = stack.split("\n");
-      frame = parseStackLine(lines[1]);
-    }
-  } catch (e) {
-    // ignore
-  }
-  if (!frame) {
-    frame = unknownFrame;
-  }
-  return record("error", e.toString(), stack, frame.file, frame.line, 
frame.column);
-}
-
-
-/**
- * Cache for reports.  Also used when something is so broken that we can't even
- * access the database.
- */
-const reportCache: { [reportId: string]: any } = {};
-
-
-/**
- * Get a UUID that does not use cryptographically secure randomness.
- * Formatted as RFC4122 version 4 UUID.
- */
-function getInsecureUuid() {
-  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c: string) 
=> {
-    const r = Math.random() * 16 | 0;
-    const v = c === "x" ? r : (r & 0x3 | 0x8);
-    return v.toString(16);
-  });
-}
-
-
-/**
- * Store a report and return a unique identifier to retrieve it later.
- */
-export async function storeReport(report: any): Promise<string> {
-  const uid = getInsecureUuid();
-  reportCache[uid] = report;
-  return uid;
-}
-
-
-/**
- * Retrieve a report by its unique identifier.
- */
-export async function getReport(reportUid: string): Promise<any> {
-  return reportCache[reportUid];
-}
-
-
-/**
- * Record a log entry in the database.
- */
-export async function record(level: Level,
-                             msg: string,
-                             detail?: string,
-                             source?: string,
-                             line?: number,
-                             col?: number): Promise<void> {
-  if (typeof indexedDB === "undefined") {
-    console.log("can't access DB for logging in this context");
-    console.log("log was", { level, msg, detail, source, line, col });
-    return;
-  }
-
-  let myBarrier: any;
-
-  if (barrier) {
-    const p = barrier.promise;
-    myBarrier = barrier = openPromise();
-    await p;
-  } else {
-    myBarrier = barrier = openPromise();
-  }
-
-  try {
-    if (!db) {
-      db = await openLoggingDb();
-    }
-
-    const count = await new QueryRoot(db).count(logsStore);
-
-    if (count > 1000) {
-      await new QueryRoot(db).deleteIf(logsStore, (e, i) => (i < 200));
-    }
-
-    const entry: LogEntry = {
-      col,
-      detail,
-      level,
-      line,
-      msg,
-      source,
-      timestamp: new Date().getTime(),
-    };
-    await new QueryRoot(db).put(logsStore, entry);
-  } finally {
-    await Promise.resolve().then(() => myBarrier.resolve());
-  }
-}
-
-const loggingDbVersion = 2;
-
-const logsStore: Store<LogEntry> = new Store<LogEntry>("logs");
-
-/**
- * Get a handle to the IndexedDB used to store
- * logs.
- */
-export function openLoggingDb(): Promise<IDBDatabase> {
-  return new Promise<IDBDatabase>((resolve, reject) => {
-    const req = indexedDB.open("taler-logging", loggingDbVersion);
-    req.onerror = (e) => {
-      reject(e);
-    };
-    req.onsuccess = (e) => {
-      resolve(req.result);
-    };
-    req.onupgradeneeded = (e) => {
-      const resDb = req.result;
-      if (e.oldVersion !== 0) {
-        try {
-          resDb.deleteObjectStore("logs");
-        } catch (e) {
-          console.error(e);
-        }
-      }
-      resDb.createObjectStore("logs", { keyPath: "id", autoIncrement: true });
-      resDb.createObjectStore("reports", { keyPath: "uid", autoIncrement: 
false });
-    };
-  });
-}
-
-/**
- * Log a message at severity info.
- */
-export const info = makeInfo();
-
-/**
- * Log a message at severity debug.
- */
-export const debug = makeDebug();
-
-/**
- * Log a message at severity warn.
- */
-export const warn = makeWarn();
-
-/**
- * Log a message at severity error.
- */
-export const error = makeError();
diff --git a/src/query.ts b/src/query.ts
index 7c939046..7d03cfea 100644
--- a/src/query.ts
+++ b/src/query.ts
@@ -1,3 +1,5 @@
+import { openPromise } from "./promiseUtils";
+
 /*
  This file is part of TALER
  (C) 2016 GNUnet e.V.
@@ -20,9 +22,6 @@
  * @author Florian Dold
  */
 
- import { openPromise } from "./promiseUtils";
-import { join } from "path";
-
 /**
  * Result of an inner join.
  */
@@ -63,928 +62,335 @@ export interface IndexOptions {
   multiEntry?: boolean;
 }
 
-/**
- * Definition of an index.
- */
-export class Index<S extends IDBValidKey, T> {
-  /**
-   * Name of the store that this index is associated with.
-   */
-  storeName: string;
-
-  /**
-   * Options to use for the index.
-   */
-  options: IndexOptions;
-
-  constructor(
-    s: Store<T>,
-    public indexName: string,
-    public keyPath: string | string[],
-    options?: IndexOptions,
-  ) {
-    const defaultOptions = {
-      multiEntry: false,
+function requestToPromise(req: IDBRequest): Promise<any> {
+  return new Promise((resolve, reject) => {
+    req.onsuccess = () => {
+      resolve(req.result);
     };
-    this.options = { ...defaultOptions, ...(options || {}) };
-    this.storeName = s.name;
-  }
-
-  /**
-   * We want to have the key type parameter in use somewhere,
-   * because otherwise the compiler complains.  In iterIndex the
-   * key type is pretty useful.
-   */
-  protected _dummyKey: S | undefined;
+    req.onerror = () => {
+      reject(req.error);
+    };
+  });
 }
 
-/**
- * Stream that can be filtered, reduced or joined
- * with indices.
- */
-export interface QueryStream<T> {
-  /**
-   * Join the current query with values from an index.
-   * 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>>;
-  /**
-   * 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>>;
-  /**
-   * 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>>;
-
-  /**
-   * Only keep elements in the result stream for which the predicate returns
-   * true.
-   */
-  filter(f: (x: T) => boolean): QueryStream<T>;
-
-  /**
-   * Fold the stream, resulting in a single value.
-   */
-  fold<S>(f: (v: T, acc: S) => S, start: S): Promise<S>;
-
-  /**
-   * Execute a function for every value of the stream, for the
-   * side-effects of the function.
-   */
-  forEach(f: (v: T) => void): Promise<void>;
-
-  /**
-   * Map each element of the stream using a function, resulting in another
-   * stream of a different type.
-   */
-  map<S>(f: (x: T) => S): QueryStream<S>;
-
-  /**
-   * Map each element of the stream to a potentially empty array, and collect
-   * the result in a stream of the flattened arrays.
-   */
-  flatMap<S>(f: (x: T) => S[]): QueryStream<S>;
-
-  /**
-   * Collect the stream into an array and return a promise for it.
-   */
-  toArray(): Promise<T[]>;
-
-  /**
-   * Get the first value of the stream.
-   */
-  first(): QueryValue<T>;
-
-  /**
-   * Run the query without returning a result.
-   * Useful for queries with side effects.
-   */
-  run(): Promise<void>;
+export function oneShotGet<T>(
+  db: IDBDatabase,
+  store: Store<T>,
+  key: any,
+): Promise<T | undefined> {
+  const tx = db.transaction([store.name], "readonly");
+  const req = tx.objectStore(store.name).get(key);
+  return requestToPromise(req);
 }
 
-/**
- * Query result that consists of at most one value.
- */
-export interface QueryValue<T> {
-  /**
-   * Apply a function to a query value.
-   */
-  map<S>(f: (x: T) => S): QueryValue<S>;
-  /**
-   * Conditionally execute either of two queries based
-   * on a property of this query value.
-   *
-   * Useful to properly implement complex queries within a transaction (as
-   * opposed to just computing the conditional and then executing either
-   * 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>;
+export function oneShotGetIndexed<S extends IDBValidKey, T>(
+  db: IDBDatabase,
+  index: Index<S, T>,
+  key: any,
+): Promise<T | undefined> {
+  const tx = db.transaction([index.storeName], "readonly");
+  const req = tx.objectStore(index.storeName).index(index.indexName).get(key);
+  return requestToPromise(req);
 }
 
-abstract class BaseQueryValue<T> implements QueryValue<T> {
-  constructor(public root: QueryRoot) {}
-
-  map<S>(f: (x: T) => S): QueryValue<S> {
-    return new MapQueryValue<T, S>(this, f);
-  }
+export function oneShotPut<T>(
+  db: IDBDatabase,
+  store: Store<T>,
+  value: T,
+  key?: any,
+): Promise<any> {
+  const tx = db.transaction([store.name], "readwrite");
+  const req = tx.objectStore(store.name).put(value, key);
+  return requestToPromise(req);
+}
 
-  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)) {
-          onTrue(new QueryRoot(this.root.db));
+function applyMutation<T>(req: IDBRequest, f: (x: T) => T | undefined): 
Promise<void> {
+  return new Promise((resolve, reject) => {
+    req.onsuccess = () => {
+      const cursor = req.result;
+      if (cursor) {
+        const val = cursor.value();
+        const modVal = f(val);
+        if (modVal !== undefined && modVal !== null) {
+          const req2: IDBRequest = cursor.update(modVal);
+          req2.onerror = () => {
+            reject(req2.error);
+          };
+          req2.onsuccess = () => {
+            cursor.continue();
+          };
         } else {
-          onFalse(new QueryRoot(this.root.db));
+          cursor.continue();
         }
-      });
-      resolve();
-    });
-  }
-
-  abstract subscribeOne(f: SubscribeOneFn): void;
-}
-
-class FirstQueryValue<T> extends BaseQueryValue<T> {
-  private gotValue = false;
-  private s: QueryStreamBase<T>;
-
-  constructor(stream: QueryStreamBase<T>) {
-    super(stream.root);
-    this.s = stream;
-  }
-
-  subscribeOne(f: SubscribeOneFn): void {
-    this.s.subscribe((isDone, value, tx) => {
-      if (this.gotValue) {
-        return;
-      }
-      if (isDone) {
-        f(undefined, tx);
       } else {
-        f(value, tx);
+        resolve();
       }
-      this.gotValue = true;
-    });
-  }
+    };
+    req.onerror = () => {
+      reject(req.error);
+    };
+  });
 }
 
-class MapQueryValue<T, S> extends BaseQueryValue<S> {
-  constructor(private v: BaseQueryValue<T>, private mapFn: (x: T) => S) {
-    super(v.root);
-  }
-
-  subscribeOne(f: SubscribeOneFn): void {
-    this.v.subscribeOne((v, tx) => this.mapFn(v));
-  }
+export function oneShotMutate<T>(
+  db: IDBDatabase,
+  store: Store<T>,
+  key: any,
+  f: (x: T) => T | undefined,
+): Promise<void> {
+  const tx = db.transaction([store.name], "readwrite");
+  const req = tx.objectStore(store.name).openCursor(key);
+  return applyMutation(req, f);
 }
 
-/**
- * Exception that should be thrown by client code to abort a transaction.
- */
-export const AbortTransaction = Symbol("abort_transaction");
-
-abstract class QueryStreamBase<T> implements QueryStream<T> {
-  abstract subscribe(
-    f: (isDone: boolean, value: any, tx: IDBTransaction) => void,
-  ): void;
-  constructor(public root: QueryRoot) {}
-
-  first(): QueryValue<T> {
-    return new FirstQueryValue(this);
-  }
-
-  flatMap<S>(f: (x: T) => S[]): QueryStream<S> {
-    return new QueryStreamFlatMap<T, S>(this, f);
-  }
+type CursorResult<T> = CursorEmptyResult<T> | CursorValueResult<T>;
 
-  map<S>(f: (x: T) => S): QueryStream<S> {
-    return new QueryStreamMap(this, f);
-  }
-
-  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>> {
-    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>> {
-    this.root.addStoreAccess(store.name, false);
-    return new QueryStreamKeyJoin<T, S>(this, store.name, keyFn);
-  }
-
-  filter(f: (x: any) => boolean): QueryStream<T> {
-    return new QueryStreamFilter(this, f);
-  }
-
-  toArray(): Promise<T[]> {
-    const { resolve, promise } = openPromise<T[]>();
-    const values: T[] = [];
-
-    this.subscribe((isDone, value) => {
-      if (isDone) {
-        resolve(values);
-        return;
-      }
-      values.push(value);
-    });
-
-    return Promise.resolve()
-      .then(() => this.root.finish())
-      .then(() => promise);
-  }
-
-  fold<A>(f: (x: T, acc: A) => A, init: A): Promise<A> {
-    const { resolve, promise } = openPromise<A>();
-    let acc = init;
-
-    this.subscribe((isDone, value) => {
-      if (isDone) {
-        resolve(acc);
-        return;
-      }
-      acc = f(value, acc);
-    });
-
-    return Promise.resolve()
-      .then(() => this.root.finish())
-      .then(() => promise);
-  }
-
-  forEach(f: (x: T) => void): Promise<void> {
-    const { resolve, promise } = openPromise<void>();
-
-    this.subscribe((isDone, value) => {
-      if (isDone) {
-        resolve();
-        return;
-      }
-      f(value);
-    });
-
-    return Promise.resolve()
-      .then(() => this.root.finish())
-      .then(() => promise);
-  }
-
-  run(): Promise<void> {
-    const { resolve, promise } = openPromise<void>();
-
-    this.subscribe((isDone, value) => {
-      if (isDone) {
-        resolve();
-        return;
-      }
-    });
-
-    return Promise.resolve()
-      .then(() => this.root.finish())
-      .then(() => promise);
-  }
+interface CursorEmptyResult<T> {
+  hasValue: false;
 }
 
-type FilterFn = (e: any) => boolean;
-type SubscribeFn = (done: boolean, value: any, tx: IDBTransaction) => void;
-type SubscribeOneFn = (value: any, tx: IDBTransaction) => void;
-
-class QueryStreamFilter<T> extends QueryStreamBase<T> {
-  constructor(public s: QueryStreamBase<T>, public filterFn: FilterFn) {
-    super(s.root);
-  }
-
-  subscribe(f: SubscribeFn) {
-    this.s.subscribe((isDone, value, tx) => {
-      if (isDone) {
-        f(true, undefined, tx);
-        return;
-      }
-      if (this.filterFn(value)) {
-        f(false, value, tx);
-      }
-    });
-  }
+interface CursorValueResult<T> {
+  hasValue: true;
+  value: T;
 }
 
-class QueryStreamFlatMap<T, S> extends QueryStreamBase<S> {
-  constructor(public s: QueryStreamBase<T>, public flatMapFn: (v: T) => S[]) {
-    super(s.root);
-  }
-
-  subscribe(f: SubscribeFn) {
-    this.s.subscribe((isDone, value, tx) => {
-      if (isDone) {
-        f(true, undefined, tx);
-        return;
+class ResultStream<T> {
+  private currentPromise: Promise<void>;
+  private gotCursorEnd: boolean = false;
+  private awaitingResult: boolean = false;
+
+  constructor(private req: IDBRequest) {
+    this.awaitingResult = true;
+    let p = openPromise<void>();
+    this.currentPromise = p.promise;
+    req.onsuccess = () => {
+      if (!this.awaitingResult) {
+        throw Error("BUG: invariant violated");
       }
-      const values = this.flatMapFn(value);
-      for (const v in values) {
-        f(false, v, tx);
+      const cursor = req.result;
+      if (cursor) {
+        this.awaitingResult = false;
+        p.resolve();
+        p = openPromise<void>();
+        this.currentPromise = p.promise;
+      } else {
+        this.gotCursorEnd = true;
+        p.resolve();
       }
-    });
-  }
-}
-
-class QueryStreamMap<S, T> extends QueryStreamBase<T> {
-  constructor(public s: QueryStreamBase<S>, public mapFn: (v: S) => T) {
-    super(s.root);
+    };
+    req.onerror = () => {
+      p.reject(req.error);
+    };
   }
 
-  subscribe(f: SubscribeFn) {
-    this.s.subscribe((isDone, value, tx) => {
-      if (isDone) {
-        f(true, undefined, tx);
-        return;
+  async toArray(): Promise<T[]> {
+    const arr: T[] = [];
+    while (true) {
+      const x = await this.next();
+      if (x.hasValue) {
+        arr.push(x.value);
+      } else {
+        break;
       }
-      const mappedValue = this.mapFn(value);
-      f(false, mappedValue, tx);
-    });
-  }
-}
-
-class QueryStreamIndexJoin<T, S> extends QueryStreamBase<JoinResult<T, S>> {
-  constructor(
-    public s: QueryStreamBase<T>,
-    public storeName: string,
-    public indexName: string,
-    public key: any,
-  ) {
-    super(s.root);
+    }
+    return arr;
   }
 
-  subscribe(f: SubscribeFn) {
-    this.s.subscribe((isDone, value, tx) => {
-      if (isDone) {
-        f(true, undefined, tx);
-        return;
+  async map<R>(f: (x: T) => R): Promise<R[]> {
+    const arr: R[] = [];
+    while (true) {
+      const x = await this.next();
+      if (x.hasValue) {
+        arr.push(f(x.value));
+      } else {
+        break;
       }
-      const joinKey = this.key(value);
-      const s = tx.objectStore(this.storeName).index(this.indexName);
-      const req = s.openCursor(IDBKeyRange.only(joinKey));
-      req.onsuccess = () => {
-        const cursor = req.result;
-        if (cursor) {
-          f(false, { left: value, right: cursor.value }, tx);
-          cursor.continue();
-        }
-      };
-    });
-  }
-}
-
-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);
+    }
+    return arr;
   }
 
-  subscribe(f: SubscribeFn) {
-    this.s.subscribe((isDone, value, tx) => {
-      if (isDone) {
-        f(true, undefined, tx);
-        return;
+  async forEach(f: (x: T) => void): Promise<void> {
+    while (true) {
+      const x = await this.next();
+      if (x.hasValue) {
+        f(x.value);
+      } else {
+        break;
       }
-      const s = tx.objectStore(this.storeName).index(this.indexName);
-      const req = s.openCursor(IDBKeyRange.only(this.key(value)));
-      let gotMatch = false;
-      req.onsuccess = () => {
-        const cursor = req.result;
-        if (cursor) {
-          gotMatch = true;
-          f(false, { left: value, right: cursor.value }, tx);
-          cursor.continue();
-        } else {
-          if (!gotMatch) {
-            f(false, { left: value }, tx);
-          }
-        }
-      };
-    });
-  }
-}
-
-class QueryStreamKeyJoin<T, S> extends QueryStreamBase<JoinResult<T, S>> {
-  constructor(
-    public s: QueryStreamBase<T>,
-    public storeName: string,
-    public key: any,
-  ) {
-    super(s.root);
+    }
   }
 
-  subscribe(f: SubscribeFn) {
-    this.s.subscribe((isDone, value, tx) => {
-      if (isDone) {
-        f(true, undefined, tx);
-        return;
-      }
-      const s = tx.objectStore(this.storeName);
-      const req = s.openCursor(IDBKeyRange.only(this.key(value)));
-      req.onsuccess = () => {
-        const cursor = req.result;
-        if (cursor) {
-          f(false, { left: value, right: cursor.value }, tx);
-          cursor.continue();
-        } else {
-          f(true, undefined, tx);
+  async filter(f: (x: T) => boolean): Promise<T[]> {
+    const arr: T[] = [];
+    while (true) {
+      const x = await this.next();
+      if (x.hasValue) {
+        if (f(x.value)) {
+          arr.push(x.value)
         }
-      };
-    });
-  }
-}
-
-class IterQueryStream<T> extends QueryStreamBase<T> {
-  private storeName: string;
-  private options: any;
-  private subscribers: SubscribeFn[];
-
-  constructor(qr: QueryRoot, storeName: string, options: any) {
-    super(qr);
-    this.options = options;
-    this.storeName = storeName;
-    this.subscribers = [];
-
-    const doIt = (tx: IDBTransaction) => {
-      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);
       } else {
-        s = tx.objectStore(this.storeName);
-      }
-      let kr: IDBKeyRange | undefined;
-      if (only !== undefined) {
-        kr = IDBKeyRange.only(this.options.only);
+        break;
       }
-      const req = s.openCursor(kr);
-      req.onsuccess = () => {
-        const cursor: IDBCursorWithValue = req.result;
-        if (cursor) {
-          for (const f of this.subscribers) {
-            f(false, cursor.value, tx);
-          }
-          cursor.continue();
-        } else {
-          for (const f of this.subscribers) {
-            f(true, undefined, tx);
-          }
-        }
-      };
-    };
-
-    this.root.addWork(doIt);
+    }
+    return arr;
   }
 
-  subscribe(f: SubscribeFn) {
-    this.subscribers.push(f);
+  async next(): Promise<CursorResult<T>> {
+    if (this.gotCursorEnd) {
+      return { hasValue: false };
+    }
+    if (!this.awaitingResult) {
+      const cursor = this.req.result;
+      if (!cursor) {
+        throw Error("assertion failed");
+      }
+      this.awaitingResult = true;
+      cursor.continue();
+    }
+    await this.currentPromise;
+    if (this.gotCursorEnd) {
+      return { hasValue: false };
+    }
+    const cursor = this.req.result;
+    if (!cursor) {
+      throw Error("assertion failed");
+    }
+    return { hasValue: true, value: cursor.value };
   }
 }
 
-/**
- * Root wrapper around an IndexedDB for queries with a fluent interface.
- */
-export class QueryRoot {
-  private work: Array<(t: IDBTransaction) => void> = [];
-  private stores: Set<string> = new Set();
-  private kickoffPromise: Promise<void>;
-
-  /**
-   * Some operations is a write operation,
-   * and we need to do a "readwrite" transaction/
-   */
-  private hasWrite: boolean;
-
-  private finishScheduled: boolean;
-
-  private finished: boolean = false;
+export function oneShotIter<T>(
+  db: IDBDatabase,
+  store: Store<T>
+): ResultStream<T> {
+  const tx = db.transaction([store.name], "readonly");
+  const req = tx.objectStore(store.name).openCursor();
+  return new ResultStream<T>(req);
+}
 
-  private keys: { [keyName: string]: IDBValidKey } = {};
+export function oneShotIterIndex<S extends IDBValidKey, T>(
+  db: IDBDatabase,
+  index: Index<S, T>,
+  query?: any,
+): ResultStream<T> {
+  const tx = db.transaction([index.storeName], "readonly");
+  const req = 
tx.objectStore(index.storeName).index(index.indexName).openCursor(query);
+  return new ResultStream<T>(req);
+}
 
-  constructor(public db: IDBDatabase) {}
+class TransactionHandle {
+  constructor(private tx: IDBTransaction) {}
 
-  /**
-   * Get a named key that was created during the query.
-   */
-  key(keyName: string): IDBValidKey | undefined {
-    return this.keys[keyName];
+  put<T>(store: Store<T>, value: T, key?: any): Promise<any> {
+    const req = this.tx.objectStore(store.name).put(value, key);
+    return requestToPromise(req);
   }
 
-  private checkFinished() {
-    if (this.finished) {
-      throw Error("Can't add work to query after it was started");
-    }
-  }
-
-  /**
-   * Get a stream of all objects in the store.
-   */
-  iter<T>(store: Store<T>): QueryStream<T> {
-    this.checkFinished();
-    this.stores.add(store.name);
-    this.scheduleFinish();
-    return new IterQueryStream<T>(this, store.name, {});
+  add<T>(store: Store<T>, value: T, key?: any): Promise<any> {
+    const req = this.tx.objectStore(store.name).add(value, key);
+    return requestToPromise(req);
   }
 
-  /**
-   * Count the number of objects in a store.
-   */
-  count<T>(store: Store<T>): Promise<number> {
-    this.checkFinished();
-    const { resolve, promise } = openPromise<number>();
-
-    const doCount = (tx: IDBTransaction) => {
-      const s = tx.objectStore(store.name);
-      const req = s.count();
-      req.onsuccess = () => {
-        resolve(req.result);
-      };
-    };
-
-    this.addWork(doCount, store.name, false);
-    return Promise.resolve()
-      .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 {
-    this.checkFinished();
-    const doDeleteIf = (tx: IDBTransaction) => {
-      const s = tx.objectStore(store.name);
-      const req = s.openCursor();
-      let n = 0;
-      req.onsuccess = () => {
-        const cursor: IDBCursorWithValue | null = req.result;
-        if (cursor) {
-          if (predicate(cursor.value, n++)) {
-            cursor.delete();
-          }
-          cursor.continue();
-        }
-      };
-    };
-    this.addWork(doDeleteIf, store.name, true);
-    return this;
+  get<T>(store: Store<T>, key: any): Promise<T | undefined> {
+    const req = this.tx.objectStore(store.name).get(key);
+    return requestToPromise(req);
   }
 
-  iterIndex<S extends IDBValidKey, T>(
-    index: Index<S, T>,
-    only?: S,
-  ): QueryStream<T> {
-    this.checkFinished();
-    this.stores.add(index.storeName);
-    this.scheduleFinish();
-    return new IterQueryStream<T>(this, index.storeName, {
-      indexName: index.indexName,
-      only,
-    });
+  iter<T>(store: Store<T>, key?: any): ResultStream<T> {
+    const req = this.tx.objectStore(store.name).openCursor(key);
+    return new ResultStream<T>(req); 
   }
 
-  /**
-   * Put an object into the given object store.
-   * Overrides if an existing object with the same key exists
-   * in the store.
-   */
-  put<T>(store: Store<T>, val: T, keyName?: string): QueryRoot {
-    this.checkFinished();
-    const doPut = (tx: IDBTransaction) => {
-      const req = tx.objectStore(store.name).put(val);
-      if (keyName) {
-        req.onsuccess = () => {
-          this.keys[keyName] = req.result;
-        };
-      }
-    };
-    this.scheduleFinish();
-    this.addWork(doPut, store.name, true);
-    return this;
+  delete<T>(store: Store<T>, key: any): Promise<void> {
+    const req = this.tx.objectStore(store.name).delete(key);
+    return requestToPromise(req);
   }
 
-  /**
-   * Put an object into a store or return an existing record.
-   */
-  putOrGetExisting<T>(store: Store<T>, val: T, key: IDBValidKey): Promise<T> {
-    this.checkFinished();
-    const { resolve, promise } = openPromise<T>();
-    const doPutOrGet = (tx: IDBTransaction) => {
-      const objstore = tx.objectStore(store.name);
-      const req = objstore.get(key);
-      req.onsuccess = () => {
-        if (req.result !== undefined) {
-          resolve(req.result);
-        } else {
-          const req2 = objstore.add(val);
-          req2.onsuccess = () => {
-            resolve(val);
-          };
-        }
-      };
-    };
-    this.scheduleFinish();
-    this.addWork(doPutOrGet, store.name, true);
-    return promise;
+  mutate<T>(store: Store<T>, key: any, f: (x: T) => T | undefined) {
+    const req = this.tx.objectStore(store.name).openCursor(key);
+    return applyMutation(req, f);
   }
+}
 
-  putWithResult<T>(store: Store<T>, val: T): Promise<IDBValidKey> {
-    this.checkFinished();
-    const { resolve, promise } = openPromise<IDBValidKey>();
-    const doPutWithResult = (tx: IDBTransaction) => {
-      const req = tx.objectStore(store.name).put(val);
-      req.onsuccess = () => {
-        resolve(req.result);
-      };
-      this.scheduleFinish();
+export function runWithWriteTransaction<T>(
+  db: IDBDatabase,
+  stores: Store<any>[],
+  f: (t: TransactionHandle) => Promise<T>,
+): Promise<T> {
+  return new Promise((resolve, reject) => {
+    const storeName = stores.map(x => x.name);
+    const tx = db.transaction(storeName, "readwrite");
+    let funResult: any = undefined;
+    let gotFunResult: boolean = false;
+    tx.onerror = () => {
+      console.error("error in transaction:", tx.error);
+      reject(tx.error);
     };
-    this.addWork(doPutWithResult, store.name, true);
-    return Promise.resolve()
-      .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 {
-    this.checkFinished();
-    const doPut = (tx: IDBTransaction) => {
-      const req = tx.objectStore(store.name).openCursor(IDBKeyRange.only(key));
-      req.onsuccess = () => {
-        const cursor = req.result;
-        if (cursor) {
-          const value = cursor.value;
-          let modifiedValue: T | undefined;
-          try {
-            modifiedValue = f(value);
-          } catch (e) {
-            if (e === AbortTransaction) {
-              tx.abort();
-              return;
-            }
-            throw e;
-          }
-          if (modifiedValue !== undefined && modifiedValue !== null) {
-            cursor.update(modifiedValue);
-          }
-          cursor.continue();
-        }
-      };
-    };
-    this.scheduleFinish();
-    this.addWork(doPut, store.name, true);
-    return this;
-  }
-
-  /**
-   * Add all object from an iterable to the given object store.
-   */
-  putAll<T>(store: Store<T>, iterable: T[]): QueryRoot {
-    this.checkFinished();
-    const doPutAll = (tx: IDBTransaction) => {
-      for (const obj of iterable) {
-        tx.objectStore(store.name).put(obj);
+    tx.oncomplete = () => {
+      // This is a fatal error: The transaction completed *before*
+      // the transaction function returned.  Likely, the transaction
+      // function waited on a promise that is *not* resolved in the
+      // microtask queue, thus triggering the auto-commit behavior.
+      // Unfortunately, the auto-commit behavior of IDB can't be switched
+      // of.  There are some proposals to add this functionality in the future.
+      if (!gotFunResult) {
+        const msg =
+          "BUG: transaction closed before transaction function returned";
+        console.error(msg);
+        reject(Error(msg));
       }
+      resolve(funResult);
     };
-    this.scheduleFinish();
-    this.addWork(doPutAll, store.name, true);
-    return this;
-  }
-
-  /**
-   * Add an object to the given object store.
-   * Fails if the object's key is already present
-   * in the object store.
-   */
-  add<T>(store: Store<T>, val: T): QueryRoot {
-    this.checkFinished();
-    const doAdd = (tx: IDBTransaction) => {
-      tx.objectStore(store.name).add(val);
+    tx.onabort = () => {
+      console.error("aborted transaction");
+      reject(AbortTransaction);
     };
-    this.scheduleFinish();
-    this.addWork(doAdd, store.name, true);
-    return this;
-  }
-
-  /**
-   * Get one object from a store by its key.
-   */
-  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<T | undefined>();
-
-    const doGet = (tx: IDBTransaction) => {
-      const req = tx.objectStore(store.name).get(key);
-      req.onsuccess = () => {
-        resolve(req.result);
-      };
-    };
-
-    this.addWork(doGet, store.name, false);
-    return Promise.resolve()
-      .then(() => this.finish())
-      .then(() => promise);
-  }
+    const th = new TransactionHandle(tx);
+    const resP = f(th);
+    resP.then(result => {
+      gotFunResult = true;
+      funResult = result;
+    });
+  });
+}
 
+/**
+ * Definition of an index.
+ */
+export class Index<S extends IDBValidKey, T> {
   /**
-   * Get get objects from a store by their keys.
-   * If no object for a key exists, the resulting position in the array
-   * contains 'undefined'.
+   * Name of the store that this index is associated with.
    */
-  getMany<T>(store: Store<T>, keys: any[]): Promise<T[]> {
-    this.checkFinished();
-
-    const { resolve, promise } = openPromise<T[]>();
-    const results: T[] = [];
-
-    const doGetMany = (tx: IDBTransaction) => {
-      for (const key of keys) {
-        if (key === void 0) {
-          throw Error("key must not be undefined");
-        }
-        const req = tx.objectStore(store.name).get(key);
-        req.onsuccess = () => {
-          results.push(req.result);
-          if (results.length === keys.length) {
-            resolve(results);
-          }
-        };
-      }
-    };
-
-    this.addWork(doGetMany, store.name, false);
-    return Promise.resolve()
-      .then(() => this.finish())
-      .then(() => promise);
-  }
+  storeName: string;
 
   /**
-   * Get one object from a store by its key.
+   * Options to use for the index.
    */
-  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<T | undefined>();
+  options: IndexOptions;
 
-    const doGetIndexed = (tx: IDBTransaction) => {
-      const req = tx
-        .objectStore(index.storeName)
-        .index(index.indexName)
-        .get(key);
-      req.onsuccess = () => {
-        resolve(req.result);
-      };
+  constructor(
+    s: Store<T>,
+    public indexName: string,
+    public keyPath: string | string[],
+    options?: IndexOptions,
+  ) {
+    const defaultOptions = {
+      multiEntry: false,
     };
-
-    this.addWork(doGetIndexed, index.storeName, false);
-    return Promise.resolve()
-      .then(() => this.finish())
-      .then(() => promise);
-  }
-
-  private scheduleFinish() {
-    if (!this.finishScheduled) {
-      Promise.resolve().then(() => this.finish());
-      this.finishScheduled = true;
-    }
-  }
-
-  /**
-   * Finish the query, and start the query in the first place if necessary.
-   */
-  finish(): Promise<void> {
-    if (this.kickoffPromise) {
-      return this.kickoffPromise;
-    }
-    this.kickoffPromise = new Promise<void>((resolve, reject) => {
-      // At this point, we can't add any more work
-      this.finished = true;
-      if (this.work.length === 0) {
-        resolve();
-        return;
-      }
-      const mode = this.hasWrite ? "readwrite" : "readonly";
-      const tx = this.db.transaction(Array.from(this.stores), mode);
-      tx.oncomplete = () => {
-        resolve();
-      };
-      tx.onabort = () => {
-        console.warn(
-          `aborted ${mode} transaction on stores [${[...this.stores]}]`,
-        );
-        reject(Error("transaction aborted"));
-      };
-      tx.onerror = e => {
-        console.warn(`error in transaction`, (e.target as any).error);
-      };
-      for (const w of this.work) {
-        w(tx);
-      }
-    });
-    return this.kickoffPromise;
+    this.options = { ...defaultOptions, ...(options || {}) };
+    this.storeName = s.name;
   }
 
   /**
-   * Delete an object by from the given object store.
+   * We want to have the key type parameter in use somewhere,
+   * because otherwise the compiler complains.  In iterIndex the
+   * key type is pretty useful.
    */
-  delete<T>(store: Store<T>, key: any): QueryRoot {
-    this.checkFinished();
-    const doDelete = (tx: IDBTransaction) => {
-      tx.objectStore(store.name).delete(key);
-    };
-    this.scheduleFinish();
-    this.addWork(doDelete, store.name, true);
-    return this;
-  }
+  protected _dummyKey: S | undefined;
+}
 
-  /**
-   * Low-level function to add a task to the internal work queue.
-   */
-  addWork(
-    workFn: (t: IDBTransaction) => void,
-    storeName?: string,
-    isWrite?: boolean,
-  ) {
-    this.work.push(workFn);
-    if (storeName) {
-      this.addStoreAccess(storeName, isWrite);
-    }
-  }
 
-  addStoreAccess(storeName: string, isWrite?: boolean) {
-    if (storeName) {
-      this.stores.add(storeName);
-    }
-    if (isWrite) {
-      this.hasWrite = true;
-    }
-  }
-}
+/**
+ * Exception that should be thrown by client code to abort a transaction.
+ */
+export const AbortTransaction = Symbol("abort_transaction");
diff --git a/src/wallet.ts b/src/wallet.ts
index f5219c45..32b0833c 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -29,14 +29,19 @@ import {
   canonicalizeBaseUrl,
   getTalerStampSec,
   strcmp,
+  extractTalerStamp,
 } from "./helpers";
 import { HttpRequestLibrary, RequestException } from "./http";
 import * as LibtoolVersion from "./libtoolVersion";
 import {
   AbortTransaction,
-  JoinLeftResult,
-  JoinResult,
-  QueryRoot,
+  oneShotPut,
+  oneShotGet,
+  runWithWriteTransaction,
+  oneShotIter,
+  oneShotIterIndex,
+  oneShotGetIndexed,
+  oneShotMutate,
 } from "./query";
 import { TimerGroup } from "./timer";
 
@@ -63,6 +68,8 @@ import {
   TipRecord,
   WireFee,
   WithdrawalRecord,
+  ExchangeDetails,
+  ExchangeUpdateStatus,
 } from "./dbTypes";
 import {
   Auditor,
@@ -110,6 +117,8 @@ import {
   PendingOperationInfo,
   PendingOperationsResponse,
   HistoryQuery,
+  getTimestampNow,
+  OperationError,
 } from "./walletTypes";
 import { openPromise } from "./promiseUtils";
 import {
@@ -338,6 +347,18 @@ interface CoinsForPaymentArgs {
   wireMethod: string;
 }
 
+/**
+ * This error is thrown when an
+ */
+class OperationFailedAndReportedError extends Error {
+  constructor(public reason: Error) {
+    super("Reported failed operation: " + reason.message);
+
+    // Set the prototype explicitly.
+    Object.setPrototypeOf(this, OperationFailedAndReportedError.prototype);
+  }
+}
+
 /**
  * The platform-independent wallet implementation.
  */
@@ -372,10 +393,6 @@ export class Wallet {
    */
   private runningOperations: Set<string> = new Set();
 
-  q(): QueryRoot {
-    return new QueryRoot(this.db);
-  }
-
   constructor(
     db: IDBDatabase,
     http: HttpRequestLibrary,
@@ -389,31 +406,49 @@ export class Wallet {
     this.notifier = notifier;
     this.cryptoApi = new CryptoApi(cryptoWorkerFactory);
     this.timerGroup = new TimerGroup();
+  }
 
-    const init = async () => {
-      await this.fillDefaults().catch(e => console.log(e));
+  public async processPending(): Promise<void> {
+
+    const exchangeBaseUrlList = await oneShotIter(this.db, 
Stores.exchanges).map((x) => x.baseUrl);
+
+    for (let exchangeBaseUrl of exchangeBaseUrlList) {
+      await this.updateExchangeFromUrl(exchangeBaseUrl);
+    }
+  }
+
+  /**
+   * Start processing pending operations asynchronously.
+   */
+  public start() {
+    const work = async () => {
       await this.collectGarbage().catch(e => console.log(e));
       this.updateExchanges();
       this.resumePendingFromDb();
       this.timerGroup.every(1000 * 60 * 15, () => this.updateExchanges());
     };
-
-    init();
+    work();
   }
 
-  private async fillDefaults() {
-    const onTrue = (r: QueryRoot) => {};
-    const onFalse = (r: QueryRoot) => {
-      Wallet.enableTracing && console.log("applying defaults");
-      r.put(Stores.config, { key: "currencyDefaultsApplied", value: true })
-        .putAll(Stores.currencies, builtinCurrencies)
-        .finish();
-    };
-    await this.q()
-      .iter(Stores.config)
-      .filter(x => x.key === "currencyDefaultsApplied")
-      .first()
-      .cond(x => x && x.value, onTrue, onFalse);
+  /**
+   * Insert the hard-coded defaults for exchanges, coins and
+   * auditors into the database, unless these defaults have
+   * already been applied.
+   */
+  async fillDefaults() {
+      await runWithWriteTransaction(this.db, [Stores.config, 
Stores.currencies], async (tx) => {
+        let applied = false;
+        await tx.iter(Stores.config).forEach((x) => {
+          if (x.key == "currencyDefaultsApplied" && x.value == true) {
+            applied = true;
+          }
+        });
+        if (!applied) {
+          for (let c of builtinCurrencies) {
+            await tx.put(Stores.currencies, c);
+          }
+        }
+      });
   }
 
   private startOperation(operationId: string) {
@@ -429,12 +464,9 @@ export class Wallet {
   }
 
   async updateExchanges(): Promise<void> {
-    const exchangesUrls = await this.q()
-      .iter(Stores.exchanges)
-      .map(e => e.baseUrl)
-      .toArray();
+    const exchangeUrls = await oneShotIter(this.db, Stores.exchanges).map((e) 
=> e.baseUrl);
 
-    for (const url of exchangesUrls) {
+    for (const url of exchangeUrls) {
       this.updateExchangeFromUrl(url).catch(e => {
         console.error("updating exchange failed", e);
       });
@@ -448,69 +480,46 @@ export class Wallet {
   private resumePendingFromDb(): void {
     Wallet.enableTracing && console.log("resuming pending operations from db");
 
-    this.q()
-      .iter(Stores.reserves)
-      .forEach(reserve => {
+    oneShotIter(this.db, Stores.reserves).forEach(reserve => {
         Wallet.enableTracing &&
           console.log("resuming reserve", reserve.reserve_pub);
         this.processReserve(reserve.reserve_pub);
-      });
-
-    this.q()
-      .iter(Stores.precoins)
-      .forEach(preCoin => {
-        Wallet.enableTracing && console.log("resuming precoin");
-        this.processPreCoin(preCoin.coinPub);
-      });
+    });
 
-    this.q()
-      .iter(Stores.refresh)
-      .forEach((r: RefreshSessionRecord) => {
-        this.continueRefreshSession(r);
-      });
+    oneShotIter(this.db, Stores.precoins).forEach(preCoin => {
+      Wallet.enableTracing && console.log("resuming precoin");
+      this.processPreCoin(preCoin.coinPub);
+    });
 
-    this.q()
-      .iter(Stores.coinsReturns)
-      .forEach((r: CoinsReturnRecord) => {
-        this.depositReturnedCoins(r);
-      });
+    oneShotIter(this.db, Stores.refresh).forEach((r: RefreshSessionRecord) => {
+      this.continueRefreshSession(r);
+    });
 
-    // FIXME: optimize via index
-    this.q()
-      .iter(Stores.coins)
-      .forEach((c: CoinRecord) => {
-        if (c.status === CoinStatus.Dirty) {
-          Wallet.enableTracing &&
-            console.log("resuming pending refresh for coin", c);
-          this.refresh(c.coinPub);
-        }
-      });
+    oneShotIter(this.db, Stores.coinsReturns).forEach((r: CoinsReturnRecord) 
=> {
+      this.depositReturnedCoins(r);
+    });
   }
 
   private async getCoinsForReturn(
     exchangeBaseUrl: string,
     amount: AmountJson,
   ): Promise<CoinWithDenom[] | undefined> {
-    const exchange = await this.q().get(Stores.exchanges, exchangeBaseUrl);
+    const exchange = await oneShotGet(this.db, Stores.exchanges, 
exchangeBaseUrl);
     if (!exchange) {
       throw Error(`Exchange ${exchangeBaseUrl} not known to the wallet`);
     }
 
-    const coins: CoinRecord[] = await this.q()
-      .iterIndex(Stores.coins.exchangeBaseUrlIndex, exchange.baseUrl)
-      .toArray();
+    const coins: CoinRecord[] = await oneShotIterIndex(this.db, 
Stores.coins.exchangeBaseUrlIndex, exchange.baseUrl).toArray();
 
     if (!coins || !coins.length) {
       return [];
     }
 
-    const denoms = await this.q()
-      .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl)
-      .toArray();
+    const denoms = await oneShotIterIndex(this.db, 
Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl).toArray();
 
     // Denomination of the first coin, we assume that all other
     // coins have the same currency
-    const firstDenom = await this.q().get(Stores.denominations, [
+    const firstDenom = await oneShotGet(this.db, Stores.denominations, [
       exchange.baseUrl,
       coins[0].denomPub,
     ]);
@@ -521,7 +530,7 @@ export class Wallet {
 
     const cds: CoinWithDenom[] = [];
     for (const coin of coins) {
-      const denom = await this.q().get(Stores.denominations, [
+      const denom = await oneShotGet(this.db, Stores.denominations, [
         exchange.baseUrl,
         coin.denomPub,
       ]);
@@ -572,16 +581,22 @@ export class Wallet {
 
     let remainingAmount = paymentAmount;
 
-    const exchanges = await this.q()
-      .iter(Stores.exchanges)
-      .toArray();
+    const exchanges = await oneShotIter(this.db, Stores.exchanges).toArray();
 
     for (const exchange of exchanges) {
       let isOkay: boolean = false;
+      const exchangeDetails = exchange.details;
+      if (!exchangeDetails) {
+        continue;
+      }
+      const exchangeFees = exchange.wireInfo;
+      if (!exchangeFees) {
+        continue;
+      }
 
       // is the exchange explicitly allowed?
       for (const allowedExchange of allowedExchanges) {
-        if (allowedExchange.master_pub === exchange.masterPublicKey) {
+        if (allowedExchange.master_pub === exchangeDetails.masterPublicKey) {
           isOkay = true;
           break;
         }
@@ -590,7 +605,7 @@ export class Wallet {
       // is the exchange allowed because of one of its auditors?
       if (!isOkay) {
         for (const allowedAuditor of allowedAuditors) {
-          for (const auditor of exchange.auditors) {
+          for (const auditor of exchangeDetails.auditors) {
             if (auditor.auditor_pub === allowedAuditor.auditor_pub) {
               isOkay = true;
               break;
@@ -606,20 +621,17 @@ export class Wallet {
         continue;
       }
 
-      const coins: CoinRecord[] = await this.q()
-        .iterIndex(Stores.coins.exchangeBaseUrlIndex, exchange.baseUrl)
-        .toArray();
+      const coins = await oneShotIterIndex(this.db, 
Stores.coins.exchangeBaseUrlIndex, exchange.baseUrl).toArray();
+
+      const denoms = await oneShotIterIndex(this.db, 
Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl).toArray();
 
-      const denoms = await this.q()
-        .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl)
-        .toArray();
       if (!coins || coins.length === 0) {
         continue;
       }
 
       // Denomination of the first coin, we assume that all other
       // coins have the same currency
-      const firstDenom = await this.q().get(Stores.denominations, [
+      const firstDenom = await oneShotGet(this.db, Stores.denominations, [
         exchange.baseUrl,
         coins[0].denomPub,
       ]);
@@ -629,7 +641,7 @@ export class Wallet {
       const currency = firstDenom.value.currency;
       const cds: CoinWithDenom[] = [];
       for (const coin of coins) {
-        const denom = await this.q().get(Stores.denominations, [
+        const denom = await oneShotGet(this.db, Stores.denominations, [
           exchange.baseUrl,
           coin.denomPub,
         ]);
@@ -651,18 +663,9 @@ export class Wallet {
         cds.push({ coin, denom });
       }
 
-      const fees = await this.q().get(
-        Stores.exchangeWireFees,
-        exchange.baseUrl,
-      );
-      if (!fees) {
-        console.error("no fees found for exchange", exchange);
-        continue;
-      }
-
       let totalFees = Amounts.getZero(currency);
       let wireFee: AmountJson | undefined;
-      for (const fee of fees.feesForType[wireMethod] || []) {
+      for (const fee of exchangeFees.feesForType[wireMethod] || []) {
         if (fee.startStamp <= wireFeeTime && fee.endStamp >= wireFeeTime) {
           wireFee = fee.wireFee;
           break;
@@ -723,10 +726,13 @@ export class Wallet {
       timestamp_refund: 0,
     };
 
-    await this.q()
-      .put(Stores.purchases, t)
-      .putAll(Stores.coins, payCoinInfo.updatedCoins)
-      .finish();
+    await runWithWriteTransaction(this.db, [Stores.coins, Stores.purchases], 
async (tx) => {
+      await tx.put(Stores.purchases, t);
+      for (let c of payCoinInfo.updatedCoins) {
+        await tx.put(Stores.coins, c);
+      }
+    });
+
     this.badge.showNotification();
     this.notifier.notify();
     return t;
@@ -773,7 +779,8 @@ export class Wallet {
 
     console.log("proposal", proposal);
 
-    const differentPurchase = await this.q().getIndexed(
+    const differentPurchase = await oneShotGetIndexed(
+      this.db,
       Stores.purchases.fulfillmentUrlIndex,
       proposal.contractTerms.fulfillment_url,
     );
@@ -805,10 +812,7 @@ export class Wallet {
     }
 
     // First check if we already payed for it.
-    const purchase = await this.q().get(
-      Stores.purchases,
-      proposal.contractTermsHash,
-    );
+    const purchase = await oneShotGet(this.db, Stores.purchases, 
proposal.contractTermsHash);
 
     if (!purchase) {
       const paymentAmount = 
Amounts.parseOrThrow(proposal.contractTerms.amount);
@@ -890,10 +894,7 @@ export class Wallet {
    *  downloaded in the context of a session ID.
    */
   async downloadProposal(url: string, sessionId?: string): Promise<number> {
-    const oldProposal = await this.q().getIndexed(
-      Stores.proposals.urlIndex,
-      url,
-    );
+    const oldProposal = await oneShotGetIndexed(this.db, 
Stores.proposals.urlIndex, url);
     if (oldProposal) {
       return oldProposal.id!;
     }
@@ -924,7 +925,7 @@ export class Wallet {
       downloadSessionId: sessionId,
     };
 
-    const id = await this.q().putWithResult(Stores.proposals, proposalRecord);
+    const id = await oneShotPut(this.db, Stores.proposals, proposalRecord);
     this.notifier.notify();
     if (typeof id !== "number") {
       throw Error("db schema wrong");
@@ -934,19 +935,15 @@ export class Wallet {
 
   async refundFailedPay(proposalId: number) {
     console.log(`refunding failed payment with proposal id ${proposalId}`);
-    const proposal: ProposalDownloadRecord | undefined = await this.q().get(
-      Stores.proposals,
-      proposalId,
-    );
-
+    const proposal = await oneShotGet(this.db, Stores.proposals, proposalId);
     if (!proposal) {
       throw Error(`proposal with id ${proposalId} not found`);
     }
 
-    const purchase = await this.q().get(
+    const purchase = await oneShotGet(this.db, 
       Stores.purchases,
-      proposal.contractTermsHash,
-    );
+      proposal.contractTermsHash);
+
     if (!purchase) {
       throw Error("purchase not found for proposal");
     }
@@ -960,7 +957,7 @@ export class Wallet {
     contractTermsHash: string,
     sessionId: string | undefined,
   ): Promise<ConfirmPayResult> {
-    const purchase = await this.q().get(Stores.purchases, contractTermsHash);
+    const purchase = await oneShotGet(this.db, Stores.purchases, 
contractTermsHash);
     if (!purchase) {
       throw Error("Purchase not found: " + contractTermsHash);
     }
@@ -998,7 +995,7 @@ export class Wallet {
     purchase.finished = true;
     const modifiedCoins: CoinRecord[] = [];
     for (const pc of purchase.payReq.coins) {
-      const c = await this.q().get<CoinRecord>(Stores.coins, pc.coin_pub);
+      const c = await oneShotGet(this.db, Stores.coins, pc.coin_pub);
       if (!c) {
         console.error("coin not found");
         throw Error("coin used in payment not found");
@@ -1007,10 +1004,13 @@ export class Wallet {
       modifiedCoins.push(c);
     }
 
-    await this.q()
-      .putAll(Stores.coins, modifiedCoins)
-      .put(Stores.purchases, purchase)
-      .finish();
+    await runWithWriteTransaction(this.db, [Stores.coins, Stores.purchases], 
async (tx) => {
+      for (let c of modifiedCoins) {
+        tx.put(Stores.coins, c);
+      }
+      tx.put(Stores.purchases, purchase);
+    });
+
     for (const c of purchase.payReq.coins) {
       this.refresh(c.coin_pub);
     }
@@ -1031,9 +1031,7 @@ export class Wallet {
    */
   async refreshDirtyCoins(): Promise<{ numRefreshed: number }> {
     let n = 0;
-    const coins = await this.q()
-      .iter(Stores.coins)
-      .toArray();
+    const coins = await oneShotIter(this.db, Stores.coins).toArray();
     for (let coin of coins) {
       if (coin.status == CoinStatus.Dirty) {
         try {
@@ -1059,10 +1057,7 @@ export class Wallet {
       console.log(
         `executing confirmPay with proposalId ${proposalId} and 
sessionIdOverride ${sessionIdOverride}`,
       );
-    const proposal: ProposalDownloadRecord | undefined = await this.q().get(
-      Stores.proposals,
-      proposalId,
-    );
+    const proposal = await oneShotGet(this.db, Stores.proposals, proposalId);
 
     if (!proposal) {
       throw Error(`proposal with id ${proposalId} not found`);
@@ -1070,10 +1065,9 @@ export class Wallet {
 
     const sessionId = sessionIdOverride || proposal.downloadSessionId;
 
-    let purchase = await this.q().get(
+    let purchase = await oneShotGet(this.db, 
       Stores.purchases,
-      proposal.contractTermsHash,
-    );
+      proposal.contractTermsHash,);
 
     if (purchase) {
       return this.submitPay(purchase.contractTermsHash, sessionId);
@@ -1145,7 +1139,13 @@ export class Wallet {
       return;
     }
     const coinKeys = sp.payCoinInfo.updatedCoins.map(x => x.coinPub);
-    const coins = await this.q().getMany(Stores.coins, coinKeys);
+    const coins: CoinRecord[] = [];
+    for (let coinKey of coinKeys) {
+      const cc = await oneShotGet(this.db, Stores.coins, coinKey);
+      if (cc) {
+        coins.push(cc);
+      }
+    }
     for (let i = 0; i < coins.length; i++) {
       const specCoin = sp.payCoinInfo.originalCoins[i];
       const currentCoin = coins[i];
@@ -1164,13 +1164,12 @@ export class Wallet {
   }
 
   /**
-   * Send reserve details 
+   * Send reserve details
    */
   private async sendReserveInfoToBank(reservePub: string) {
-    const reserve = await this.q().get<ReserveRecord>(
+    const reserve = await oneShotGet(this.db, 
       Stores.reserves,
-      reservePub,
-    );
+      reservePub);
     if (!reserve) {
       throw Error("reserve not in db");
     }
@@ -1191,7 +1190,7 @@ export class Wallet {
     }
 
     if (status.transfer_done) {
-      await this.q().mutate(Stores.reserves, reservePub, r => {
+      await oneShotMutate(this.db, Stores.reserves, reservePub, (r) => {
         r.timestamp_confirmed = now;
         return r;
       });
@@ -1207,7 +1206,7 @@ export class Wallet {
         console.log("bank error response", e);
         throw e;
       }
-      await this.q().mutate(Stores.reserves, reservePub, r => {
+      await oneShotMutate(this.db, Stores.reserves, reservePub, (r) => {
         r.timestamp_reserve_info_posted = now;
         return r;
       });
@@ -1238,10 +1237,7 @@ export class Wallet {
       // Sometimes though, we want to try again faster.
       let maxTimeout = 3000 * 60;
       try {
-        const reserve = await this.q().get<ReserveRecord>(
-          Stores.reserves,
-          reservePub,
-        );
+        const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
         if (!reserve) {
           isHardError = true;
           throw Error("reserve not in db");
@@ -1302,7 +1298,7 @@ export class Wallet {
     const op = openPromise<void>();
 
     const processPreCoinInternal = async (retryDelayMs: number = 200) => {
-      const preCoin = await this.q().get(Stores.precoins, preCoinPub);
+      const preCoin = await oneShotGet(this.db, Stores.precoins, preCoinPub);
       if (!preCoin) {
         console.log("processPreCoin: preCoinPub not found");
         return;
@@ -1325,15 +1321,14 @@ export class Wallet {
       this.processPreCoinConcurrent++;
 
       try {
-        const exchange = await this.q().get(
+        const exchange = await oneShotGet(this.db, 
           Stores.exchanges,
-          preCoin.exchangeBaseUrl,
-        );
+          preCoin.exchangeBaseUrl,);
         if (!exchange) {
           console.error("db inconsistent: exchange for precoin not found");
           return;
         }
-        const denom = await this.q().get(Stores.denominations, [
+        const denom = await oneShotGet(this.db, Stores.denominations, [
           preCoin.exchangeBaseUrl,
           preCoin.denomPub,
         ]);
@@ -1358,11 +1353,11 @@ export class Wallet {
           return r;
         };
 
-        await this.q()
-          .mutate(Stores.reserves, preCoin.reservePub, mutateReserve)
-          .delete(Stores.precoins, coin.coinPub)
-          .add(Stores.coins, coin)
-          .finish();
+        await runWithWriteTransaction(this.db, [Stores.reserves, 
Stores.precoins, Stores.coins], async (tx) => {
+          await tx.mutate(Stores.reserves, preCoin.reservePub, mutateReserve);
+          await tx.delete(Stores.precoins, coin.coinPub);
+          await tx.add(Stores.coins, coin);
+        });
 
         this.badge.showNotification();
 
@@ -1402,20 +1397,6 @@ export class Wallet {
     }
   }
 
-  /**
-   * Update the timestamp of when an exchange was used.
-   */
-  async updateExchangeUsedTime(exchangeBaseUrl: string): Promise<void> {
-    const now = new Date().getTime();
-    const update = (r: ExchangeRecord) => {
-      r.lastUsedTime = now;
-      return r;
-    };
-    await this.q()
-      .mutate(Stores.exchanges, exchangeBaseUrl, update)
-      .finish();
-  }
-
   /**
    * Create a reserve, but do not flag it as confirmed yet.
    *
@@ -1451,38 +1432,38 @@ export class Wallet {
       const rec = {
         paytoUri: senderWire,
       };
-      await this.q()
-        .put(Stores.senderWires, rec)
-        .finish();
+      await oneShotPut(this.db, Stores.senderWires, rec);
     }
 
-    await this.updateExchangeUsedTime(req.exchange);
     const exchangeInfo = await this.updateExchangeFromUrl(req.exchange);
+    const exchangeDetails = exchangeInfo.details;
+    if (!exchangeDetails) {
+      throw Error("exchange not updated");
+    }
     const { isAudited, isTrusted } = await this.getExchangeTrust(exchangeInfo);
-    let currencyRecord = await this.q().get(
-      Stores.currencies,
-      exchangeInfo.currency,
-    );
+    let currencyRecord = await oneShotGet(this.db, Stores.currencies, 
exchangeDetails.currency);
     if (!currencyRecord) {
       currencyRecord = {
         auditors: [],
         exchanges: [],
         fractionalDigits: 2,
-        name: exchangeInfo.currency,
+        name: exchangeDetails.currency,
       };
     }
 
     if (!isAudited && !isTrusted) {
       currencyRecord.exchanges.push({
         baseUrl: req.exchange,
-        exchangePub: exchangeInfo.masterPublicKey,
+        exchangePub: exchangeDetails.masterPublicKey,
       });
     }
 
-    await this.q()
-      .put(Stores.currencies, currencyRecord)
-      .put(Stores.reserves, reserveRecord)
-      .finish();
+    const cr: CurrencyRecord = currencyRecord;
+
+    runWithWriteTransaction(this.db, [Stores.currencies, Stores.reserves], 
async (tx) => {
+      await tx.put(Stores.currencies, cr);
+      await tx.put(Stores.reserves, reserveRecord);
+    });
 
     if (req.bankWithdrawStatusUrl) {
       this.processReserve(keypair.pub);
@@ -1506,17 +1487,13 @@ export class Wallet {
    */
   async confirmReserve(req: ConfirmReserveRequest): Promise<void> {
     const now = new Date().getTime();
-    const reserve: ReserveRecord | undefined = await this.q().get<
-      ReserveRecord
-    >(Stores.reserves, req.reservePub);
+    const reserve = await oneShotGet(this.db, Stores.reserves, req.reservePub);
     if (!reserve) {
       console.error("Unable to confirm reserve, not found in DB");
       return;
     }
     reserve.timestamp_confirmed = now;
-    await this.q()
-      .put(Stores.reserves, reserve)
-      .finish();
+    await oneShotPut(this.db, Stores.reserves, reserve);
     this.notifier.notify();
 
     this.processReserve(reserve.reserve_pub);
@@ -1589,22 +1566,33 @@ export class Wallet {
       reservePub: reserve.reserve_pub,
       withdrawalAmount: Amounts.toString(withdrawAmount),
       startTimestamp: stampMsNow,
-    }
+    };
 
-    const preCoinRecords: PreCoinRecord[] = await 
Promise.all(denomsForWithdraw.map(async denom => {
-      return await this.cryptoApi.createPreCoin(denom, reserve);
-    }));
+    const preCoinRecords: PreCoinRecord[] = await Promise.all(
+      denomsForWithdraw.map(async denom => {
+        return await this.cryptoApi.createPreCoin(denom, reserve);
+      }),
+    );
 
-    const totalCoinValue = Amounts.sum(denomsForWithdraw.map(x => 
x.value)).amount
-    const totalCoinWithdrawFee = Amounts.sum(denomsForWithdraw.map(x => 
x.feeWithdraw)).amount
-    const totalWithdrawAmount = Amounts.add(totalCoinValue, 
totalCoinWithdrawFee).amount
+    const totalCoinValue = Amounts.sum(denomsForWithdraw.map(x => x.value))
+      .amount;
+    const totalCoinWithdrawFee = Amounts.sum(
+      denomsForWithdraw.map(x => x.feeWithdraw),
+    ).amount;
+    const totalWithdrawAmount = Amounts.add(
+      totalCoinValue,
+      totalCoinWithdrawFee,
+    ).amount;
 
     function mutateReserve(r: ReserveRecord): ReserveRecord {
       const currentAmount = r.current_amount;
       if (!currentAmount) {
         throw Error("can't withdraw when amount is unknown");
       }
-      r.precoin_amount = Amounts.add(r.precoin_amount, 
totalWithdrawAmount).amount;
+      r.precoin_amount = Amounts.add(
+        r.precoin_amount,
+        totalWithdrawAmount,
+      ).amount;
       const result = Amounts.sub(currentAmount, totalWithdrawAmount);
       if (result.saturated) {
         console.error("can't create precoins, saturated");
@@ -1623,11 +1611,13 @@ export class Wallet {
     // 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()
-        .putAll(Stores.precoins, preCoinRecords)
-        .put(Stores.withdrawals, withdrawalRecord)
-        .mutate(Stores.reserves, reserve.reserve_pub, mutateReserve)
-        .finish();
+      await runWithWriteTransaction(this.db, [Stores.precoins, 
Stores.withdrawals, Stores.reserves], async (tx) => {
+        for (let pcr of preCoinRecords) {
+          await tx.put(Stores.precoins, pcr);
+        }
+        await tx.mutate(Stores.reserves, reserve.reserve_pub, mutateReserve);
+        await tx.put(Stores.withdrawals, withdrawalRecord);
+      });
     } catch (e) {
       return;
     }
@@ -1642,10 +1632,7 @@ export class Wallet {
    * by quering the reserve's exchange.
    */
   private async updateReserve(reservePub: string): Promise<ReserveRecord> {
-    const reserve = await this.q().get<ReserveRecord>(
-      Stores.reserves,
-      reservePub,
-    );
+    const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
     if (!reserve) {
       throw Error("reserve not in db");
     }
@@ -1669,44 +1656,16 @@ export class Wallet {
       throw Error();
     }
     reserve.current_amount = Amounts.parseOrThrow(reserveInfo.balance);
-    await this.q()
-      .put(Stores.reserves, reserve)
-      .finish();
+    await oneShotPut(this.db, Stores.reserves, reserve);
     this.notifier.notify();
     return reserve;
   }
 
-  /**
-   * Get the wire information for the exchange with the given base URL.
-   */
-  async getWireInfo(exchangeBaseUrl: string): Promise<ExchangeWireJson> {
-    exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
-    const reqUrl = new URI("wire")
-      .absoluteTo(exchangeBaseUrl)
-      .addQuery("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
-    const resp = await this.http.get(reqUrl.href());
-
-    if (resp.status !== 200) {
-      throw Error("/wire request failed");
-    }
-
-    const wiJson = resp.responseJson;
-    if (!wiJson) {
-      throw Error("/wire response malformed");
-    }
-
-    return ExchangeWireJson.checked(wiJson);
-  }
-
-  async getPossibleDenoms(exchangeBaseUrl: string) {
-    return this.q()
-      .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl)
-      .filter(
-        d =>
-          d.status === DenominationStatus.Unverified ||
-          d.status === DenominationStatus.VerifiedGood,
-      )
-      .toArray();
+  async getPossibleDenoms(exchangeBaseUrl: string): 
Promise<DenominationRecord[]> {
+    return await oneShotIterIndex(this.db, 
Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl).filter((d) => {
+      return d.status === DenominationStatus.Unverified ||
+      d.status === DenominationStatus.VerifiedGood;
+    });
   }
 
   /**
@@ -1718,19 +1677,17 @@ export class Wallet {
   async getVerifiedSmallestWithdrawAmount(
     exchangeBaseUrl: string,
   ): Promise<AmountJson> {
-    const exchange = await this.q().get(Stores.exchanges, exchangeBaseUrl);
+    const exchange = await oneShotGet(this.db, Stores.exchanges, 
exchangeBaseUrl);
     if (!exchange) {
       throw Error(`exchange ${exchangeBaseUrl} not found`);
     }
+    const exchangeDetails = exchange.details;
+    if (!exchangeDetails) {
+      throw Error(`exchange ${exchangeBaseUrl} details not available`);
+    }
+
+    const possibleDenoms = await this.getPossibleDenoms(exchange.baseUrl);
 
-    const possibleDenoms = await this.q()
-      .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl)
-      .filter(
-        d =>
-          d.status === DenominationStatus.Unverified ||
-          d.status === DenominationStatus.VerifiedGood,
-      )
-      .toArray();
     possibleDenoms.sort((d1, d2) => {
       const a1 = Amounts.add(d1.feeWithdraw, d1.value).amount;
       const a2 = Amounts.add(d2.feeWithdraw, d2.value).amount;
@@ -1743,21 +1700,19 @@ export class Wallet {
       }
       const valid = await this.cryptoApi.isValidDenom(
         denom,
-        exchange.masterPublicKey,
+        exchangeDetails.masterPublicKey,
       );
       if (!valid) {
         denom.status = DenominationStatus.VerifiedBad;
       } else {
         denom.status = DenominationStatus.VerifiedGood;
       }
-      await this.q()
-        .put(Stores.denominations, denom)
-        .finish();
+      await oneShotPut(this.db, Stores.denominations, denom);
       if (valid) {
         return Amounts.add(denom.feeWithdraw, denom.value).amount;
       }
     }
-    return Amounts.getZero(exchange.currency);
+    return Amounts.getZero(exchangeDetails.currency);
   }
 
   /**
@@ -1771,19 +1726,16 @@ export class Wallet {
     exchangeBaseUrl: string,
     amount: AmountJson,
   ): Promise<DenominationRecord[]> {
-    const exchange = await this.q().get(Stores.exchanges, exchangeBaseUrl);
+    const exchange = await oneShotGet(this.db, Stores.exchanges, 
exchangeBaseUrl);
     if (!exchange) {
       throw Error(`exchange ${exchangeBaseUrl} not found`);
     }
+    const exchangeDetails = exchange.details;
+    if (!exchangeDetails) {
+      throw Error(`exchange ${exchangeBaseUrl} details not available`);
+    }
 
-    const possibleDenoms = await this.q()
-      .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl)
-      .filter(
-        d =>
-          d.status === DenominationStatus.Unverified ||
-          d.status === DenominationStatus.VerifiedGood,
-      )
-      .toArray();
+    const possibleDenoms = await this.getPossibleDenoms(exchange.baseUrl);
 
     let allValid = false;
 
@@ -1797,7 +1749,7 @@ export class Wallet {
         if (denom.status === DenominationStatus.Unverified) {
           const valid = await this.cryptoApi.isValidDenom(
             denom,
-            exchange.masterPublicKey,
+            exchangeDetails.masterPublicKey,
           );
           if (!valid) {
             denom.status = DenominationStatus.VerifiedBad;
@@ -1806,9 +1758,7 @@ export class Wallet {
             denom.status = DenominationStatus.VerifiedGood;
             nextPossibleDenoms.push(denom);
           }
-          await this.q()
-            .put(Stores.denominations, denom)
-            .finish();
+          await oneShotPut(this.db, Stores.denominations, denom);
         } else {
           nextPossibleDenoms.push(denom);
         }
@@ -1826,19 +1776,22 @@ export class Wallet {
   ): Promise<{ isTrusted: boolean; isAudited: boolean }> {
     let isTrusted = false;
     let isAudited = false;
-    const currencyRecord = await this.q().get(
+    const exchangeDetails = exchangeInfo.details;
+    if (!exchangeDetails) {
+      throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
+    }
+    const currencyRecord = await oneShotGet(this.db, 
       Stores.currencies,
-      exchangeInfo.currency,
-    );
+      exchangeDetails.currency);
     if (currencyRecord) {
       for (const trustedExchange of currencyRecord.exchanges) {
-        if (trustedExchange.exchangePub === exchangeInfo.masterPublicKey) {
+        if (trustedExchange.exchangePub === exchangeDetails.masterPublicKey) {
           isTrusted = true;
           break;
         }
       }
       for (const trustedAuditor of currencyRecord.auditors) {
-        for (const exchangeAuditor of exchangeInfo.auditors) {
+        for (const exchangeAuditor of exchangeDetails.auditors) {
           if (trustedAuditor.auditorPub === exchangeAuditor.auditor_pub) {
             isAudited = true;
             break;
@@ -1872,6 +1825,16 @@ export class Wallet {
     amount: AmountJson,
   ): Promise<ReserveCreationInfo> {
     const exchangeInfo = await this.updateExchangeFromUrl(baseUrl);
+    const exchangeDetails = exchangeInfo.details;
+    if (!exchangeDetails) {
+      throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
+    }
+    const exchangeWireInfo = exchangeInfo.wireInfo;
+    if (!exchangeWireInfo) {
+      throw Error(
+        `exchange ${exchangeInfo.baseUrl} wire details not available`,
+      );
+    }
 
     const selectedDenoms = await this.getVerifiedWithdrawDenomList(
       baseUrl,
@@ -1887,16 +1850,8 @@ export class Wallet {
       )
       .reduce((a, b) => Amounts.add(a, b).amount);
 
-    const wireInfo = await this.getWireInfo(baseUrl);
-
-    const wireFees = await this.q().get(Stores.exchangeWireFees, baseUrl);
-    if (!wireFees) {
-      // should never happen unless DB is inconsistent
-      throw Error(`no wire fees found for exchange ${baseUrl}`);
-    }
-
     const exchangeWireAccounts: string[] = [];
-    for (let account of wireInfo.accounts) {
+    for (let account of exchangeWireInfo.accounts) {
       exchangeWireAccounts.push(account.url);
     }
 
@@ -1910,17 +1865,14 @@ export class Wallet {
       }
     }
 
-    const possibleDenoms =
-      (await this.q()
-        .iterIndex(Stores.denominations.exchangeBaseUrlIndex, baseUrl)
-        .filter(d => d.isOffered)
-        .toArray()) || [];
+    const possibleDenoms = await oneShotIterIndex(
+      this.db,
+      Stores.denominations.exchangeBaseUrlIndex,
+      baseUrl)
+      .filter((d) => d.isOffered);
 
     const trustedAuditorPubs = [];
-    const currencyRecord = await this.q().get<CurrencyRecord>(
-      Stores.currencies,
-      amount.currency,
-    );
+    const currencyRecord = await oneShotGet(this.db, Stores.currencies, 
amount.currency);
     if (currencyRecord) {
       trustedAuditorPubs.push(
         ...currencyRecord.auditors.map(a => a.auditorPub),
@@ -1928,10 +1880,10 @@ export class Wallet {
     }
 
     let versionMatch;
-    if (exchangeInfo.protocolVersion) {
+    if (exchangeDetails.protocolVersion) {
       versionMatch = LibtoolVersion.compare(
         WALLET_PROTOCOL_VERSION,
-        exchangeInfo.protocolVersion,
+        exchangeDetails.protocolVersion,
       );
 
       if (
@@ -1940,10 +1892,10 @@ export class Wallet {
         versionMatch.currentCmp === -1
       ) {
         console.warn(
-          `wallet version ${WALLET_PROTOCOL_VERSION} might be outdated 
(exchange has ${exchangeInfo.protocolVersion}), checking for updates`,
+          `wallet version ${WALLET_PROTOCOL_VERSION} might be outdated 
(exchange has ${exchangeDetails.protocolVersion}), checking for updates`,
         );
         if (isFirefox()) {
-          console.log("skipping update check on Firefox")
+          console.log("skipping update check on Firefox");
         } else {
           chrome.runtime.requestUpdateCheck((status, details) => {
             console.log("update check status:", status);
@@ -1956,7 +1908,7 @@ export class Wallet {
       earliestDepositExpiration,
       exchangeInfo,
       exchangeWireAccounts,
-      exchangeVersion: exchangeInfo.protocolVersion || "unknown",
+      exchangeVersion: exchangeDetails.protocolVersion || "unknown",
       isAudited,
       isTrusted,
       numOfferedDenoms: possibleDenoms.length,
@@ -1965,7 +1917,7 @@ export class Wallet {
       trustedAuditorPubs,
       versionMatch,
       walletVersion: WALLET_PROTOCOL_VERSION,
-      wireFees,
+      wireFees: exchangeWireInfo,
       withdrawFee: acc,
     };
     return ret;
@@ -1975,8 +1927,15 @@ export class Wallet {
     exchangeBaseUrl: string,
     supportedTargetTypes: string[],
   ): Promise<string> {
-    const wireInfo = await this.getWireInfo(exchangeBaseUrl);
-    for (let account of wireInfo.accounts) {
+    const exchangeRecord = await oneShotGet(this.db, Stores.exchanges, 
exchangeBaseUrl);
+    if (!exchangeRecord) {
+      throw Error(`Exchange '${exchangeBaseUrl}' not found.`);
+    }
+    const exchangeWireInfo = exchangeRecord.wireInfo;
+    if (!exchangeWireInfo) {
+      throw Error(`Exchange wire info for '${exchangeBaseUrl}' not found.`);
+    }
+    for (let account of exchangeWireInfo.accounts) {
       const paytoUri = new URI(account.url);
       if (supportedTargetTypes.includes(paytoUri.authority())) {
         return account.url;
@@ -1986,235 +1945,173 @@ export class Wallet {
   }
 
   /**
-   * Update or add exchange DB entry by fetching the /keys information.
+   * Update or add exchange DB entry by fetching the /keys and /wire 
information.
    * Optionally link the reserve entry to the new or existing
    * exchange entry in then DB.
    */
-  async updateExchangeFromUrl(baseUrl: string): Promise<ExchangeRecord> {
-    baseUrl = canonicalizeBaseUrl(baseUrl);
-    const keysUrl = new URI("keys")
-      .absoluteTo(baseUrl)
-      .addQuery("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
-    const keysResp = await this.http.get(keysUrl.href());
-    if (keysResp.status !== 200) {
-      throw Error("/keys request failed");
-    }
-    const exchangeKeysJson = KeysJson.checked(keysResp.responseJson);
-    const exchangeWire = await this.getWireInfo(baseUrl);
-    return this.updateExchangeFromJson(baseUrl, exchangeKeysJson, 
exchangeWire);
-  }
-
-  private async suspendCoins(exchangeInfo: ExchangeRecord): Promise<void> {
-    const resultSuspendedCoins = await this.q()
-      .iterIndex(Stores.coins.exchangeBaseUrlIndex, exchangeInfo.baseUrl)
-      .indexJoinLeft(
-        Stores.denominations.exchangeBaseUrlIndex,
-        e => e.exchangeBaseUrl,
-      )
-      .fold(
-        (
-          cd: JoinLeftResult<CoinRecord, DenominationRecord>,
-          suspendedCoins: CoinRecord[],
-        ) => {
-          if (!cd.right || !cd.right.isOffered) {
-            return Array.prototype.concat(suspendedCoins, [cd.left]);
-          }
-          return Array.prototype.concat(suspendedCoins);
-        },
-        [],
-      );
-
-    const q = this.q();
-    resultSuspendedCoins.map((c: CoinRecord) => {
-      Wallet.enableTracing && console.log("suspending coin", c);
-      c.suspended = true;
-      q.put(Stores.coins, c);
-      this.badge.showNotification();
-      this.notifier.notify();
-    });
-    await q.finish();
-  }
-
-  private async updateExchangeFromJson(
+  async updateExchangeFromUrl(
     baseUrl: string,
-    exchangeKeysJson: KeysJson,
-    wireMethodDetails: ExchangeWireJson,
+    force: boolean = false,
   ): Promise<ExchangeRecord> {
-    // FIXME: all this should probably be commited atomically
-    const updateTimeSec = getTalerStampSec(exchangeKeysJson.list_issue_date);
-    if (updateTimeSec === null) {
-      throw Error("invalid update time");
-    }
-
-    if (exchangeKeysJson.denoms.length === 0) {
-      throw Error("exchange doesn't offer any denominations");
-    }
-
-    const r = await this.q().get<ExchangeRecord>(Stores.exchanges, baseUrl);
-
-    let exchangeInfo: ExchangeRecord;
+    const now = getTimestampNow();
+    baseUrl = canonicalizeBaseUrl(baseUrl);
 
+    const r = await oneShotGet(this.db, Stores.exchanges, baseUrl);
     if (!r) {
-      exchangeInfo = {
-        auditors: exchangeKeysJson.auditors,
-        baseUrl,
-        currency: Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value)
-          .currency,
-        lastUpdateTime: updateTimeSec,
-        lastUsedTime: 0,
-        masterPublicKey: exchangeKeysJson.master_public_key,
+      const newExchangeRecord: ExchangeRecord = {
+        baseUrl: baseUrl,
+        details: undefined,
+        wireInfo: undefined,
+        updateStatus: ExchangeUpdateStatus.FETCH_KEYS,
+        updateStarted: now,
       };
-      Wallet.enableTracing && console.log("making fresh exchange");
+      await oneShotPut(this.db, Stores.exchanges, newExchangeRecord);
     } else {
-      if (updateTimeSec < r.lastUpdateTime) {
-        Wallet.enableTracing && console.log("outdated /keys, not updating");
-        return r;
-      }
-      exchangeInfo = r;
-      exchangeInfo.lastUpdateTime = updateTimeSec;
-      Wallet.enableTracing && console.log("updating old exchange");
-    }
-
-    const updatedExchangeInfo = await this.updateExchangeInfo(
-      exchangeInfo,
-      exchangeKeysJson,
-    );
-    await this.suspendCoins(updatedExchangeInfo);
-    updatedExchangeInfo.protocolVersion = exchangeKeysJson.version;
-
-    await this.q()
-      .put(Stores.exchanges, updatedExchangeInfo)
-      .finish();
-
-    let oldWireFees = await this.q().get(Stores.exchangeWireFees, baseUrl);
-    if (!oldWireFees) {
-      oldWireFees = {
-        exchangeBaseUrl: baseUrl,
-        feesForType: {},
-      };
-    }
-
-    for (const paytoTargetType in wireMethodDetails.fees) {
-      let latestFeeStamp = 0;
-      const newFeeDetails = wireMethodDetails.fees[paytoTargetType];
-      const oldFeeDetails = oldWireFees.feesForType[paytoTargetType] || [];
-      oldWireFees.feesForType[paytoTargetType] = oldFeeDetails;
-      for (const oldFee of oldFeeDetails) {
-        if (oldFee.endStamp > latestFeeStamp) {
-          latestFeeStamp = oldFee.endStamp;
-        }
-      }
-      for (const fee of newFeeDetails) {
-        const start = getTalerStampSec(fee.start_date);
-        if (start === null) {
-          console.error("invalid start stamp in fee", fee);
-          continue;
-        }
-        if (start < latestFeeStamp) {
-          continue;
-        }
-        const end = getTalerStampSec(fee.end_date);
-        if (end === null) {
-          console.error("invalid end stamp in fee", fee);
-          continue;
+      runWithWriteTransaction(this.db, [Stores.exchanges], async (t) => {
+        const rec = await t.get(Stores.exchanges, baseUrl);
+        if (!rec) {
+          return;
         }
-        const wf: WireFee = {
-          closingFee: Amounts.parseOrThrow(fee.closing_fee),
-          endStamp: end,
-          sig: fee.sig,
-          startStamp: start,
-          wireFee: Amounts.parseOrThrow(fee.wire_fee),
-        };
-        const valid: boolean = await this.cryptoApi.isValidWireFee(
-          paytoTargetType,
-          wf,
-          exchangeInfo.masterPublicKey,
-        );
-        if (!valid) {
-          console.error("fee signature invalid", fee);
-          throw Error("fee signature invalid");
+        if (rec.updateStatus != ExchangeUpdateStatus.NONE && !force) {
+          return;
         }
-        oldFeeDetails.push(wf);
-      }
+        rec.updateStarted = now;
+        rec.updateStatus = ExchangeUpdateStatus.FETCH_KEYS;
+        t.put(Stores.exchanges, rec);
+      });
     }
 
-    await this.q().put(Stores.exchangeWireFees, oldWireFees);
+    await this.updateExchangeWithKeys(baseUrl);
+    await this.updateExchangeWithWireInfo(baseUrl);
 
-    if (exchangeKeysJson.payback) {
-      for (const payback of exchangeKeysJson.payback) {
-        const denom = await this.q().getIndexed(
-          Stores.denominations.denomPubHashIndex,
-          payback.h_denom_pub,
-        );
-        if (!denom) {
-          continue;
-        }
-        Wallet.enableTracing && console.log(`cashing back denom`, denom);
-        const coins = await this.q()
-          .iterIndex(Stores.coins.denomPubIndex, denom.denomPub)
-          .toArray();
-        for (const coin of coins) {
-          this.payback(coin.coinPub);
-        }
-      }
+    const updatedExchange = await oneShotGet(this.db, Stores.exchanges, 
baseUrl);
+
+    if (!updatedExchange) {
+      // This should practically never happen
+      throw Error("exchange not found");
     }
+    return updatedExchange;
+  }
 
-    return updatedExchangeInfo;
+  private async setExchangeError(
+    baseUrl: string,
+    err: OperationError,
+  ): Promise<void> {
+    const mut = (exchange: ExchangeRecord) => {
+      exchange.lastError = err;
+      return exchange;
+    };
+    await oneShotMutate(this.db, Stores.exchanges, baseUrl, mut);
   }
 
-  private async updateExchangeInfo(
-    exchangeInfo: ExchangeRecord,
-    newKeys: KeysJson,
-  ): Promise<ExchangeRecord> {
-    if (exchangeInfo.masterPublicKey !== newKeys.master_public_key) {
-      throw Error("public keys do not match");
+  /**
+   * Fetch the exchange's /keys and update our database accordingly.
+   *
+   * Exceptions thrown in this method must be caught and reported
+   * in the pending operations.
+   */
+  private async updateExchangeWithKeys(baseUrl: string): Promise<void> {
+    const existingExchangeRecord = await oneShotGet(this.db, Stores.exchanges, 
baseUrl);
+
+    if (existingExchangeRecord?.updateStatus != 
ExchangeUpdateStatus.FETCH_KEYS) {
+      return;
+    }
+    const keysUrl = new URI("keys")
+      .absoluteTo(baseUrl)
+      .addQuery("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
+    let keysResp;
+    try {
+      keysResp = await this.http.get(keysUrl.href());
+    } catch (e) {
+      await this.setExchangeError(baseUrl, {
+        type: "network",
+        details: {},
+        message: `Fetching keys failed: ${e.message}`,
+      });
+      throw e;
+    }
+    let exchangeKeysJson: KeysJson;
+    try {
+      exchangeKeysJson = KeysJson.checked(keysResp.responseJson);
+    } catch (e) {
+      await this.setExchangeError(baseUrl, {
+        type: "protocol-violation",
+        details: {},
+        message: `Parsing /keys response failed: ${e.message}`,
+      });
+      throw e;
     }
 
-    const existingDenoms: {
-      [denomPub: string]: DenominationRecord;
-    } = await this.q()
-      .iterIndex(
-        Stores.denominations.exchangeBaseUrlIndex,
-        exchangeInfo.baseUrl,
-      )
-      .fold(
-        (x: DenominationRecord, acc: typeof existingDenoms) => (
-          (acc[x.denomPub] = x), acc
-        ),
-        {},
-      );
+    const lastUpdateTimestamp = extractTalerStamp(
+      exchangeKeysJson.list_issue_date,
+    );
+    if (!lastUpdateTimestamp) {
+      const m = `Parsing /keys response failed: invalid list_issue_date.`;
+      await this.setExchangeError(baseUrl, {
+        type: "protocol-violation",
+        details: {},
+        message: m,
+      });
+      throw Error(m);
+    }
 
-    const newDenoms: typeof existingDenoms = {};
-    const newAndUnseenDenoms: typeof existingDenoms = {};
+    if (exchangeKeysJson.denoms.length === 0) {
+      const m = "exchange doesn't offer any denominations";
+      await this.setExchangeError(baseUrl, {
+        type: "protocol-violation",
+        details: {},
+        message: m,
+      });
+      throw Error(m);
+    }
 
-    for (const d of newKeys.denoms) {
-      const dr = await this.denominationRecordFromKeys(exchangeInfo.baseUrl, 
d);
-      if (!(d.denom_pub in existingDenoms)) {
-        newAndUnseenDenoms[dr.denomPub] = dr;
-      }
-      newDenoms[dr.denomPub] = dr;
+    const protocolVersion = exchangeKeysJson.version;
+    if (!protocolVersion) {
+      const m = "outdate exchange, no version in /keys response";
+      await this.setExchangeError(baseUrl, {
+        type: "protocol-violation",
+        details: {},
+        message: m,
+      });
+      throw Error(m);
     }
 
-    for (const oldDenomPub in existingDenoms) {
-      if (!(oldDenomPub in newDenoms)) {
-        const d = existingDenoms[oldDenomPub];
-        d.isOffered = false;
+    const currency = Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value)
+      .currency;
+
+    const mutExchangeRecord = (r: ExchangeRecord) => {
+      if (r.updateStatus != ExchangeUpdateStatus.FETCH_KEYS) {
+        console.log("not updating, wrong state (concurrent modification?)");
+        return undefined;
       }
-    }
+      r.details = {
+        currency,
+        protocolVersion,
+        lastUpdateTime: lastUpdateTimestamp,
+        masterPublicKey: exchangeKeysJson.master_public_key,
+        auditors: exchangeKeysJson.auditors,
+      };
+      r.updateStatus = ExchangeUpdateStatus.FETCH_WIRE;
+      r.lastError = undefined;
+      return r;
+    };
+  }
 
-    await this.q()
-      .putAll(
-        Stores.denominations,
-        Object.keys(newAndUnseenDenoms).map(d => newAndUnseenDenoms[d]),
-      )
-      .putAll(
-        Stores.denominations,
-        Object.keys(existingDenoms).map(d => existingDenoms[d]),
-      )
-      .finish();
-    return exchangeInfo;
+  private async updateExchangeWithWireInfo(exchangeBaseUrl: string) {
+    exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
+    const reqUrl = new URI("wire")
+      .absoluteTo(exchangeBaseUrl)
+      .addQuery("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
+    const resp = await this.http.get(reqUrl.href());
+
+    const wiJson = resp.responseJson;
+    if (!wiJson) {
+      throw Error("/wire response malformed");
+    }
+    const wireInfo = ExchangeWireJson.checked(wiJson);
   }
 
+
   /**
    * Get detailed balance information, sliced by exchange and by currency.
    */
@@ -2253,127 +2150,85 @@ export class Wallet {
       entryEx[field] = Amounts.add(entryEx[field], amount).amount;
     }
 
-    function collectBalances(c: CoinRecord, balance: WalletBalance) {
-      if (c.suspended) {
-        return balance;
-      }
-      if (c.status === CoinStatus.Fresh) {
-        addTo(balance, "available", c.currentAmount, c.exchangeBaseUrl);
-        return balance;
-      }
-      if (c.status === CoinStatus.Dirty) {
-        addTo(balance, "pendingIncoming", c.currentAmount, c.exchangeBaseUrl);
-        addTo(balance, "pendingIncomingDirty", c.currentAmount, 
c.exchangeBaseUrl);
-        return balance;
-      }
-      return balance;
-    }
-
-    function collectPendingWithdraw(r: ReserveRecord, balance: WalletBalance) {
-      if (!r.timestamp_confirmed) {
-        return balance;
-      }
-      let amount = Amounts.getZero(r.requested_amount.currency);
-      /*
-      let amount = r.current_amount;
-      if (!amount) {
-        amount = r.requested_amount;
-      }
-      */
-      amount = Amounts.add(amount, r.precoin_amount).amount;
-      if (Amounts.cmp(smallestWithdraw[r.exchange_base_url], amount) < 0) {
-        addTo(balance, "pendingIncoming", amount, r.exchange_base_url);
-        addTo(balance, "pendingIncomingWithdraw", amount, r.exchange_base_url);
-      }
-      return balance;
-    }
-
-    function collectPaybacks(r: ReserveRecord, balance: WalletBalance) {
-      if (!r.hasPayback) {
-        return balance;
-      }
-      if (
-        Amounts.cmp(smallestWithdraw[r.exchange_base_url], r.current_amount!) <
-        0
-      ) {
-        addTo(balance, "paybackAmount", r.current_amount!, 
r.exchange_base_url);
-      }
-      return balance;
-    }
+    const balanceStore = {
+      byCurrency: {},
+      byExchange: {},
+    };
 
-    function collectPendingRefresh(
-      r: RefreshSessionRecord,
-      balance: WalletBalance,
-    ) {
+    await runWithWriteTransaction(this.db, [Stores.coins, Stores.refresh, 
Stores.reserves, Stores.purchases], async (tx) => {
+      await tx.iter(Stores.coins).forEach((c) => {
+        if (c.suspended) {
+          return;
+        }
+        if (c.status === CoinStatus.Fresh) {
+          addTo(balanceStore, "available", c.currentAmount, c.exchangeBaseUrl);
+        }
+        if (c.status === CoinStatus.Dirty) {
+          addTo(balanceStore, "pendingIncoming", c.currentAmount, 
c.exchangeBaseUrl);
+          addTo(
+            balanceStore,
+            "pendingIncomingDirty",
+            c.currentAmount,
+            c.exchangeBaseUrl,
+          );
+        }
+      });
+      await tx.iter(Stores.refresh).forEach((r) => {
       // Don't count finished refreshes, since the refresh already resulted
       // in coins being added to the wallet.
-      if (r.finished) {
-        return balance;
-      }
-      addTo(balance, "pendingIncoming", r.valueOutput, r.exchangeBaseUrl);
-      addTo(balance, "pendingIncomingRefresh", r.valueOutput, 
r.exchangeBaseUrl);
-
-      return balance;
-    }
-
-    function collectPayments(t: PurchaseRecord, balance: WalletBalance) {
-      if (t.finished) {
-        return balance;
-      }
-      for (const c of t.payReq.coins) {
+        if (r.finished) {
+          return;
+        }
+        addTo(balanceStore, "pendingIncoming", r.valueOutput, 
r.exchangeBaseUrl);
         addTo(
-          balance,
-          "pendingPayment",
-          Amounts.parseOrThrow(c.contribution),
-          c.exchange_url,
+          balanceStore,
+          "pendingIncomingRefresh",
+          r.valueOutput,
+          r.exchangeBaseUrl,
         );
-      }
-      return balance;
-    }
+      });
 
-    function collectSmallestWithdraw(
-      e: JoinResult<ExchangeRecord, DenominationRecord>,
-      sw: any,
-    ) {
-      let min = sw[e.left.baseUrl];
-      const v = Amounts.add(e.right.value, e.right.feeWithdraw).amount;
-      if (!min) {
-        min = v;
-      } else if (Amounts.cmp(v, min) < 0) {
-        min = v;
-      }
-      sw[e.left.baseUrl] = min;
-      return sw;
-    }
+      await tx.iter(Stores.reserves).forEach((r) => {
+        if (!r.timestamp_confirmed) {
+          return;
+        }
+        let amount = Amounts.getZero(r.requested_amount.currency);
+        amount = Amounts.add(amount, r.precoin_amount).amount;
+        addTo(balanceStore, "pendingIncoming", amount, r.exchange_base_url);
+        addTo(balanceStore, "pendingIncomingWithdraw", amount, 
r.exchange_base_url);
+      });
 
-    const balanceStore = {
-      byCurrency: {},
-      byExchange: {},
-    };
-    // Mapping from exchange pub to smallest
-    // possible amount we can withdraw
-    let smallestWithdraw: { [baseUrl: string]: AmountJson } = {};
-
-    smallestWithdraw = await this.q()
-      .iter(Stores.exchanges)
-      .indexJoin(Stores.denominations.exchangeBaseUrlIndex, x => x.baseUrl)
-      .fold(collectSmallestWithdraw, {});
-
-    const tx = this.q();
-    tx.iter(Stores.coins).fold(collectBalances, balanceStore);
-    tx.iter(Stores.refresh).fold(collectPendingRefresh, balanceStore);
-    tx.iter(Stores.reserves).fold(collectPendingWithdraw, balanceStore);
-    tx.iter(Stores.reserves).fold(collectPaybacks, balanceStore);
-    tx.iter(Stores.purchases).fold(collectPayments, balanceStore);
-    await tx.finish();
-    Wallet.enableTracing && console.log("computed balances:", balanceStore)
+      await tx.iter(Stores.reserves).forEach((r) => {
+        if (!r.hasPayback) {
+          return;
+        }
+        addTo(balanceStore, "paybackAmount", r.current_amount!, 
r.exchange_base_url);
+        return balanceStore;
+      });
+
+      await tx.iter(Stores.purchases).forEach((t) => {
+        if (t.finished) {
+          return;
+        }
+        for (const c of t.payReq.coins) {
+          addTo(
+            balanceStore,
+            "pendingPayment",
+            Amounts.parseOrThrow(c.contribution),
+            c.exchange_url,
+          );
+        }
+      });
+    });
+
+    Wallet.enableTracing && console.log("computed balances:", balanceStore);
     return balanceStore;
   }
 
   async createRefreshSession(
     oldCoinPub: string,
   ): Promise<RefreshSessionRecord | undefined> {
-    const coin = await this.q().get<CoinRecord>(Stores.coins, oldCoinPub);
+    const coin = await oneShotGet(this.db, Stores.coins, oldCoinPub);
 
     if (!coin) {
       throw Error("coin not found");
@@ -2389,7 +2244,7 @@ export class Wallet {
       throw Error("db inconsistent");
     }
 
-    const oldDenom = await this.q().get(Stores.denominations, [
+    const oldDenom = await oneShotGet(this.db, Stores.denominations, [
       exchange.baseUrl,
       coin.denomPub,
     ]);
@@ -2398,9 +2253,7 @@ export class Wallet {
       throw Error("db inconsistent");
     }
 
-    const availableDenoms: DenominationRecord[] = await this.q()
-      .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl)
-      .toArray();
+    const availableDenoms: DenominationRecord[] = await 
oneShotIterIndex(this.db, Stores.denominations.exchangeBaseUrlIndex, 
exchange.baseUrl).toArray();
 
     const availableAmount = Amounts.sub(coin.currentAmount, 
oldDenom.feeRefresh)
       .amount;
@@ -2421,7 +2274,7 @@ export class Wallet {
           )} too small`,
         );
       coin.status = CoinStatus.Useless;
-      await this.q().put(Stores.coins, coin);
+      await oneShotPut(this.db, Stores.coins, coin);
       this.notifier.notify();
       return undefined;
     }
@@ -2445,16 +2298,16 @@ export class Wallet {
       return c;
     }
 
+    let key;
+
     // Store refresh session and subtract refreshed amount from
     // coin in the same transaction.
-    const query = this.q();
-    query
-      .put(Stores.refresh, refreshSession, "refreshKey")
-      .mutate(Stores.coins, coin.coinPub, mutateCoin);
-    await query.finish();
+    await runWithWriteTransaction(this.db, [Stores.refresh, Stores.coins], 
async (tx) => {
+      key = await tx.put(Stores.refresh, refreshSession);
+      await tx.mutate(Stores.coins, coin.coinPub, mutateCoin);
+    });
     this.notifier.notify();
 
-    const key = query.key("refreshKey");
     if (!key || typeof key !== "number") {
       throw Error("insert failed");
     }
@@ -2466,18 +2319,20 @@ export class Wallet {
 
   async refresh(oldCoinPub: string): Promise<void> {
     const refreshImpl = async () => {
-      const oldRefreshSessions = await this.q()
-        .iter(Stores.refresh)
-        .toArray();
+      const oldRefreshSessions = await oneShotIter(this.db, 
Stores.refresh).toArray();
       for (const session of oldRefreshSessions) {
         if (session.finished) {
           continue;
         }
         Wallet.enableTracing &&
-          console.log("waiting for unfinished old refresh session for", 
oldCoinPub, session);
+          console.log(
+            "waiting for unfinished old refresh session for",
+            oldCoinPub,
+            session,
+          );
         await this.continueRefreshSession(session);
       }
-      const coin = await this.q().get(Stores.coins, oldCoinPub);
+      const coin = await oneShotGet(this.db, Stores.coins, oldCoinPub);
       if (!coin) {
         console.warn("can't refresh, coin not in database");
         return;
@@ -2486,7 +2341,11 @@ export class Wallet {
         coin.status === CoinStatus.Useless ||
         coin.status === CoinStatus.Fresh
       ) {
-        Wallet.enableTracing && console.log("not refreshing due to coin 
status", CoinStatus[coin.status])
+        Wallet.enableTracing &&
+          console.log(
+            "not refreshing due to coin status",
+            CoinStatus[coin.status],
+          );
         return;
       }
       const refreshSession = await this.createRefreshSession(oldCoinPub);
@@ -2520,10 +2379,7 @@ export class Wallet {
     }
     if (typeof refreshSession.norevealIndex !== "number") {
       await this.refreshMelt(refreshSession);
-      const r = await this.q().get<RefreshSessionRecord>(
-        Stores.refresh,
-        refreshSession.id,
-      );
+      const r = await oneShotGet(this.db, Stores.refresh, refreshSession.id);
       if (!r) {
         throw Error("refresh session does not exist anymore");
       }
@@ -2539,10 +2395,8 @@ export class Wallet {
       return;
     }
 
-    const coin = await this.q().get<CoinRecord>(
-      Stores.coins,
-      refreshSession.meltCoinPub,
-    );
+    const coin = await oneShotGet(this.db, Stores.coins, 
refreshSession.meltCoinPub);
+
     if (!coin) {
       console.error("can't melt coin, it does not exist");
       return;
@@ -2579,9 +2433,8 @@ export class Wallet {
 
     refreshSession.norevealIndex = norevealIndex;
 
-    await this.q()
-      .put(Stores.refresh, refreshSession)
-      .finish();
+    await oneShotPut(this.db, Stores.refresh, refreshSession);
+
     this.notifier.notify();
   }
 
@@ -2598,10 +2451,7 @@ export class Wallet {
       throw Error("refresh index error");
     }
 
-    const meltCoinRecord = await this.q().get(
-      Stores.coins,
-      refreshSession.meltCoinPub,
-    );
+    const meltCoinRecord = await oneShotGet(this.db, Stores.coins, 
refreshSession.meltCoinPub);
     if (!meltCoinRecord) {
       throw Error("inconsistent database");
     }
@@ -2657,10 +2507,7 @@ export class Wallet {
       return;
     }
 
-    const exchange = await this.q().get<ExchangeRecord>(
-      Stores.exchanges,
-      refreshSession.exchangeBaseUrl,
-    );
+    const exchange = await this.findExchange(refreshSession.exchangeBaseUrl);
     if (!exchange) {
       console.error(`exchange ${refreshSession.exchangeBaseUrl} not found`);
       return;
@@ -2669,7 +2516,7 @@ export class Wallet {
     const coins: CoinRecord[] = [];
 
     for (let i = 0; i < respJson.ev_sigs.length; i++) {
-      const denom = await this.q().get(Stores.denominations, [
+      const denom = await oneShotGet(this.db, Stores.denominations, [
         refreshSession.exchangeBaseUrl,
         refreshSession.newDenoms[i],
       ]);
@@ -2702,17 +2549,25 @@ export class Wallet {
 
     refreshSession.finished = true;
 
-    await this.q()
-      .putAll(Stores.coins, coins)
-      .put(Stores.refresh, refreshSession)
-      .finish();
+    await runWithWriteTransaction(this.db, [Stores.coins, Stores.refresh], 
async (tx) => {
+      for (let coin of coins) {
+        await tx.put(Stores.coins, coin);
+      }
+      await tx.put(Stores.refresh, refreshSession);
+    });
     this.notifier.notify();
   }
 
+  async findExchange(exchangeBaseUrl: string): Promise<ExchangeRecord | 
undefined> {
+    return await oneShotGet(this.db, Stores.exchanges, exchangeBaseUrl);
+  }
+
   /**
    * Retrive the full event history for this wallet.
    */
-  async getHistory(historyQuery?: HistoryQuery): Promise<{ history: 
HistoryRecord[] }> {
+  async getHistory(
+    historyQuery?: HistoryQuery,
+  ): Promise<{ history: HistoryRecord[] }> {
     const history: HistoryRecord[] = [];
 
     // FIXME: do pagination instead of generating the full history
@@ -2721,9 +2576,7 @@ export class Wallet {
     // This works as timestamps are guaranteed to be monotonically
     // increasing even
 
-    const proposals = await this.q()
-      .iter<ProposalDownloadRecord>(Stores.proposals)
-      .toArray();
+    const proposals = await oneShotIter(this.db, Stores.proposals).toArray();
     for (const p of proposals) {
       history.push({
         detail: {
@@ -2735,7 +2588,7 @@ export class Wallet {
       });
     }
 
-    const withdrawals = await 
this.q().iter<WithdrawalRecord>(Stores.withdrawals).toArray()
+    const withdrawals = await oneShotIter(this.db, 
Stores.withdrawals).toArray();
     for (const w of withdrawals) {
       history.push({
         detail: {
@@ -2746,9 +2599,7 @@ export class Wallet {
       });
     }
 
-    const purchases = await this.q()
-      .iter<PurchaseRecord>(Stores.purchases)
-      .toArray();
+    const purchases = await oneShotIter(this.db, Stores.purchases).toArray();
     for (const p of purchases) {
       history.push({
         detail: {
@@ -2787,9 +2638,8 @@ export class Wallet {
       }
     }
 
-    const reserves: ReserveRecord[] = await this.q()
-      .iter<ReserveRecord>(Stores.reserves)
-      .toArray();
+    const reserves = await oneShotIter(this.db, Stores.reserves).toArray();
+
     for (const r of reserves) {
       history.push({
         detail: {
@@ -2813,9 +2663,7 @@ export class Wallet {
       }
     }
 
-    const tips: TipRecord[] = await this.q()
-      .iter<TipRecord>(Stores.tips)
-      .toArray();
+    const tips: TipRecord[] = await oneShotIter(this.db, 
Stores.tips).toArray();
     for (const tip of tips) {
       history.push({
         detail: {
@@ -2835,78 +2683,87 @@ export class Wallet {
   }
 
   async getPendingOperations(): Promise<PendingOperationsResponse> {
+    const pendingOperations: PendingOperationInfo[] = [];
+    const exchanges = await this.getExchanges();
+    for (let e of exchanges) {
+      switch (e.updateStatus) {
+        case ExchangeUpdateStatus.NONE:
+          if (!e.details) {
+            pendingOperations.push({
+              type: "bug",
+              message:
+                "Exchange record does not have details, but no update in 
progress.",
+              details: {
+                exchangeBaseUrl: e.baseUrl,
+              },
+            });
+          }
+          break;
+        case ExchangeUpdateStatus.FETCH_KEYS:
+          pendingOperations.push({
+            type: "exchange-update",
+            stage: "fetch-keys",
+            exchangeBaseUrl: e.baseUrl,
+          });
+          break;
+        case ExchangeUpdateStatus.FETCH_WIRE:
+          pendingOperations.push({
+            type: "exchange-update",
+            stage: "fetch-wire",
+            exchangeBaseUrl: e.baseUrl,
+          });
+          break;
+      }
+    }
     return {
-      pendingOperations: []
+      pendingOperations,
     };
   }
 
   async getDenoms(exchangeUrl: string): Promise<DenominationRecord[]> {
-    const denoms = await this.q()
-      .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeUrl)
-      .toArray();
+    const denoms = await oneShotIterIndex(this.db, 
Stores.denominations.exchangeBaseUrlIndex, exchangeUrl).toArray();
     return denoms;
   }
 
   async getProposal(
     proposalId: number,
   ): Promise<ProposalDownloadRecord | undefined> {
-    const proposal = await this.q().get(Stores.proposals, proposalId);
+    const proposal = await oneShotGet(this.db, Stores.proposals, proposalId);
     return proposal;
   }
 
   async getExchanges(): Promise<ExchangeRecord[]> {
-    return this.q()
-      .iter<ExchangeRecord>(Stores.exchanges)
-      .toArray();
+    return await oneShotIter(this.db, Stores.exchanges).toArray();
   }
 
   async getCurrencies(): Promise<CurrencyRecord[]> {
-    return this.q()
-      .iter<CurrencyRecord>(Stores.currencies)
-      .toArray();
+    return await oneShotIter(this.db, Stores.currencies).toArray();
   }
 
   async updateCurrency(currencyRecord: CurrencyRecord): Promise<void> {
     Wallet.enableTracing && console.log("updating currency to", 
currencyRecord);
-    await this.q()
-      .put(Stores.currencies, currencyRecord)
-      .finish();
+    await oneShotPut(this.db, Stores.currencies, currencyRecord);
     this.notifier.notify();
   }
 
   async getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> {
-    return this.q()
-      .iter<ReserveRecord>(Stores.reserves)
-      .filter((r: ReserveRecord) => r.exchange_base_url === exchangeBaseUrl)
-      .toArray();
+    return await oneShotIter(this.db, Stores.reserves).filter((r) => 
r.exchange_base_url === exchangeBaseUrl);
   }
 
   async getCoins(exchangeBaseUrl: string): Promise<CoinRecord[]> {
-    return this.q()
-      .iter<CoinRecord>(Stores.coins)
-      .filter((c: CoinRecord) => c.exchangeBaseUrl === exchangeBaseUrl)
-      .toArray();
+    return await oneShotIter(this.db, Stores.coins).filter((c) => 
c.exchangeBaseUrl === exchangeBaseUrl);
   }
 
   async getPreCoins(exchangeBaseUrl: string): Promise<PreCoinRecord[]> {
-    return this.q()
-      .iter<PreCoinRecord>(Stores.precoins)
-      .filter((c: PreCoinRecord) => c.exchangeBaseUrl === exchangeBaseUrl)
-      .toArray();
+    return await oneShotIter(this.db, Stores.precoins).filter((c) => 
c.exchangeBaseUrl === exchangeBaseUrl);
   }
 
-  async hashContract(contract: ContractTerms): Promise<string> {
+  private async hashContract(contract: ContractTerms): Promise<string> {
     return this.cryptoApi.hashString(canonicalJson(contract));
   }
 
-  async getCurrencyRecord(
-    currency: string,
-  ): Promise<CurrencyRecord | undefined> {
-    return this.q().get(Stores.currencies, currency);
-  }
-
   async payback(coinPub: string): Promise<void> {
-    let coin = await this.q().get(Stores.coins, coinPub);
+    let coin = await oneShotGet(this.db, Stores.coins, coinPub);
     if (!coin) {
       throw Error(`Coin ${coinPub} not found, can't request payback`);
     }
@@ -2914,7 +2771,7 @@ export class Wallet {
     if (!reservePub) {
       throw Error(`Can't request payback for a refreshed coin`);
     }
-    const reserve = await this.q().get(Stores.reserves, reservePub);
+    const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
     if (!reserve) {
       throw Error(`Reserve of coin ${coinPub} not found`);
     }
@@ -2932,9 +2789,10 @@ export class Wallet {
     // technically we might update reserve status before we get the response
     // from the reserve for the payback request.
     reserve.hasPayback = true;
-    await this.q()
-      .put(Stores.coins, coin)
-      .put(Stores.reserves, reserve);
+    await runWithWriteTransaction(this.db, [Stores.coins, Stores.reserves], 
async (tx) => {
+      await tx.put(Stores.coins, coin!!);
+      await tx.put(Stores.reserves, reserve);
+    });
     this.notifier.notify();
 
     const paybackRequest = await this.cryptoApi.createPaybackRequest(coin);
@@ -2947,17 +2805,17 @@ export class Wallet {
     if (paybackConfirmation.reserve_pub !== coin.reservePub) {
       throw Error(`Coin's reserve doesn't match reserve on payback`);
     }
-    coin = await this.q().get(Stores.coins, coinPub);
+    coin = await oneShotGet(this.db, Stores.coins, coinPub);
     if (!coin) {
       throw Error(`Coin ${coinPub} not found, can't confirm payback`);
     }
     coin.status = CoinStatus.PaybackDone;
-    await this.q().put(Stores.coins, coin);
+    await oneShotPut(this.db, Stores.coins, coin);
     this.notifier.notify();
     await this.updateReserve(reservePub!);
   }
 
-  async denominationRecordFromKeys(
+  private async denominationRecordFromKeys(
     exchangeBaseUrl: string,
     denomIn: Denomination,
   ): Promise<DenominationRecord> {
@@ -2983,20 +2841,17 @@ export class Wallet {
   }
 
   async withdrawPaybackReserve(reservePub: string): Promise<void> {
-    const reserve = await this.q().get(Stores.reserves, reservePub);
+    const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
     if (!reserve) {
       throw Error(`Reserve ${reservePub} does not exist`);
     }
     reserve.hasPayback = false;
-    await this.q().put(Stores.reserves, reserve);
+    await oneShotPut(this.db, Stores.reserves, reserve);
     this.depleteReserve(reserve);
   }
 
   async getPaybackReserves(): Promise<ReserveRecord[]> {
-    return await this.q()
-      .iter(Stores.reserves)
-      .filter(r => r.hasPayback)
-      .toArray();
+    return await oneShotIter(this.db, Stores.reserves).filter(r => 
r.hasPayback);
   }
 
   /**
@@ -3009,13 +2864,16 @@ export class Wallet {
 
   async getSenderWireInfos(): Promise<SenderWireInfos> {
     const m: { [url: string]: Set<string> } = {};
-    await this.q()
-      .iter(Stores.exchangeWireFees)
-      .map(x => {
-        const s = (m[x.exchangeBaseUrl] = m[x.exchangeBaseUrl] || new Set());
-        Object.keys(x.feesForType).map(k => s.add(k));
-      })
-      .run();
+
+    await oneShotIter(this.db, Stores.exchanges).forEach((x) => {
+      const wi = x.wireInfo;
+      if (!wi) {
+        return;
+      }
+      const s = (m[x.baseUrl] = m[x.baseUrl] || new Set());
+      Object.keys(wi.feesForType).map(k => s.add(k));
+    });
+
     Wallet.enableTracing && console.log(m);
     const exchangeWireTypes: { [url: string]: string[] } = {};
     Object.keys(m).map(e => {
@@ -3023,12 +2881,10 @@ export class Wallet {
     });
 
     const senderWiresSet: Set<string> = new Set();
-    await this.q()
-      .iter(Stores.senderWires)
-      .map(x => {
-        senderWiresSet.add(x.paytoUri);
-      })
-      .run();
+    await oneShotIter(this.db, Stores.senderWires).forEach((x) => {
+      senderWiresSet.add(x.paytoUri);
+    });
+
     const senderWires: string[] = Array.from(senderWiresSet);
 
     return {
@@ -3049,11 +2905,15 @@ export class Wallet {
       return;
     }
     const stampSecNow = Math.floor(new Date().getTime() / 1000);
-    const exchange = await this.q().get(Stores.exchanges, req.exchange);
+    const exchange = await this.findExchange(req.exchange);
     if (!exchange) {
       console.error(`Exchange ${req.exchange} not known to the wallet`);
       return;
     }
+    const exchangeDetails = exchange.details;
+    if (!exchangeDetails) {
+      throw Error("exchange information needs to be updated first.");
+    }
     Wallet.enableTracing && console.log("selecting coins for return:", req);
     const cds = await this.getCoinsForReturn(req.exchange, req.amount);
     Wallet.enableTracing && console.log(cds);
@@ -3073,7 +2933,7 @@ export class Wallet {
       amount: Amounts.toString(req.amount),
       auditors: [],
       exchanges: [
-        { master_pub: exchange.masterPublicKey, url: exchange.baseUrl },
+        { master_pub: exchangeDetails.masterPublicKey, url: exchange.baseUrl },
       ],
       extra: {},
       fulfillment_url: "",
@@ -3114,10 +2974,12 @@ export class Wallet {
       wire: req.senderWire,
     };
 
-    await this.q()
-      .put(Stores.coinsReturns, coinsReturnRecord)
-      .putAll(Stores.coins, payCoinInfo.updatedCoins)
-      .finish();
+    await runWithWriteTransaction(this.db, [Stores.coinsReturns, 
Stores.coins], async (tx) => {
+      await tx.put(Stores.coinsReturns, coinsReturnRecord);
+      for (let c of payCoinInfo.updatedCoins) {
+        await tx.put(Stores.coins, c);
+      }
+    });
     this.badge.showNotification();
     this.notifier.notify();
 
@@ -3167,10 +3029,7 @@ export class Wallet {
       // FIXME: verify signature
 
       // For every successful deposit, we replace the old record with an 
updated one
-      const currentCrr = await this.q().get(
-        Stores.coinsReturns,
-        coinsReturnRecord.contractTermsHash,
-      );
+      const currentCrr = await oneShotGet(this.db, Stores.coinsReturns, 
coinsReturnRecord.contractTermsHash);
       if (!currentCrr) {
         console.error("database inconsistent");
         continue;
@@ -3180,7 +3039,7 @@ export class Wallet {
           nc.depositedSig = respJson.sig;
         }
       }
-      await this.q().put(Stores.coinsReturns, currentCrr);
+      await oneShotPut(this.db, Stores.coinsReturns, currentCrr);
       this.notifier.notify();
     }
   }
@@ -3220,9 +3079,7 @@ export class Wallet {
     const hc = refundResponse.h_contract_terms;
 
     // Add the refund permissions to the purchase within a DB transaction
-    await this.q()
-      .mutate(Stores.purchases, hc, f)
-      .finish();
+    await oneShotMutate(this.db, Stores.purchases, hc, f);
     this.notifier.notify();
 
     await this.submitRefunds(hc);
@@ -3257,7 +3114,7 @@ export class Wallet {
   }
 
   private async submitRefunds(contractTermsHash: string): Promise<void> {
-    const purchase = await this.q().get(Stores.purchases, contractTermsHash);
+    const purchase = await oneShotGet(this.db, Stores.purchases, 
contractTermsHash);
     if (!purchase) {
       console.error(
         "not submitting refunds, contract terms not found:",
@@ -3320,10 +3177,10 @@ export class Wallet {
         return c;
       };
 
-      await this.q()
-        .mutate(Stores.purchases, contractTermsHash, transformPurchase)
-        .mutate(Stores.coins, perm.coin_pub, transformCoin)
-        .finish();
+      await runWithWriteTransaction(this.db, [Stores.purchases, Stores.coins], 
async (tx) => {
+        await tx.mutate(Stores.purchases, contractTermsHash, 
transformPurchase);
+        await tx.mutate(Stores.coins, perm.coin_pub, transformCoin);
+      });
       this.refresh(perm.coin_pub);
     }
 
@@ -3334,7 +3191,7 @@ export class Wallet {
   async getPurchase(
     contractTermsHash: string,
   ): Promise<PurchaseRecord | undefined> {
-    return this.q().get(Stores.purchases, contractTermsHash);
+    return oneShotGet(this.db, Stores.purchases, contractTermsHash);
   }
 
   async getFullRefundFees(
@@ -3343,10 +3200,7 @@ export class Wallet {
     if (refundPermissions.length === 0) {
       throw Error("no refunds given");
     }
-    const coin0 = await this.q().get(
-      Stores.coins,
-      refundPermissions[0].coin_pub,
-    );
+    const coin0 = await oneShotGet(this.db, Stores.coins, 
refundPermissions[0].coin_pub);
     if (!coin0) {
       throw Error("coin not found");
     }
@@ -3354,18 +3208,15 @@ export class Wallet {
       Amounts.parseOrThrow(refundPermissions[0].refund_amount).currency,
     );
 
-    const denoms = await this.q()
-      .iterIndex(
-        Stores.denominations.exchangeBaseUrlIndex,
-        coin0.exchangeBaseUrl,
-      )
-      .toArray();
+    const denoms = await oneShotIterIndex(this.db, 
Stores.denominations.exchangeBaseUrlIndex,
+      coin0.exchangeBaseUrl).toArray()
+
     for (const rp of refundPermissions) {
-      const coin = await this.q().get(Stores.coins, rp.coin_pub);
+      const coin = await oneShotGet(this.db, Stores.coins, rp.coin_pub);
       if (!coin) {
         throw Error("coin not found");
       }
-      const denom = await this.q().get(Stores.denominations, [
+      const denom = await oneShotGet(this.db, Stores.denominations, [
         coin0.exchangeBaseUrl,
         coin.denomPub,
       ]);
@@ -3407,19 +3258,13 @@ export class Wallet {
     tipId: string,
     merchantOrigin: string,
   ): Promise<void> {
-    let tipRecord = await this.q().get(Stores.tips, [tipId, merchantOrigin]);
+    let tipRecord = await oneShotGet(this.db, Stores.tips, [tipId, 
merchantOrigin]);
     if (!tipRecord) {
       throw Error("tip not in database");
     }
 
     tipRecord.accepted = true;
-
-    // Create one transactional query, within this transaction
-    // both the tip will be marked as accepted and coins
-    // already withdrawn will be untainted.
-    await this.q()
-      .put(Stores.tips, tipRecord)
-      .finish();
+    await oneShotPut(this.db, Stores.tips, tipRecord);
 
     if (tipRecord.pickedUp) {
       console.log("tip already picked up");
@@ -3437,7 +3282,7 @@ export class Wallet {
       );
       const coinPubs: string[] = planchets.map(x => x.coinPub);
 
-      await this.q().mutate(Stores.tips, [tipId, merchantOrigin], r => {
+      await oneShotMutate(this.db, Stores.tips, [tipId, merchantOrigin], (r) 
=> {
         if (!r.planchets) {
           r.planchets = planchets;
           r.coinPubs = coinPubs;
@@ -3448,7 +3293,7 @@ export class Wallet {
       this.notifier.notify();
     }
 
-    tipRecord = await this.q().get(Stores.tips, [tipId, merchantOrigin]);
+    tipRecord = await oneShotGet(this.db, Stores.tips, [tipId, 
merchantOrigin]);
     if (!tipRecord) {
       throw Error("tip not in database");
     }
@@ -3497,15 +3342,14 @@ export class Wallet {
         reservePub: response.reserve_pub,
         withdrawSig: response.reserve_sigs[i].reserve_sig,
       };
-      await this.q().put(Stores.precoins, preCoin);
+      await oneShotPut(this.db, Stores.precoins, preCoin);
       await this.processPreCoin(preCoin.coinPub);
     }
 
     tipRecord.pickedUp = true;
 
-    await this.q()
-      .put(Stores.tips, tipRecord)
-      .finish();
+    await oneShotPut(this.db, Stores.tips, tipRecord);
+
     this.notifier.notify();
     this.badge.showNotification();
     return;
@@ -3529,10 +3373,11 @@ export class Wallet {
 
     let amount = Amounts.parseOrThrow(tipPickupStatus.amount);
 
-    let tipRecord = await this.q().get(Stores.tips, [
+    let tipRecord = await oneShotGet(this.db, Stores.tips, [
       res.tipId,
       res.merchantOrigin,
-    ]);
+    ])
+
     if (!tipRecord) {
       const withdrawDetails = await this.getWithdrawDetailsForAmount(
         tipPickupStatus.exchange_url,
@@ -3558,7 +3403,7 @@ export class Wallet {
           withdrawDetails.withdrawFee,
         ).amount,
       };
-      await this.q().put(Stores.tips, tipRecord);
+      await oneShotPut(this.db, Stores.tips, tipRecord);
     }
 
     const tipStatus: TipStatus = {
@@ -3578,7 +3423,7 @@ export class Wallet {
   }
 
   async abortFailedPayment(contractTermsHash: string): Promise<void> {
-    const purchase = await this.q().get(Stores.purchases, contractTermsHash);
+    const purchase = await oneShotGet(this.db, Stores.purchases, 
contractTermsHash);
     if (!purchase) {
       throw Error("Purchase not found, unable to abort with refund");
     }
@@ -3595,7 +3440,7 @@ export class Wallet {
     // From now on, we can't retry payment anymore,
     // so mark this in the DB in case the /pay abort
     // does not complete on the first try.
-    await this.q().put(Stores.purchases, purchase);
+    await oneShotPut(this.db, Stores.purchases, purchase);
 
     let resp;
 
@@ -3616,15 +3461,14 @@ export class Wallet {
     const refundResponse = MerchantRefundResponse.checked(resp.responseJson);
     await this.acceptRefundResponse(refundResponse);
 
-    const markAbortDone = (p: PurchaseRecord) => {
+    await runWithWriteTransaction(this.db, [Stores.purchases], async (tx) => {
+      const p = await tx.get(Stores.purchases, purchase.contractTermsHash);
+      if (!p) {
+        return;
+      }
       p.abortDone = true;
-      return p;
-    };
-    await this.q().mutate(
-      Stores.purchases,
-      purchase.contractTermsHash,
-      markAbortDone,
-    );
+      await tx.put(Stores.purchases, p);
+    });
   }
 
   /**
@@ -3684,7 +3528,7 @@ export class Wallet {
   }
 
   async getPurchaseDetails(hc: string): Promise<PurchaseDetails> {
-    const purchase = await this.q().get(Stores.purchases, hc);
+    const purchase = await oneShotGet(this.db, Stores.purchases, hc);
     if (!purchase) {
       throw Error("unknown purchase");
     }
diff --git a/src/walletTypes.ts b/src/walletTypes.ts
index e632cd38..b227ca81 100644
--- a/src/walletTypes.ts
+++ b/src/walletTypes.ts
@@ -34,8 +34,7 @@ import {
   CoinRecord,
   DenominationRecord,
   ExchangeRecord,
-  ExchangeWireFeesRecord,
-  TipRecord,
+  ExchangeWireInfo,
 } from "./dbTypes";
 import { CoinPaySig, ContractTerms, PayReq } from "./talerTypes";
 
@@ -98,7 +97,7 @@ export interface ReserveCreationInfo {
   /**
    * Wire fees from the exchange.
    */
-  wireFees: ExchangeWireFeesRecord;
+  wireFees: ExchangeWireInfo;
 
   /**
    * Does the wallet know about an auditor for
@@ -475,7 +474,6 @@ export interface PreparePayResultError {
   error: string;
 }
 
-
 export interface PreparePayResultPaid {
   status: "paid";
   contractTerms: ContractTerms;
@@ -517,18 +515,40 @@ export interface WalletDiagnostics {
 }
 
 export interface PendingWithdrawOperation {
-  type: "withdraw"
+  type: "withdraw";
 }
 
 export interface PendingRefreshOperation {
-  type: "refresh"
+  type: "refresh";
 }
 
 export interface PendingPayOperation {
-  type: "pay"
+  type: "pay";
+}
+
+export interface OperationError {
+  type: string;
+  message: string;
+  details: any;
+}
+
+export interface PendingExchangeUpdateOperation {
+  type: "exchange-update";
+  stage: string;
+  exchangeBaseUrl: string;
+  lastError?: OperationError;
+}
+
+export interface PendingBugOperation {
+  type: "bug";
+  message: string;
+  details: any;
 }
 
-export type PendingOperationInfo = PendingWithdrawOperation
+export type PendingOperationInfo =
+  | PendingWithdrawOperation
+  | PendingBugOperation
+  | PendingExchangeUpdateOperation;
 
 export interface PendingOperationsResponse {
   pendingOperations: PendingOperationInfo[];
@@ -541,4 +561,24 @@ export interface HistoryQuery {
    * Level 1: All events.
    */
   level: number;
-}
\ No newline at end of file
+}
+
+export interface Timestamp {
+  /**
+   * Timestamp in milliseconds.
+   */
+  t_ms: number;
+}
+
+export interface Duration {
+  /**
+   * Duration in milliseconds.
+   */
+  d_ms: number;
+}
+
+export function getTimestampNow(): Timestamp {
+  return {
+    t_ms: new Date().getTime(),
+  };
+}
diff --git a/src/webex/messages.ts b/src/webex/messages.ts
index 78a1a1fd..3f6e5cc4 100644
--- a/src/webex/messages.ts
+++ b/src/webex/messages.ts
@@ -73,14 +73,6 @@ export interface MessageMap {
     request: { baseUrl: string };
     response: dbTypes.ExchangeRecord;
   };
-  "currency-info": {
-    request: { name: string };
-    response: dbTypes.CurrencyRecord;
-  };
-  "hash-contract": {
-    request: { contract: object };
-    response: string;
-  };
   "reserve-creation-info": {
     request: { baseUrl: string; amount: AmountJson };
     response: walletTypes.ReserveCreationInfo;
@@ -145,14 +137,6 @@ export interface MessageMap {
     request: {};
     response: void;
   };
-  "log-and-display-error": {
-    request: any;
-    response: void;
-  };
-  "get-report": {
-    request: { reportUid: string };
-    response: void;
-  };
   "get-purchase-details": {
     request: { contractTermsHash: string };
     response: walletTypes.PurchaseDetails;
diff --git a/src/webex/pages/error.html b/src/webex/pages/error.html
deleted file mode 100644
index 51a8fd73..00000000
--- a/src/webex/pages/error.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<!DOCTYPE html>
-<html>
-
-<head>
-  <meta charset="UTF-8">
-  <title>Taler Wallet: Error Occured</title>
-
-  <link rel="stylesheet" type="text/css" href="../style/wallet.css">
-
-  <link rel="icon" href="/img/icon.png">
-
-  <script src="/dist/page-common-bundle.js"></script>
-  <script src="/dist/error-bundle.js"></script>
-
-  <body>
-    <div id="container"></div>
-  </body>
-</html>
diff --git a/src/webex/pages/error.tsx b/src/webex/pages/error.tsx
deleted file mode 100644
index dee8ce44..00000000
--- a/src/webex/pages/error.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- This file is part of TALER
- (C) 2015-2016 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/>
- */
-
-
-/**
- * Page shown to the user to confirm creation
- * of a reserve, usually requested by the bank.
- *
- * @author Florian Dold
- */
-
-
-import * as React from "react";
-import * as ReactDOM from "react-dom";
-import URI = require("urijs");
-
-import * as wxApi from "../wxApi";
-
-import { Collapsible } from "../renderHtml";
-
-interface ErrorProps {
-  report: any;
-}
-
-class ErrorView extends React.Component<ErrorProps, { }> {
-  render(): JSX.Element {
-    const report = this.props.report;
-    if (!report) {
-      return (
-        <div id="main">
-          <h1>Error Report Not Found</h1>
-          <p>This page is supposed to display an error reported by the GNU 
Taler wallet,
-              but the corresponding error report can't be found.</p>
-          <p>Maybe the error occured before the browser was restarted or the 
wallet was reloaded.</p>
-        </div>
-      );
-    }
-    try {
-      switch (report.name) {
-        case "pay-post-failed": {
-          const summary = report.contractTerms.summary || 
report.contractTerms.order_id;
-          return (
-            <div id="main">
-              <h1>Failed to send payment</h1>
-              <p>
-                Failed to send payment for <strong>{summary}</strong>{" "}
-                to merchant 
<strong>{report.contractTerms.merchant.name}</strong>.
-              </p>
-              <p>
-                You can <a 
href={report.contractTerms.fulfillment_url}>retry</a> the payment.{" "}
-                If this problem persists, please contact the mechant with the 
error details below.
-              </p>
-              <Collapsible initiallyCollapsed={true} title="Error Details">
-                <pre>
-                  {JSON.stringify(report, null, " ")}
-                </pre>
-              </Collapsible>
-            </div>
-          );
-        }
-        default:
-          return (
-            <div id="main">
-              <h1>Unknown Error</h1>
-              The GNU Taler wallet reported an unknown error.  Here are the 
details:
-              <pre>
-                {JSON.stringify(report, null, " ")}
-              </pre>
-            </div>
-          );
-      }
-    } catch (e) {
-        return (
-          <div id="main">
-            <h1>Error</h1>
-            The GNU Taler wallet reported an error.  Here are the details:
-            <pre>
-              {JSON.stringify(report, null, " ")}
-            </pre>
-            A detailed error report could not be generated:
-            <pre>
-              {e.toString()}
-            </pre>
-          </div>
-        );
-    }
-  }
-}
-
-async function main() {
-  const url = new URI(document.location.href);
-  const query: any = URI.parseQuery(url.query());
-
-  const container = document.getElementById("container");
-  if (!container) {
-    console.error("fatal: can't mount component, countainer missing");
-    return;
-  }
-
-  // report that we'll render, either looked up from the
-  // logging module or synthesized here for fixed/fatal errors
-  let report;
-
-  const reportUid: string = query.reportUid;
-  if (!reportUid) {
-    report = {
-      name: "missing-error",
-    };
-  } else {
-    report = await wxApi.getReport(reportUid);
-  }
-
-  ReactDOM.render(<ErrorView report={report} />, container);
-}
-
-document.addEventListener("DOMContentLoaded", () => main());
diff --git a/src/webex/pages/logs.html b/src/webex/pages/logs.html
deleted file mode 100644
index 9545269e..00000000
--- a/src/webex/pages/logs.html
+++ /dev/null
@@ -1,27 +0,0 @@
-<!DOCTYPE html>
-<html>
-
-<head>
-  <meta charset="UTF-8">
-  <title>Taler Wallet: Logs</title>
-
-  <link rel="stylesheet" type="text/css" href="../style/wallet.css">
-
-  <link rel="icon" href="/img/icon.png">
-
-  <script src="/dist/page-common-bundle.js"></script>
-  <script src="/dist/logs-bundle.js"></script>
-
-  <style>
-    .tree-item {
-            margin: 2em;
-            border-radius: 5px;
-            border: 1px solid gray;
-            padding: 1em;
-    }
-  </style>
-
-  <body>
-    <div id="container"></div>
-  </body>
-</html>
diff --git a/src/webex/pages/logs.tsx b/src/webex/pages/logs.tsx
deleted file mode 100644
index c4fe670a..00000000
--- a/src/webex/pages/logs.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- This file is part of TALER
- (C) 2016 Inria
-
- 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/>
- */
-
-/**
- * Show wallet logs.
- *
- * @author Florian Dold
- */
-
-import {
-  LogEntry,
-  getLogs,
-} from "../../logging";
-
-import * as React from "react";
-import * as ReactDOM from "react-dom";
-
-interface LogViewProps {
-  log: LogEntry;
-}
-
-class LogView extends React.Component<LogViewProps, {}> {
-  render(): JSX.Element {
-    const e = this.props.log;
-    return (
-      <div className="tree-item">
-        <ul>
-          <li>level: {e.level}</li>
-          <li>msg: {e.msg}</li>
-          <li>id: {e.id || "unknown"}</li>
-          <li>file: {e.source || "(unknown)"}</li>
-          <li>line: {e.line || "(unknown)"}</li>
-          <li>col: {e.col || "(unknown)"}</li>
-          {(e.detail ? <li> detail: <pre>{e.detail}</pre></li> : [])}
-        </ul>
-      </div>
-    );
-  }
-}
-
-interface LogsState {
-  logs: LogEntry[]|undefined;
-}
-
-class Logs extends React.Component<{}, LogsState> {
-  constructor(props: {}) {
-    super({});
-    this.update();
-    this.state = {} as any;
-  }
-
-  async update() {
-    const logs = await getLogs();
-    this.setState({logs});
-  }
-
-  render(): JSX.Element {
-    const logs = this.state.logs;
-    if (!logs) {
-      return <span>...</span>;
-    }
-    return (
-      <div className="tree-item">
-        Logs:
-        {logs.map((e) => <LogView log={e} />)}
-      </div>
-    );
-  }
-}
-
-document.addEventListener("DOMContentLoaded", () => {
-  ReactDOM.render(<Logs />, document.getElementById("container")!);
-});
diff --git a/src/webex/renderHtml.tsx b/src/webex/renderHtml.tsx
index e2f82105..f2cccfba 100644
--- a/src/webex/renderHtml.tsx
+++ b/src/webex/renderHtml.tsx
@@ -137,12 +137,12 @@ function AuditorDetailsView(props: {
       </p>
     );
   }
-  if (rci.exchangeInfo.auditors.length === 0) {
+  if ((rci.exchangeInfo.details?.auditors ?? []).length === 0) {
     return <p>The exchange is not audited by any auditors.</p>;
   }
   return (
     <div>
-      {rci.exchangeInfo.auditors.map(a => (
+      {(rci.exchangeInfo.details?.auditors ?? []).map(a => (
         <div>
           <h3>Auditor {a.auditor_url}</h3>
           <p>
@@ -231,7 +231,7 @@ function FeeDetailsView(props: {
     <div>
       <h3>Overview</h3>
       <p>
-        Public key: <ExpanderText text={rci.exchangeInfo.masterPublicKey} />
+        Public key: <ExpanderText 
text={rci.exchangeInfo.details?.masterPublicKey ?? "??"} />
       </p>
       <p>
         {i18n.str`Withdrawal fees:`} {withdrawFee}
diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts
index 39c31ca5..a5067213 100644
--- a/src/webex/wxApi.ts
+++ b/src/webex/wxApi.ts
@@ -123,13 +123,6 @@ export function getCurrencies(): Promise<CurrencyRecord[]> 
{
 }
 
 
-/**
- * Get information about a specific currency.
- */
-export function getCurrency(name: string): Promise<CurrencyRecord|null> {
-  return callBackend("currency-info", {name});
-}
-
 
 /**
  * Get information about a specific exchange.
@@ -225,12 +218,6 @@ export function submitPay(contractTermsHash: string, 
sessionId: string | undefin
   return callBackend("submit-pay", { contractTermsHash, sessionId });
 }
 
-/**
- * Hash a contract.  Throws if its not a valid contract.
- */
-export function hashContract(contract: object): Promise<string> {
-  return callBackend("hash-contract", { contract });
-}
 
 /**
  * Mark a reserve as confirmed.
@@ -284,25 +271,6 @@ export function returnCoins(args: { amount: AmountJson, 
exchange: string, sender
 }
 
 
-/**
- * Record an error report and display it in a tabl.
- *
- * If sameTab is set, the error report will be opened in the current tab,
- * otherwise in a new tab.
- */
-export function logAndDisplayError(args: any): Promise<void> {
-  return callBackend("log-and-display-error", args);
-}
-
-/**
- * Get an error report from the logging database for the
- * given report UID.
- */
-export function getReport(reportUid: string): Promise<any> {
-  return callBackend("get-report", { reportUid });
-}
-
-
 /**
  * Look up a purchase in the wallet database from
  * the contract terms hash.
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index 16cd2a78..f4decbc6 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -24,7 +24,6 @@
  * Imports.
  */
 import { BrowserHttpLib } from "../http";
-import * as logging from "../logging";
 import { AmountJson } from "../amounts";
 import {
   ConfirmReserveRequest,
@@ -138,22 +137,6 @@ async function handleMessage(
       }
       return needsWallet().updateExchangeFromUrl(detail.baseUrl);
     }
-    case "currency-info": {
-      if (!detail.name) {
-        return Promise.resolve({ error: "name missing" });
-      }
-      return needsWallet().getCurrencyRecord(detail.name);
-    }
-    case "hash-contract": {
-      if (!detail.contract) {
-        return Promise.resolve({ error: "contract missing" });
-      }
-      return needsWallet()
-        .hashContract(detail.contract)
-        .then(hash => {
-          return hash;
-        });
-    }
     case "reserve-creation-info": {
       if (!detail.baseUrl || typeof detail.baseUrl !== "string") {
         return Promise.resolve({ error: "bad url" });
@@ -243,20 +226,6 @@ async function handleMessage(
       };
       return resp;
     }
-    case "log-and-display-error":
-      logging.storeReport(detail).then(reportUid => {
-        const url = chrome.extension.getURL(
-          `/src/webex/pages/error.html?reportUid=${reportUid}`,
-        );
-        if (detail.sameTab && sender && sender.tab && sender.tab.id) {
-          chrome.tabs.update(detail.tabId, { url });
-        } else {
-          chrome.tabs.create({ url });
-        }
-      });
-      return;
-    case "get-report":
-      return logging.getReport(detail.reportUid);
     case "get-purchase-details": {
       const contractTermsHash = detail.contractTermsHash;
       if (!contractTermsHash) {
@@ -574,17 +543,6 @@ export async function wxMain() {
     chrome.runtime.reload();
   });
 
-  window.onerror = (m, source, lineno, colno, error) => {
-    logging.record(
-      "error",
-      "".concat(m as any, error as any),
-      undefined,
-      source || "(unknown)",
-      lineno || 0,
-      colno || 0,
-    );
-  };
-
   chrome.tabs.query({}, tabs => {
     console.log("got tabs", tabs);
     for (const tab of tabs) {
diff --git a/tsconfig.json b/tsconfig.json
index e190e14b..25087b60 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -53,7 +53,6 @@
     "src/index.ts",
     "src/libtoolVersion-test.ts",
     "src/libtoolVersion.ts",
-    "src/logging.ts",
     "src/promiseUtils.ts",
     "src/query.ts",
     "src/talerTypes.ts",
@@ -72,8 +71,6 @@
     "src/webex/pages/add-auditor.tsx",
     "src/webex/pages/auditors.tsx",
     "src/webex/pages/benchmark.tsx",
-    "src/webex/pages/error.tsx",
-    "src/webex/pages/logs.tsx",
     "src/webex/pages/pay.tsx",
     "src/webex/pages/payback.tsx",
     "src/webex/pages/popup.tsx",
diff --git a/webpack.config.js b/webpack.config.js
index 09645588..ab893eac 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -80,8 +80,6 @@ module.exports = function (env) {
       "pay": "./src/webex/pages/pay.tsx",
       "withdraw": "./src/webex/pages/withdraw.tsx",
       "welcome": "./src/webex/pages/welcome.tsx",
-      "error": "./src/webex/pages/error.tsx",
-      "logs": "./src/webex/pages/logs.tsx",
       "payback": "./src/webex/pages/payback.tsx",
       "popup": "./src/webex/pages/popup.tsx",
       "reset-required": "./src/webex/pages/reset-required.tsx",
diff --git a/yarn.lock b/yarn.lock
index 2e7ec95c..dea3f786 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3378,10 +3378,10 @@ iconv-lite@0.4.24, iconv-lite@^0.4.4, 
iconv-lite@~0.4.13:
   dependencies:
     safer-buffer ">= 2.1.2 < 3"
 
-idb-bridge@^0.0.10:
-  version "0.0.10"
-  resolved 
"https://registry.yarnpkg.com/idb-bridge/-/idb-bridge-0.0.10.tgz#69d59550dc722f6bf62cb98a4d4a2f2b9e66653c";
-  integrity 
sha512-AE8YipDGjnS+APSlupcPNCNslCAErRwfUgJ8aNZigmVIr3xzI0YPGWr6NC6UI6ofpQ1DczUlaKqA86m1R/COGQ==
+idb-bridge@^0.0.11:
+  version "0.0.11"
+  resolved 
"https://registry.yarnpkg.com/idb-bridge/-/idb-bridge-0.0.11.tgz#ba2fbd24b7e6f7f4de8333ed12b0912e64dda308";
+  integrity 
sha512-fLlHce/WwT6eD3sc54gsfvM5fZqrhAPwBNH4uU/y6D0C1+0higH7OgC5/wploMhkmNYkQID3BMNZvSUBr0leSQ==
 
 ieee754@^1.1.4:
   version "1.1.13"

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



reply via email to

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