gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (74433c3e -> 04a60770)


From: gnunet
Subject: [taler-wallet-core] branch master updated (74433c3e -> 04a60770)
Date: Fri, 13 Dec 2019 13:10:24 +0100

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

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

    from 74433c3e refactor: re-structure type definitions
     new 24650b17 organize imports
     new f3329ecf refactor DB access
     new 04a60770 new history schema

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


Summary of changes:
 src/db.ts                   | 129 ++-------
 src/headless/helpers.ts     |   9 +-
 src/operations/balance.ts   |   5 +-
 src/operations/exchanges.ts |  34 +--
 src/operations/history.ts   | 168 +----------
 src/operations/pay.ts       | 103 +++----
 src/operations/payback.ts   |  16 +-
 src/operations/pending.ts   |   5 +-
 src/operations/refresh.ts   |  68 ++---
 src/operations/reserves.ts  |  38 +--
 src/operations/return.ts    |  25 +-
 src/operations/state.ts     |   3 +-
 src/operations/tip.ts       |  22 +-
 src/operations/withdraw.ts  |  53 ++--
 src/types/dbTypes.ts        |   8 -
 src/types/history.ts        | 693 ++++++++++++++++++++++++++++++++++++++++++--
 src/types/walletTypes.ts    |   8 +-
 src/util/query.ts           | 302 ++++++++++++-------
 src/wallet.ts               |  46 ++-
 src/webex/pages/popup.tsx   |   4 +-
 src/webex/wxBackend.ts      |  40 +--
 21 files changed, 1087 insertions(+), 692 deletions(-)

diff --git a/src/db.ts b/src/db.ts
index 70338122..e1c5c8f8 100644
--- a/src/db.ts
+++ b/src/db.ts
@@ -1,118 +1,35 @@
-import { Stores, WALLET_DB_VERSION } from "./types/dbTypes";
-import { Store, Index } from "./util/query";
+import { Stores } from "./types/dbTypes";
+import { openDatabase, Database } from "./util/query";
 
-const DB_NAME = "taler";
+const TALER_DB_NAME = "taler";
+
+/**
+ * Current database version, should be incremented
+ * each time we do incompatible schema changes on the database.
+ * In the future we might consider adding migration functions for
+ * each version increment.
+ */
+export const WALLET_DB_VERSION = 28;
 
 /**
  * Return a promise that resolves
  * to the taler wallet db.
  */
-export function openDatabase(
+export function openTalerDatabase(
   idbFactory: IDBFactory,
   onVersionChange: () => void,
   onUpgradeUnsupported: (oldVersion: number, newVersion: number) => void,
 ): Promise<IDBDatabase> {
-  return new Promise<IDBDatabase>((resolve, reject) => {
-    const req = idbFactory.open(DB_NAME, WALLET_DB_VERSION);
-    req.onerror = e => {
-      console.log("taler database error", e);
-      reject(new Error("database error"));
-    };
-    req.onsuccess = e => {
-      req.result.onversionchange = (evt: IDBVersionChangeEvent) => {
-        console.log(
-          `handling live db version change from ${evt.oldVersion} to 
${evt.newVersion}`,
-        );
-        req.result.close();
-        onVersionChange();
-      };
-      resolve(req.result);
-    };
-    req.onupgradeneeded = e => {
-      const db = req.result;
-      console.log(
-        `DB: upgrade needed: oldVersion=${e.oldVersion}, 
newVersion=${e.newVersion}`,
-      );
-      switch (e.oldVersion) {
-        case 0: // DB does not exist yet
-          for (const n in Stores) {
-            if ((Stores as any)[n] instanceof Store) {
-              const si: Store<any> = (Stores as any)[n];
-              const s = db.createObjectStore(si.name, si.storeParams);
-              for (const indexName in si as any) {
-                if ((si as any)[indexName] instanceof Index) {
-                  const ii: Index<any, any> = (si as any)[indexName];
-                  s.createIndex(ii.indexName, ii.keyPath, ii.options);
-                }
-              }
-            }
-          }
-          break;
-        default:
-          if (e.oldVersion !== WALLET_DB_VERSION) {
-            onUpgradeUnsupported(e.oldVersion, WALLET_DB_VERSION);
-            throw Error("incompatible DB");
-          }
-          break;
-      }
-    };
-  });
+  return openDatabase(
+    idbFactory,
+    TALER_DB_NAME,
+    WALLET_DB_VERSION,
+    Stores,
+    onVersionChange,
+    onUpgradeUnsupported,
+  );
 }
 
-export function exportDatabase(db: IDBDatabase): Promise<any> {
-  const dump = {
-    name: db.name,
-    stores: {} as { [s: string]: any },
-    version: db.version,
-  };
-
-  return new Promise((resolve, reject) => {
-    const tx = db.transaction(Array.from(db.objectStoreNames));
-    tx.addEventListener("complete", () => {
-      resolve(dump);
-    });
-    // tslint:disable-next-line:prefer-for-of
-    for (let i = 0; i < db.objectStoreNames.length; i++) {
-      const name = db.objectStoreNames[i];
-      const storeDump = {} as { [s: string]: any };
-      dump.stores[name] = storeDump;
-      tx.objectStore(name)
-        .openCursor()
-        .addEventListener("success", (e: Event) => {
-          const cursor = (e.target as any).result;
-          if (cursor) {
-            storeDump[cursor.key] = cursor.value;
-            cursor.continue();
-          }
-        });
-    }
-  });
-}
-
-export function importDatabase(db: IDBDatabase, dump: any): Promise<void> {
-  console.log("importing db", dump);
-  return new Promise<void>((resolve, reject) => {
-    const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite");
-    if (dump.stores) {
-      for (const storeName in dump.stores) {
-        const objects = [];
-        const dumpStore = dump.stores[storeName];
-        for (const key in dumpStore) {
-          objects.push(dumpStore[key]);
-        }
-        console.log(`importing ${objects.length} records into ${storeName}`);
-        const store = tx.objectStore(storeName);
-        for (const obj of objects) {
-          store.put(obj);
-        }
-      }
-    }
-    tx.addEventListener("complete", () => {
-      resolve();
-    });
-  });
-}
-
-export function deleteDatabase(idbFactory: IDBFactory) {
-  idbFactory.deleteDatabase(DB_NAME);
-}
+export function deleteTalerDatabase(idbFactory: IDBFactory) {
+  Database.deleteDatabase(idbFactory, TALER_DB_NAME);
+}
\ No newline at end of file
diff --git a/src/headless/helpers.ts b/src/headless/helpers.ts
index 2c0824a7..33304cd0 100644
--- a/src/headless/helpers.ts
+++ b/src/headless/helpers.ts
@@ -23,7 +23,7 @@
  */
 import { Wallet } from "../wallet";
 import { MemoryBackend, BridgeIDBFactory, shimIndexedDB } from "idb-bridge";
-import { openDatabase } from "../db";
+import { openTalerDatabase } from "../db";
 import Axios, { AxiosPromise, AxiosResponse } from "axios";
 import {
   HttpRequestLibrary,
@@ -39,6 +39,7 @@ import { NodeThreadCryptoWorkerFactory } from 
"../crypto/workers/nodeThreadWorke
 import { SynchronousCryptoWorkerFactory } from 
"../crypto/workers/synchronousWorker";
 import { RequestThrottler } from "../util/RequestThrottler";
 import { WalletNotification, NotificationType } from "../types/notifications";
+import { Database } from "../util/query";
 
 const logger = new Logger("helpers.ts");
 
@@ -191,7 +192,7 @@ export async function getDefaultNodeWallet(
 
   shimIndexedDB(myBridgeIdbFactory);
 
-  const myDb = await openDatabase(
+  const myDb = await openTalerDatabase(
     myIdbFactory,
     myVersionChange,
     myUnsupportedUpgrade,
@@ -202,7 +203,9 @@ export async function getDefaultNodeWallet(
 
   const worker = new NodeThreadCryptoWorkerFactory();
 
-  const w = new Wallet(myDb, myHttpLib, worker);
+  const dbWrap = new Database(myDb);
+
+  const w = new Wallet(dbWrap, myHttpLib, worker);
   if (args.notifyHandler) {
     w.addNotificationListener(args.notifyHandler);
   }
diff --git a/src/operations/balance.ts b/src/operations/balance.ts
index 8c8a2a9c..f5a51abe 100644
--- a/src/operations/balance.ts
+++ b/src/operations/balance.ts
@@ -18,7 +18,7 @@
  * Imports.
  */
 import { WalletBalance, WalletBalanceEntry } from "../types/walletTypes";
-import { runWithReadTransaction } from "../util/query";
+import { Database } from "../util/query";
 import { InternalWalletState } from "./state";
 import { Stores, TipRecord, CoinStatus } from "../types/dbTypes";
 import * as Amounts from "../util/amounts";
@@ -73,8 +73,7 @@ export async function getBalances(
     byExchange: {},
   };
 
-  await runWithReadTransaction(
-    ws.db,
+  await ws.db.runWithReadTransaction(
     [Stores.coins, Stores.refresh, Stores.reserves, Stores.purchases, 
Stores.withdrawalSession],
     async tx => {
       await tx.iter(Stores.coins).forEach(c => {
diff --git a/src/operations/exchanges.ts b/src/operations/exchanges.ts
index 836bce6e..6c4c1aa0 100644
--- a/src/operations/exchanges.ts
+++ b/src/operations/exchanges.ts
@@ -32,10 +32,7 @@ import {
   extractTalerStampOrThrow,
 } from "../util/helpers";
 import {
-  oneShotGet,
-  oneShotPut,
-  runWithWriteTransaction,
-  oneShotMutate,
+  Database
 } from "../util/query";
 import * as Amounts from "../util/amounts";
 import { parsePaytoUri } from "../util/payto";
@@ -81,7 +78,7 @@ async function setExchangeError(
     exchange.lastError = err;
     return exchange;
   };
-  await oneShotMutate(ws.db, Stores.exchanges, baseUrl, mut);
+  await ws.db.mutate( Stores.exchanges, baseUrl, mut);
 }
 
 /**
@@ -94,8 +91,7 @@ async function updateExchangeWithKeys(
   ws: InternalWalletState,
   baseUrl: string,
 ): Promise<void> {
-  const existingExchangeRecord = await oneShotGet(
-    ws.db,
+  const existingExchangeRecord = await ws.db.get(
     Stores.exchanges,
     baseUrl,
   );
@@ -180,8 +176,7 @@ async function updateExchangeWithKeys(
     ),
   );
 
-  await runWithWriteTransaction(
-    ws.db,
+  await ws.db.runWithWriteTransaction(
     [Stores.exchanges, Stores.denominations],
     async tx => {
       const r = await tx.get(Stores.exchanges, baseUrl);
@@ -222,7 +217,7 @@ async function updateExchangeWithTermsOfService(
   ws: InternalWalletState,
   exchangeBaseUrl: string,
 ) {
-  const exchange = await oneShotGet(ws.db, Stores.exchanges, exchangeBaseUrl);
+  const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
   if (!exchange) {
     return;
   }
@@ -243,7 +238,7 @@ async function updateExchangeWithTermsOfService(
   const tosText = await resp.text();
   const tosEtag = resp.headers.get("etag") || undefined;
 
-  await runWithWriteTransaction(ws.db, [Stores.exchanges], async tx => {
+  await ws.db.runWithWriteTransaction([Stores.exchanges], async tx => {
     const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
     if (!r) {
       return;
@@ -263,7 +258,7 @@ export async function acceptExchangeTermsOfService(
   exchangeBaseUrl: string,
   etag: string | undefined,
 ) {
-  await runWithWriteTransaction(ws.db, [Stores.exchanges], async tx => {
+  await ws.db.runWithWriteTransaction([Stores.exchanges], async tx => {
     const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
     if (!r) {
       return;
@@ -283,7 +278,7 @@ async function updateExchangeWithWireInfo(
   ws: InternalWalletState,
   exchangeBaseUrl: string,
 ) {
-  const exchange = await oneShotGet(ws.db, Stores.exchanges, exchangeBaseUrl);
+  const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
   if (!exchange) {
     return;
   }
@@ -349,7 +344,7 @@ async function updateExchangeWithWireInfo(
     feesForType[wireMethod] = feeList;
   }
 
-  await runWithWriteTransaction(ws.db, [Stores.exchanges], async tx => {
+  await ws.db.runWithWriteTransaction([Stores.exchanges], async tx => {
     const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
     if (!r) {
       return;
@@ -392,7 +387,7 @@ async function updateExchangeFromUrlImpl(
   const now = getTimestampNow();
   baseUrl = canonicalizeBaseUrl(baseUrl);
 
-  const r = await oneShotGet(ws.db, Stores.exchanges, baseUrl);
+  const r = await ws.db.get(Stores.exchanges, baseUrl);
   if (!r) {
     const newExchangeRecord: ExchangeRecord = {
       baseUrl: baseUrl,
@@ -407,9 +402,9 @@ async function updateExchangeFromUrlImpl(
       termsOfServiceLastEtag: undefined,
       termsOfServiceText: undefined,
     };
-    await oneShotPut(ws.db, Stores.exchanges, newExchangeRecord);
+    await ws.db.put(Stores.exchanges, newExchangeRecord);
   } else {
-    await runWithWriteTransaction(ws.db, [Stores.exchanges], async t => {
+    await ws.db.runWithWriteTransaction([Stores.exchanges], async t => {
       const rec = await t.get(Stores.exchanges, baseUrl);
       if (!rec) {
         return;
@@ -431,7 +426,7 @@ async function updateExchangeFromUrlImpl(
   await updateExchangeWithWireInfo(ws, baseUrl);
   await updateExchangeWithTermsOfService(ws, baseUrl);
 
-  const updatedExchange = await oneShotGet(ws.db, Stores.exchanges, baseUrl);
+  const updatedExchange = await ws.db.get(Stores.exchanges, baseUrl);
 
   if (!updatedExchange) {
     // This should practically never happen
@@ -453,8 +448,7 @@ export async function getExchangeTrust(
   if (!exchangeDetails) {
     throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
   }
-  const currencyRecord = await oneShotGet(
-    ws.db,
+  const currencyRecord = await ws.db.get(
     Stores.currencies,
     exchangeDetails.currency,
   );
diff --git a/src/operations/history.ts b/src/operations/history.ts
index 9c4bb6a9..64f5b21c 100644
--- a/src/operations/history.ts
+++ b/src/operations/history.ts
@@ -17,12 +17,11 @@
 /**
  * Imports.
  */
-import { oneShotIter, runWithReadTransaction } from "../util/query";
 import { InternalWalletState } from "./state";
 import { Stores, TipRecord } from "../types/dbTypes";
 import * as Amounts from "../util/amounts";
 import { AmountJson } from "../util/amounts";
-import { HistoryQuery, HistoryEvent } from "../types/history";
+import { HistoryQuery, HistoryEvent, HistoryEventType } from 
"../types/history";
 
 /**
  * Retrive the full event history for this wallet.
@@ -38,8 +37,7 @@ export async function getHistory(
   // This works as timestamps are guaranteed to be monotonically
   // increasing even
 
-  await runWithReadTransaction(
-    ws.db,
+  await ws.db.runWithReadTransaction(
     [
       Stores.currencies,
       Stores.coins,
@@ -53,166 +51,8 @@ export async function getHistory(
       Stores.withdrawalSession,
     ],
     async tx => {
-      await tx.iter(Stores.proposals).forEach(p => {
-        history.push({
-          detail: {},
-          timestamp: p.timestamp,
-          type: "claim-order",
-          explicit: false,
-        });
-      });
-
-      await tx.iter(Stores.withdrawalSession).forEach(w => {
-        history.push({
-          detail: {
-            withdrawalAmount: w.rawWithdrawalAmount,
-          },
-          timestamp: w.startTimestamp,
-          type: "withdraw-started",
-          explicit: false,
-        });
-        if (w.finishTimestamp) {
-          history.push({
-            detail: {
-              withdrawalAmount: w.rawWithdrawalAmount,
-            },
-            timestamp: w.finishTimestamp,
-            type: "withdraw-finished",
-            explicit: false,
-          });
-        }
-      });
-
-      await tx.iter(Stores.purchases).forEach(p => {
-        history.push({
-          detail: {
-            amount: p.contractTerms.amount,
-            contractTermsHash: p.contractTermsHash,
-            fulfillmentUrl: p.contractTerms.fulfillment_url,
-            merchantName: p.contractTerms.merchant.name,
-          },
-          timestamp: p.acceptTimestamp,
-          type: "pay-started",
-          explicit: false,
-        });
-        if (p.firstSuccessfulPayTimestamp) {
-          history.push({
-            detail: {
-              amount: p.contractTerms.amount,
-              contractTermsHash: p.contractTermsHash,
-              fulfillmentUrl: p.contractTerms.fulfillment_url,
-              merchantName: p.contractTerms.merchant.name,
-            },
-            timestamp: p.firstSuccessfulPayTimestamp,
-            type: "pay-finished",
-            explicit: false,
-          });
-        }
-        if (p.lastRefundStatusTimestamp) {
-          const contractAmount = Amounts.parseOrThrow(p.contractTerms.amount);
-          const amountsPending = Object.keys(p.refundsPending).map(x =>
-            Amounts.parseOrThrow(p.refundsPending[x].refund_amount),
-          );
-          const amountsDone = Object.keys(p.refundsDone).map(x =>
-            Amounts.parseOrThrow(p.refundsDone[x].refund_amount),
-          );
-          const amounts: AmountJson[] = amountsPending.concat(amountsDone);
-          const amount = Amounts.add(
-            Amounts.getZero(contractAmount.currency),
-            ...amounts,
-          ).amount;
-
-          history.push({
-            detail: {
-              contractTermsHash: p.contractTermsHash,
-              fulfillmentUrl: p.contractTerms.fulfillment_url,
-              merchantName: p.contractTerms.merchant.name,
-              refundAmount: amount,
-            },
-            timestamp: p.lastRefundStatusTimestamp,
-            type: "refund",
-            explicit: false,
-          });
-        }
-      });
-
-      await tx.iter(Stores.reserves).forEach(r => {
-        const reserveType = r.bankWithdrawStatusUrl ? "taler-bank" : "manual";
-        history.push({
-          detail: {
-            exchangeBaseUrl: r.exchangeBaseUrl,
-            requestedAmount: Amounts.toString(r.initiallyRequestedAmount),
-            reservePub: r.reservePub,
-            reserveType,
-            bankWithdrawStatusUrl: r.bankWithdrawStatusUrl,
-          },
-          timestamp: r.created,
-          type: "reserve-created",
-          explicit: false,
-        });
-        if (r.timestampConfirmed) {
-          history.push({
-            detail: {
-              exchangeBaseUrl: r.exchangeBaseUrl,
-              requestedAmount: Amounts.toString(r.initiallyRequestedAmount),
-              reservePub: r.reservePub,
-              reserveType,
-              bankWithdrawStatusUrl: r.bankWithdrawStatusUrl,
-            },
-            timestamp: r.created,
-            type: "reserve-confirmed",
-            explicit: false,
-          });
-        }
-      });
-
-      await tx.iter(Stores.tips).forEach(tip => {
-        history.push({
-          detail: {
-            accepted: tip.accepted,
-            amount: tip.amount,
-            merchantBaseUrl: tip.merchantBaseUrl,
-            tipId: tip.merchantTipId,
-          },
-          timestamp: tip.createdTimestamp,
-          explicit: false,
-          type: "tip",
-        });
-      });
-
-      await tx.iter(Stores.exchanges).forEach(exchange => {
-        history.push({
-          type: "exchange-added",
-          explicit: false,
-          timestamp: exchange.timestampAdded,
-          detail: {
-            exchangeBaseUrl: exchange.baseUrl,
-          },
-        });
-      });
-
-      await tx.iter(Stores.refresh).forEach((r) => {
-        history.push({
-          type: "refresh-started",
-          explicit: false,
-          timestamp: r.created,
-          detail: {
-            refreshSessionId: r.refreshSessionId,
-          },
-        });
-        if (r.finishedTimestamp) {
-          history.push({
-            type: "refresh-finished",
-            explicit: false,
-            timestamp: r.finishedTimestamp,
-            detail: {
-              refreshSessionId: r.refreshSessionId,
-            },
-          });
-        }
-
-      });
-    },
+      // FIXME: implement new history schema!!
+    }
   );
 
   history.sort((h1, h2) => Math.sign(h1.timestamp.t_ms - h2.timestamp.t_ms));
diff --git a/src/operations/pay.ts b/src/operations/pay.ts
index 08d22792..27f0e440 100644
--- a/src/operations/pay.ts
+++ b/src/operations/pay.ts
@@ -36,13 +36,7 @@ import {
   OperationError,
 } from "../types/walletTypes";
 import {
-  oneShotIter,
-  oneShotIterIndex,
-  oneShotGet,
-  runWithWriteTransaction,
-  oneShotPut,
-  oneShotGetIndexed,
-  oneShotMutate,
+  Database
 } from "../util/query";
 import {
   Stores,
@@ -202,7 +196,7 @@ async function getCoinsForPayment(
 
   let remainingAmount = paymentAmount;
 
-  const exchanges = await oneShotIter(ws.db, Stores.exchanges).toArray();
+  const exchanges = await ws.db.iter(Stores.exchanges).toArray();
 
   for (const exchange of exchanges) {
     let isOkay: boolean = false;
@@ -242,14 +236,12 @@ async function getCoinsForPayment(
       continue;
     }
 
-    const coins = await oneShotIterIndex(
-      ws.db,
+    const coins = await ws.db.iterIndex(
       Stores.coins.exchangeBaseUrlIndex,
       exchange.baseUrl,
     ).toArray();
 
-    const denoms = await oneShotIterIndex(
-      ws.db,
+    const denoms = await ws.db.iterIndex(
       Stores.denominations.exchangeBaseUrlIndex,
       exchange.baseUrl,
     ).toArray();
@@ -260,7 +252,7 @@ async function getCoinsForPayment(
 
     // Denomination of the first coin, we assume that all other
     // coins have the same currency
-    const firstDenom = await oneShotGet(ws.db, Stores.denominations, [
+    const firstDenom = await ws.db.get(Stores.denominations, [
       exchange.baseUrl,
       coins[0].denomPub,
     ]);
@@ -270,7 +262,7 @@ async function getCoinsForPayment(
     const currency = firstDenom.value.currency;
     const cds: CoinWithDenom[] = [];
     for (const coin of coins) {
-      const denom = await oneShotGet(ws.db, Stores.denominations, [
+      const denom = await ws.db.get(Stores.denominations, [
         exchange.baseUrl,
         coin.denomPub,
       ]);
@@ -377,8 +369,7 @@ async function recordConfirmPay(
     paymentSubmitPending: true,
   };
 
-  await runWithWriteTransaction(
-    ws.db,
+  await ws.db.runWithWriteTransaction(
     [Stores.coins, Stores.purchases, Stores.proposals],
     async tx => {
       const p = await tx.get(Stores.proposals, proposal.proposalId);
@@ -417,7 +408,7 @@ export async function abortFailedPayment(
   ws: InternalWalletState,
   proposalId: string,
 ): Promise<void> {
-  const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
+  const purchase = await ws.db.get(Stores.purchases, proposalId);
   if (!purchase) {
     throw Error("Purchase not found, unable to abort with refund");
   }
@@ -434,7 +425,7 @@ export async function abortFailedPayment(
   // 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 oneShotPut(ws.db, Stores.purchases, purchase);
+  await ws.db.put(Stores.purchases, purchase);
 
   let resp;
 
@@ -457,7 +448,7 @@ export async function abortFailedPayment(
   const refundResponse = MerchantRefundResponse.checked(await resp.json());
   await acceptRefundResponse(ws, purchase.proposalId, refundResponse);
 
-  await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
+  await ws.db.runWithWriteTransaction([Stores.purchases], async tx => {
     const p = await tx.get(Stores.purchases, proposalId);
     if (!p) {
       return;
@@ -472,7 +463,7 @@ async function incrementProposalRetry(
   proposalId: string,
   err: OperationError | undefined,
 ): Promise<void> {
-  await runWithWriteTransaction(ws.db, [Stores.proposals], async tx => {
+  await ws.db.runWithWriteTransaction([Stores.proposals], async tx => {
     const pr = await tx.get(Stores.proposals, proposalId);
     if (!pr) {
       return;
@@ -494,7 +485,7 @@ async function incrementPurchasePayRetry(
   err: OperationError | undefined,
 ): Promise<void> {
   console.log("incrementing purchase pay retry with error", err);
-  await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
+  await ws.db.runWithWriteTransaction([Stores.purchases], async tx => {
     const pr = await tx.get(Stores.purchases, proposalId);
     if (!pr) {
       return;
@@ -516,7 +507,7 @@ async function incrementPurchaseQueryRefundRetry(
   err: OperationError | undefined,
 ): Promise<void> {
   console.log("incrementing purchase refund query retry with error", err);
-  await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
+  await ws.db.runWithWriteTransaction([Stores.purchases], async tx => {
     const pr = await tx.get(Stores.purchases, proposalId);
     if (!pr) {
       return;
@@ -538,7 +529,7 @@ async function incrementPurchaseApplyRefundRetry(
   err: OperationError | undefined,
 ): Promise<void> {
   console.log("incrementing purchase refund apply retry with error", err);
-  await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
+  await ws.db.runWithWriteTransaction([Stores.purchases], async tx => {
     const pr = await tx.get(Stores.purchases, proposalId);
     if (!pr) {
       return;
@@ -571,7 +562,7 @@ async function resetDownloadProposalRetry(
   ws: InternalWalletState,
   proposalId: string,
 ) {
-  await oneShotMutate(ws.db, Stores.proposals, proposalId, x => {
+  await ws.db.mutate(Stores.proposals, proposalId, x => {
     if (x.retryInfo.active) {
       x.retryInfo = initRetryInfo();
     }
@@ -587,7 +578,7 @@ async function processDownloadProposalImpl(
   if (forceNow) {
     await resetDownloadProposalRetry(ws, proposalId);
   }
-  const proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
+  const proposal = await ws.db.get(Stores.proposals, proposalId);
   if (!proposal) {
     return;
   }
@@ -621,8 +612,7 @@ async function processDownloadProposalImpl(
 
   const fulfillmentUrl = proposalResp.contract_terms.fulfillment_url;
 
-  await runWithWriteTransaction(
-    ws.db,
+  await ws.db.runWithWriteTransaction(
     [Stores.proposals, Stores.purchases],
     async tx => {
       const p = await tx.get(Stores.proposals, proposalId);
@@ -677,8 +667,7 @@ async function startDownloadProposal(
   orderId: string,
   sessionId: string | undefined,
 ): Promise<string> {
-  const oldProposal = await oneShotGetIndexed(
-    ws.db,
+  const oldProposal = await ws.db.getIndexed(
     Stores.proposals.urlAndOrderIdIndex,
     [merchantBaseUrl, orderId],
   );
@@ -705,7 +694,7 @@ async function startDownloadProposal(
     downloadSessionId: sessionId,
   };
 
-  await runWithWriteTransaction(ws.db, [Stores.proposals], async (tx) => {
+  await ws.db.runWithWriteTransaction([Stores.proposals], async (tx) => {
     const existingRecord = await 
tx.getIndexed(Stores.proposals.urlAndOrderIdIndex, [
       merchantBaseUrl,
       orderId,
@@ -725,7 +714,7 @@ export async function submitPay(
   ws: InternalWalletState,
   proposalId: string,
 ): Promise<ConfirmPayResult> {
-  const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
+  const purchase = await ws.db.get(Stores.purchases, proposalId);
   if (!purchase) {
     throw Error("Purchase not found: " + proposalId);
   }
@@ -788,7 +777,7 @@ export async function submitPay(
 
   const modifiedCoins: CoinRecord[] = [];
   for (const pc of purchase.payReq.coins) {
-    const c = await oneShotGet(ws.db, Stores.coins, pc.coin_pub);
+    const c = await ws.db.get(Stores.coins, pc.coin_pub);
     if (!c) {
       console.error("coin not found");
       throw Error("coin used in payment not found");
@@ -797,8 +786,7 @@ export async function submitPay(
     modifiedCoins.push(c);
   }
 
-  await runWithWriteTransaction(
-    ws.db,
+  await ws.db.runWithWriteTransaction(
     [Stores.coins, Stores.purchases],
     async tx => {
       for (let c of modifiedCoins) {
@@ -849,7 +837,7 @@ export async function preparePay(
     uriResult.sessionId,
   );
 
-  let proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
+  let proposal = await ws.db.get(Stores.proposals, proposalId);
   if (!proposal) {
     throw Error(`could not get proposal ${proposalId}`);
   }
@@ -859,7 +847,7 @@ export async function preparePay(
       throw Error("invalid proposal state");
     }
     console.log("using existing purchase for same product");
-    proposal = await oneShotGet(ws.db, Stores.proposals, existingProposalId);
+    proposal = await ws.db.get(Stores.proposals, existingProposalId);
     if (!proposal) {
       throw Error("existing proposal is in wrong state");
     }
@@ -878,7 +866,7 @@ export async function preparePay(
   proposalId = proposal.proposalId;
 
   // First check if we already payed for it.
-  const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
+  const purchase = await ws.db.get(Stores.purchases, proposalId);
 
   if (!purchase) {
     const paymentAmount = Amounts.parseOrThrow(contractTerms.amount);
@@ -966,7 +954,7 @@ async function getSpeculativePayData(
   const coinKeys = sp.payCoinInfo.updatedCoins.map(x => x.coinPub);
   const coins: CoinRecord[] = [];
   for (let coinKey of coinKeys) {
-    const cc = await oneShotGet(ws.db, Stores.coins, coinKey);
+    const cc = await ws.db.get(Stores.coins, coinKey);
     if (cc) {
       coins.push(cc);
     }
@@ -997,7 +985,7 @@ export async function confirmPay(
   logger.trace(
     `executing confirmPay with proposalId ${proposalId} and sessionIdOverride 
${sessionIdOverride}`,
   );
-  const proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
+  const proposal = await ws.db.get(Stores.proposals, proposalId);
 
   if (!proposal) {
     throw Error(`proposal with id ${proposalId} not found`);
@@ -1008,7 +996,7 @@ export async function confirmPay(
     throw Error("proposal is in invalid state");
   }
 
-  let purchase = await oneShotGet(ws.db, Stores.purchases, 
d.contractTermsHash);
+  let purchase = await ws.db.get(Stores.purchases, d.contractTermsHash);
 
   if (purchase) {
     if (
@@ -1016,7 +1004,7 @@ export async function confirmPay(
       sessionIdOverride != purchase.lastSessionId
     ) {
       logger.trace(`changing session ID to ${sessionIdOverride}`);
-      await oneShotMutate(ws.db, Stores.purchases, purchase.proposalId, x => {
+      await ws.db.mutate(Stores.purchases, purchase.proposalId, x => {
         x.lastSessionId = sessionIdOverride;
         x.paymentSubmitPending = true;
         return x;
@@ -1092,8 +1080,7 @@ export async function getFullRefundFees(
   if (refundPermissions.length === 0) {
     throw Error("no refunds given");
   }
-  const coin0 = await oneShotGet(
-    ws.db,
+  const coin0 = await ws.db.get(
     Stores.coins,
     refundPermissions[0].coin_pub,
   );
@@ -1104,18 +1091,17 @@ export async function getFullRefundFees(
     Amounts.parseOrThrow(refundPermissions[0].refund_amount).currency,
   );
 
-  const denoms = await oneShotIterIndex(
-    ws.db,
+  const denoms = await ws.db.iterIndex(
     Stores.denominations.exchangeBaseUrlIndex,
     coin0.exchangeBaseUrl,
   ).toArray();
 
   for (const rp of refundPermissions) {
-    const coin = await oneShotGet(ws.db, Stores.coins, rp.coin_pub);
+    const coin = await ws.db.get(Stores.coins, rp.coin_pub);
     if (!coin) {
       throw Error("coin not found");
     }
-    const denom = await oneShotGet(ws.db, Stores.denominations, [
+    const denom = await ws.db.get(Stores.denominations, [
       coin0.exchangeBaseUrl,
       coin.denomPub,
     ]);
@@ -1147,7 +1133,7 @@ async function acceptRefundResponse(
 
   let numNewRefunds = 0;
 
-  await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
+  await ws.db.runWithWriteTransaction([Stores.purchases], async tx => {
     const p = await tx.get(Stores.purchases, proposalId);
     if (!p) {
       console.error("purchase not found, not adding refunds");
@@ -1215,8 +1201,7 @@ async function startRefundQuery(
   ws: InternalWalletState,
   proposalId: string,
 ): Promise<void> {
-  const success = await runWithWriteTransaction(
-    ws.db,
+  const success = await ws.db.runWithWriteTransaction(
     [Stores.purchases],
     async tx => {
       const p = await tx.get(Stores.purchases, proposalId);
@@ -1259,8 +1244,7 @@ export async function applyRefund(
     throw Error("invalid refund URI");
   }
 
-  const purchase = await oneShotGetIndexed(
-    ws.db,
+  const purchase = await ws.db.getIndexed(
     Stores.purchases.orderIdIndex,
     [parseResult.merchantBaseUrl, parseResult.orderId],
   );
@@ -1292,7 +1276,7 @@ async function resetPurchasePayRetry(
   ws: InternalWalletState,
   proposalId: string,
 ) {
-  await oneShotMutate(ws.db, Stores.purchases, proposalId, x => {
+  await ws.db.mutate(Stores.purchases, proposalId, x => {
     if (x.payRetryInfo.active) {
       x.payRetryInfo = initRetryInfo();
     }
@@ -1308,7 +1292,7 @@ async function processPurchasePayImpl(
   if (forceNow) {
     await resetPurchasePayRetry(ws, proposalId);
   }
-  const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
+  const purchase = await ws.db.get(Stores.purchases, proposalId);
   if (!purchase) {
     return;
   }
@@ -1336,7 +1320,7 @@ async function resetPurchaseQueryRefundRetry(
   ws: InternalWalletState,
   proposalId: string,
 ) {
-  await oneShotMutate(ws.db, Stores.purchases, proposalId, x => {
+  await ws.db.mutate(Stores.purchases, proposalId, x => {
     if (x.refundStatusRetryInfo.active) {
       x.refundStatusRetryInfo = initRetryInfo();
     }
@@ -1352,7 +1336,7 @@ async function processPurchaseQueryRefundImpl(
   if (forceNow) {
     await resetPurchaseQueryRefundRetry(ws, proposalId);
   }
-  const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
+  const purchase = await ws.db.get(Stores.purchases, proposalId);
   if (!purchase) {
     return;
   }
@@ -1398,7 +1382,7 @@ async function resetPurchaseApplyRefundRetry(
   ws: InternalWalletState,
   proposalId: string,
 ) {
-  await oneShotMutate(ws.db, Stores.purchases, proposalId, x => {
+  await ws.db.mutate(Stores.purchases, proposalId, x => {
     if (x.refundApplyRetryInfo.active) {
       x.refundApplyRetryInfo = initRetryInfo();
     }
@@ -1414,7 +1398,7 @@ async function processPurchaseApplyRefundImpl(
   if (forceNow) {
     await resetPurchaseApplyRefundRetry(ws, proposalId);
   }
-  const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
+  const purchase = await ws.db.get(Stores.purchases, proposalId);
   if (!purchase) {
     console.error("not submitting refunds, payment not found:");
     return;
@@ -1448,8 +1432,7 @@ async function processPurchaseApplyRefundImpl(
 
     let allRefundsProcessed = false;
 
-    await runWithWriteTransaction(
-      ws.db,
+    await ws.db.runWithWriteTransaction(
       [Stores.purchases, Stores.coins],
       async tx => {
         const p = await tx.get(Stores.purchases, proposalId);
diff --git a/src/operations/payback.ts b/src/operations/payback.ts
index 2d8a7283..51adb6ad 100644
--- a/src/operations/payback.ts
+++ b/src/operations/payback.ts
@@ -18,10 +18,7 @@
  * Imports.
  */
 import {
-  oneShotIter,
-  runWithWriteTransaction,
-  oneShotGet,
-  oneShotPut,
+  Database
 } from "../util/query";
 import { InternalWalletState } from "./state";
 import { Stores, TipRecord, CoinStatus } from "../types/dbTypes";
@@ -37,7 +34,7 @@ export async function payback(
   ws: InternalWalletState,
   coinPub: string,
 ): Promise<void> {
-  let coin = await oneShotGet(ws.db, Stores.coins, coinPub);
+  let coin = await ws.db.get(Stores.coins, coinPub);
   if (!coin) {
     throw Error(`Coin ${coinPub} not found, can't request payback`);
   }
@@ -45,7 +42,7 @@ export async function payback(
   if (!reservePub) {
     throw Error(`Can't request payback for a refreshed coin`);
   }
-  const reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
+  const reserve = await ws.db.get(Stores.reserves, reservePub);
   if (!reserve) {
     throw Error(`Reserve of coin ${coinPub} not found`);
   }
@@ -58,8 +55,7 @@ export async function payback(
   // technically we might update reserve status before we get the response
   // from the reserve for the payback request.
   reserve.hasPayback = true;
-  await runWithWriteTransaction(
-    ws.db,
+  await ws.db.runWithWriteTransaction(
     [Stores.coins, Stores.reserves],
     async tx => {
       await tx.put(Stores.coins, coin!!);
@@ -80,12 +76,12 @@ export async function payback(
   if (paybackConfirmation.reserve_pub !== coin.reservePub) {
     throw Error(`Coin's reserve doesn't match reserve on payback`);
   }
-  coin = await oneShotGet(ws.db, Stores.coins, coinPub);
+  coin = await ws.db.get(Stores.coins, coinPub);
   if (!coin) {
     throw Error(`Coin ${coinPub} not found, can't confirm payback`);
   }
   coin.status = CoinStatus.Dormant;
-  await oneShotPut(ws.db, Stores.coins, coin);
+  await ws.db.put(Stores.coins, coin);
   ws.notify({
     type: NotificationType.PaybackFinished,
   });
diff --git a/src/operations/pending.ts b/src/operations/pending.ts
index b9fc1d20..13859c64 100644
--- a/src/operations/pending.ts
+++ b/src/operations/pending.ts
@@ -22,7 +22,7 @@ import {
   Timestamp,
   Duration,
 } from "../types/walletTypes";
-import { runWithReadTransaction, TransactionHandle } from "../util/query";
+import { Database, TransactionHandle } from "../util/query";
 import { InternalWalletState } from "./state";
 import {
   Stores,
@@ -425,8 +425,7 @@ export async function getPendingOperations(
     pendingOperations: [],
   };
   const now = getTimestampNow();
-  await runWithReadTransaction(
-    ws.db,
+  await ws.db.runWithReadTransaction(
     [
       Stores.exchanges,
       Stores.reserves,
diff --git a/src/operations/refresh.ts b/src/operations/refresh.ts
index 4e4449d9..4ffc3ea6 100644
--- a/src/operations/refresh.ts
+++ b/src/operations/refresh.ts
@@ -27,21 +27,12 @@ import {
   updateRetryInfoTimeout,
 } from "../types/dbTypes";
 import { amountToPretty } from "../util/helpers";
-import {
-  oneShotGet,
-  oneShotMutate,
-  runWithWriteTransaction,
-  TransactionAbort,
-  oneShotIterIndex,
-} from "../util/query";
+import { Database } from "../util/query";
 import { InternalWalletState } from "./state";
 import { Logger } from "../util/logging";
 import { getWithdrawDenomList } from "./withdraw";
 import { updateExchangeFromUrl } from "./exchanges";
-import {
-  getTimestampNow,
-  OperationError,
-} from "../types/walletTypes";
+import { getTimestampNow, OperationError } from "../types/walletTypes";
 import { guardOperationException } from "./errors";
 import { NotificationType } from "../types/notifications";
 
@@ -84,11 +75,7 @@ async function refreshMelt(
   ws: InternalWalletState,
   refreshSessionId: string,
 ): Promise<void> {
-  const refreshSession = await oneShotGet(
-    ws.db,
-    Stores.refresh,
-    refreshSessionId,
-  );
+  const refreshSession = await ws.db.get(Stores.refresh, refreshSessionId);
   if (!refreshSession) {
     return;
   }
@@ -96,11 +83,7 @@ async function refreshMelt(
     return;
   }
 
-  const coin = await oneShotGet(
-    ws.db,
-    Stores.coins,
-    refreshSession.meltCoinPub,
-  );
+  const coin = await ws.db.get(Stores.coins, refreshSession.meltCoinPub);
 
   if (!coin) {
     console.error("can't melt coin, it does not exist");
@@ -139,7 +122,7 @@ async function refreshMelt(
 
   refreshSession.norevealIndex = norevealIndex;
 
-  await oneShotMutate(ws.db, Stores.refresh, refreshSessionId, rs => {
+  await ws.db.mutate(Stores.refresh, refreshSessionId, rs => {
     if (rs.norevealIndex !== undefined) {
       return;
     }
@@ -159,11 +142,7 @@ async function refreshReveal(
   ws: InternalWalletState,
   refreshSessionId: string,
 ): Promise<void> {
-  const refreshSession = await oneShotGet(
-    ws.db,
-    Stores.refresh,
-    refreshSessionId,
-  );
+  const refreshSession = await ws.db.get(Stores.refresh, refreshSessionId);
   if (!refreshSession) {
     return;
   }
@@ -179,8 +158,7 @@ async function refreshReveal(
     throw Error("refresh index error");
   }
 
-  const meltCoinRecord = await oneShotGet(
-    ws.db,
+  const meltCoinRecord = await ws.db.get(
     Stores.coins,
     refreshSession.meltCoinPub,
   );
@@ -241,7 +219,7 @@ async function refreshReveal(
   const coins: CoinRecord[] = [];
 
   for (let i = 0; i < respJson.ev_sigs.length; i++) {
-    const denom = await oneShotGet(ws.db, Stores.denominations, [
+    const denom = await ws.db.get(Stores.denominations, [
       refreshSession.exchangeBaseUrl,
       refreshSession.newDenoms[i],
     ]);
@@ -274,8 +252,7 @@ async function refreshReveal(
     coins.push(coin);
   }
 
-  await runWithWriteTransaction(
-    ws.db,
+  await ws.db.runWithWriteTransaction(
     [Stores.coins, Stores.refresh],
     async tx => {
       const rs = await tx.get(Stores.refresh, refreshSessionId);
@@ -306,7 +283,7 @@ async function incrementRefreshRetry(
   refreshSessionId: string,
   err: OperationError | undefined,
 ): Promise<void> {
-  await runWithWriteTransaction(ws.db, [Stores.refresh], async tx => {
+  await ws.db.runWithWriteTransaction([Stores.refresh], async tx => {
     const r = await tx.get(Stores.refresh, refreshSessionId);
     if (!r) {
       return;
@@ -341,7 +318,7 @@ async function resetRefreshSessionRetry(
   ws: InternalWalletState,
   refreshSessionId: string,
 ) {
-  await oneShotMutate(ws.db, Stores.refresh, refreshSessionId, (x) => {
+  await ws.db.mutate(Stores.refresh, refreshSessionId, x => {
     if (x.retryInfo.active) {
       x.retryInfo = initRetryInfo();
     }
@@ -357,11 +334,7 @@ async function processRefreshSessionImpl(
   if (forceNow) {
     await resetRefreshSessionRetry(ws, refreshSessionId);
   }
-  const refreshSession = await oneShotGet(
-    ws.db,
-    Stores.refresh,
-    refreshSessionId,
-  );
+  const refreshSession = await ws.db.get(Stores.refresh, refreshSessionId);
   if (!refreshSession) {
     return;
   }
@@ -380,7 +353,7 @@ export async function refresh(
   oldCoinPub: string,
   force: boolean = false,
 ): Promise<void> {
-  const coin = await oneShotGet(ws.db, Stores.coins, oldCoinPub);
+  const coin = await ws.db.get(Stores.coins, oldCoinPub);
   if (!coin) {
     console.warn("can't refresh, coin not in database");
     return;
@@ -402,7 +375,7 @@ export async function refresh(
     throw Error("db inconsistent: exchange of coin not found");
   }
 
-  const oldDenom = await oneShotGet(ws.db, Stores.denominations, [
+  const oldDenom = await ws.db.get(Stores.denominations, [
     exchange.baseUrl,
     coin.denomPub,
   ]);
@@ -411,11 +384,9 @@ export async function refresh(
     throw Error("db inconsistent: denomination for coin not found");
   }
 
-  const availableDenoms: DenominationRecord[] = await oneShotIterIndex(
-    ws.db,
-    Stores.denominations.exchangeBaseUrlIndex,
-    exchange.baseUrl,
-  ).toArray();
+  const availableDenoms: DenominationRecord[] = await ws.db
+    .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl)
+    .toArray();
 
   const availableAmount = Amounts.sub(coin.currentAmount, oldDenom.feeRefresh)
     .amount;
@@ -428,7 +399,7 @@ export async function refresh(
         availableAmount,
       )} too small`,
     );
-    await oneShotMutate(ws.db, Stores.coins, oldCoinPub, x => {
+    await ws.db.mutate(Stores.coins, oldCoinPub, x => {
       if (x.status != coin.status) {
         // Concurrent modification?
         return;
@@ -450,8 +421,7 @@ export async function refresh(
 
   // Store refresh session and subtract refreshed amount from
   // coin in the same transaction.
-  await runWithWriteTransaction(
-    ws.db,
+  await ws.db.runWithWriteTransaction(
     [Stores.refresh, Stores.coins],
     async tx => {
       const c = await tx.get(Stores.coins, coin.coinPub);
diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts
index 5ad13a67..215d5ba7 100644
--- a/src/operations/reserves.ts
+++ b/src/operations/reserves.ts
@@ -33,10 +33,7 @@ import {
   updateRetryInfoTimeout,
 } from "../types/dbTypes";
 import {
-  oneShotMutate,
-  oneShotPut,
-  oneShotGet,
-  runWithWriteTransaction,
+  Database,
   TransactionAbort,
 } from "../util/query";
 import { Logger } from "../util/logging";
@@ -104,7 +101,7 @@ export async function createReserve(
     const rec = {
       paytoUri: senderWire,
     };
-    await oneShotPut(ws.db, Stores.senderWires, rec);
+    await ws.db.put(Stores.senderWires, rec);
   }
 
   const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange);
@@ -114,8 +111,7 @@ export async function createReserve(
     throw Error("exchange not updated");
   }
   const { isAudited, isTrusted } = await getExchangeTrust(ws, exchangeInfo);
-  let currencyRecord = await oneShotGet(
-    ws.db,
+  let currencyRecord = await ws.db.get(
     Stores.currencies,
     exchangeDetails.currency,
   );
@@ -137,8 +133,7 @@ export async function createReserve(
 
   const cr: CurrencyRecord = currencyRecord;
 
-  const resp = await runWithWriteTransaction(
-    ws.db,
+  const resp = await ws.db.runWithWriteTransaction(
     [Stores.currencies, Stores.reserves, Stores.bankWithdrawUris],
     async tx => {
       // Check if we have already created a reserve for that 
bankWithdrawStatusUrl
@@ -212,7 +207,7 @@ async function registerReserveWithBank(
   ws: InternalWalletState,
   reservePub: string,
 ): Promise<void> {
-  let reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
+  let reserve = await ws.db.get(Stores.reserves, reservePub);
   switch (reserve?.reserveStatus) {
     case ReserveRecordStatus.WAIT_CONFIRM_BANK:
     case ReserveRecordStatus.REGISTERING_BANK:
@@ -233,7 +228,7 @@ async function registerReserveWithBank(
     selected_exchange: reserve.exchangeWire,
   });
   console.log("got response", bankResp);
-  await oneShotMutate(ws.db, Stores.reserves, reservePub, r => {
+  await ws.db.mutate(Stores.reserves, reservePub, r => {
     switch (r.reserveStatus) {
       case ReserveRecordStatus.REGISTERING_BANK:
       case ReserveRecordStatus.WAIT_CONFIRM_BANK:
@@ -266,7 +261,7 @@ async function processReserveBankStatusImpl(
   ws: InternalWalletState,
   reservePub: string,
 ): Promise<void> {
-  let reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
+  let reserve = await ws.db.get(Stores.reserves, reservePub);
   switch (reserve?.reserveStatus) {
     case ReserveRecordStatus.WAIT_CONFIRM_BANK:
     case ReserveRecordStatus.REGISTERING_BANK:
@@ -303,7 +298,7 @@ async function processReserveBankStatusImpl(
   }
 
   if (status.transfer_done) {
-    await oneShotMutate(ws.db, Stores.reserves, reservePub, r => {
+    await ws.db.mutate(Stores.reserves, reservePub, r => {
       switch (r.reserveStatus) {
         case ReserveRecordStatus.REGISTERING_BANK:
         case ReserveRecordStatus.WAIT_CONFIRM_BANK:
@@ -319,7 +314,7 @@ async function processReserveBankStatusImpl(
     });
     await processReserveImpl(ws, reservePub, true);
   } else {
-    await oneShotMutate(ws.db, Stores.reserves, reservePub, r => {
+    await ws.db.mutate(Stores.reserves, reservePub, r => {
       switch (r.reserveStatus) {
         case ReserveRecordStatus.WAIT_CONFIRM_BANK:
           break;
@@ -339,7 +334,7 @@ async function incrementReserveRetry(
   reservePub: string,
   err: OperationError | undefined,
 ): Promise<void> {
-  await runWithWriteTransaction(ws.db, [Stores.reserves], async tx => {
+  await ws.db.runWithWriteTransaction([Stores.reserves], async tx => {
     const r = await tx.get(Stores.reserves, reservePub);
     if (!r) {
       return;
@@ -363,7 +358,7 @@ async function updateReserve(
   ws: InternalWalletState,
   reservePub: string,
 ): Promise<void> {
-  const reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
+  const reserve = await ws.db.get(Stores.reserves, reservePub);
   if (!reserve) {
     throw Error("reserve not in db");
   }
@@ -400,7 +395,7 @@ async function updateReserve(
   }
   const reserveInfo = ReserveStatus.checked(await resp.json());
   const balance = Amounts.parseOrThrow(reserveInfo.balance);
-  await oneShotMutate(ws.db, Stores.reserves, reserve.reservePub, r => {
+  await ws.db.mutate(Stores.reserves, reserve.reservePub, r => {
     if (r.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
       return;
     }
@@ -442,7 +437,7 @@ async function processReserveImpl(
   reservePub: string,
   forceNow: boolean = false,
 ): Promise<void> {
-  const reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
+  const reserve = await ws.db.get(Stores.reserves, reservePub);
   if (!reserve) {
     console.log("not processing reserve: reserve does not exist");
     return;
@@ -488,7 +483,7 @@ export async function confirmReserve(
   req: ConfirmReserveRequest,
 ): Promise<void> {
   const now = getTimestampNow();
-  await oneShotMutate(ws.db, Stores.reserves, req.reservePub, reserve => {
+  await ws.db.mutate(Stores.reserves, req.reservePub, reserve => {
     if (reserve.reserveStatus !== ReserveRecordStatus.UNCONFIRMED) {
       return;
     }
@@ -515,7 +510,7 @@ async function depleteReserve(
   ws: InternalWalletState,
   reservePub: string,
 ): Promise<void> {
-  const reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
+  const reserve = await ws.db.get(Stores.reserves, reservePub);
   if (!reserve) {
     return;
   }
@@ -600,8 +595,7 @@ async function depleteReserve(
     return r;
   }
 
-  const success = await runWithWriteTransaction(
-    ws.db,
+  const success = await ws.db.runWithWriteTransaction(
     [Stores.withdrawalSession, Stores.reserves],
     async tx => {
       const myReserve = await tx.get(Stores.reserves, reservePub);
diff --git a/src/operations/return.ts b/src/operations/return.ts
index 74885a73..01d2802d 100644
--- a/src/operations/return.ts
+++ b/src/operations/return.ts
@@ -21,7 +21,7 @@ import {
   ReturnCoinsRequest,
   CoinWithDenom,
 } from "../types/walletTypes";
-import { runWithWriteTransaction, oneShotGet, oneShotIterIndex, oneShotPut } 
from "../util/query";
+import { Database } from "../util/query";
 import { InternalWalletState } from "./state";
 import { Stores, TipRecord, CoinStatus, CoinsReturnRecord, CoinRecord } from 
"../types/dbTypes";
 import * as Amounts from "../util/amounts";
@@ -38,8 +38,7 @@ async function getCoinsForReturn(
   exchangeBaseUrl: string,
   amount: AmountJson,
 ): Promise<CoinWithDenom[] | undefined> {
-  const exchange = await oneShotGet(
-    ws.db,
+  const exchange = await ws.db.get(
     Stores.exchanges,
     exchangeBaseUrl,
   );
@@ -47,8 +46,7 @@ async function getCoinsForReturn(
     throw Error(`Exchange ${exchangeBaseUrl} not known to the wallet`);
   }
 
-  const coins: CoinRecord[] = await oneShotIterIndex(
-    ws.db,
+  const coins: CoinRecord[] = await ws.db.iterIndex(
     Stores.coins.exchangeBaseUrlIndex,
     exchange.baseUrl,
   ).toArray();
@@ -57,15 +55,14 @@ async function getCoinsForReturn(
     return [];
   }
 
-  const denoms = await oneShotIterIndex(
-    ws.db,
+  const denoms = await ws.db.iterIndex(
     Stores.denominations.exchangeBaseUrlIndex,
     exchange.baseUrl,
   ).toArray();
 
   // Denomination of the first coin, we assume that all other
   // coins have the same currency
-  const firstDenom = await oneShotGet(ws.db, Stores.denominations, [
+  const firstDenom = await ws.db.get(Stores.denominations, [
     exchange.baseUrl,
     coins[0].denomPub,
   ]);
@@ -76,7 +73,7 @@ async function getCoinsForReturn(
 
   const cds: CoinWithDenom[] = [];
   for (const coin of coins) {
-    const denom = await oneShotGet(ws.db, Stores.denominations, [
+    const denom = await ws.db.get(Stores.denominations, [
       exchange.baseUrl,
       coin.denomPub,
     ]);
@@ -121,7 +118,7 @@ export async function returnCoins(
     return;
   }
   const stampSecNow = Math.floor(new Date().getTime() / 1000);
-  const exchange = await oneShotGet(ws.db, Stores.exchanges, req.exchange);
+  const exchange = await ws.db.get(Stores.exchanges, req.exchange);
   if (!exchange) {
     console.error(`Exchange ${req.exchange} not known to the wallet`);
     return;
@@ -190,8 +187,7 @@ export async function returnCoins(
     wire: req.senderWire,
   };
 
-  await runWithWriteTransaction(
-    ws.db,
+  await ws.db.runWithWriteTransaction(
     [Stores.coinsReturns, Stores.coins],
     async tx => {
       await tx.put(Stores.coinsReturns, coinsReturnRecord);
@@ -248,8 +244,7 @@ async function depositReturnedCoins(
     // FIXME: verify signature
 
     // For every successful deposit, we replace the old record with an updated 
one
-    const currentCrr = await oneShotGet(
-      ws.db,
+    const currentCrr = await ws.db.get(
       Stores.coinsReturns,
       coinsReturnRecord.contractTermsHash,
     );
@@ -262,6 +257,6 @@ async function depositReturnedCoins(
         nc.depositedSig = respJson.sig;
       }
     }
-    await oneShotPut(ws.db, Stores.coinsReturns, currentCrr);
+    await ws.db.put(Stores.coinsReturns, currentCrr);
   }
 }
diff --git a/src/operations/state.ts b/src/operations/state.ts
index 47bf40de..1e4b9036 100644
--- a/src/operations/state.ts
+++ b/src/operations/state.ts
@@ -25,6 +25,7 @@ import { AsyncOpMemoMap, AsyncOpMemoSingle } from 
"../util/asyncMemo";
 import { Logger } from "../util/logging";
 import { PendingOperationsResponse } from "../types/pending";
 import { WalletNotification } from "../types/notifications";
+import { Database } from "../util/query";
 
 type NotificationListener = (n: WalletNotification) => void;
 
@@ -45,7 +46,7 @@ export class InternalWalletState {
   listeners: NotificationListener[] = [];
 
   constructor(
-    public db: IDBDatabase,
+    public db: Database,
     public http: HttpRequestLibrary,
     cryptoWorkerFactory: CryptoWorkerFactory,
   ) {
diff --git a/src/operations/tip.ts b/src/operations/tip.ts
index 0a710f67..f723374f 100644
--- a/src/operations/tip.ts
+++ b/src/operations/tip.ts
@@ -15,7 +15,7 @@
  */
 
  
-import { oneShotGet, oneShotPut, oneShotMutate, runWithWriteTransaction } from 
"../util/query";
+import { Database } from "../util/query";
 import { InternalWalletState } from "./state";
 import { parseTipUri } from "../util/taleruri";
 import { TipStatus, getTimestampNow, OperationError } from 
"../types/walletTypes";
@@ -53,7 +53,7 @@ export async function getTipStatus(
 
   let amount = Amounts.parseOrThrow(tipPickupStatus.amount);
 
-  let tipRecord = await oneShotGet(ws.db, Stores.tips, [
+  let tipRecord = await ws.db.get(Stores.tips, [
     res.merchantTipId,
     res.merchantOrigin,
   ]);
@@ -87,7 +87,7 @@ export async function getTipStatus(
       retryInfo: initRetryInfo(),
       lastError: undefined,
     };
-    await oneShotPut(ws.db, Stores.tips, tipRecord);
+    await ws.db.put(Stores.tips, tipRecord);
   }
 
   const tipStatus: TipStatus = {
@@ -112,7 +112,7 @@ async function incrementTipRetry(
   refreshSessionId: string,
   err: OperationError | undefined,
 ): Promise<void> {
-  await runWithWriteTransaction(ws.db, [Stores.tips], async tx => {
+  await ws.db.runWithWriteTransaction([Stores.tips], async tx => {
     const t = await tx.get(Stores.tips, refreshSessionId);
     if (!t) {
       return;
@@ -141,7 +141,7 @@ async function resetTipRetry(
   ws: InternalWalletState,
   tipId: string,
 ): Promise<void> {
-  await oneShotMutate(ws.db, Stores.tips, tipId, (x) => {
+  await ws.db.mutate(Stores.tips, tipId, (x) => {
     if (x.retryInfo.active) {
       x.retryInfo = initRetryInfo();
     }
@@ -157,7 +157,7 @@ async function processTipImpl(
   if (forceNow) {
     await resetTipRetry(ws, tipId);
   }
-  let tipRecord = await oneShotGet(ws.db, Stores.tips, tipId);
+  let tipRecord = await ws.db.get(Stores.tips, tipId);
   if (!tipRecord) {
     return;
   }
@@ -179,7 +179,7 @@ async function processTipImpl(
       denomsForWithdraw.map(d => ws.cryptoApi.createTipPlanchet(d)),
     );
 
-    await oneShotMutate(ws.db, Stores.tips, tipId, r => {
+    await ws.db.mutate(Stores.tips, tipId, r => {
       if (!r.planchets) {
         r.planchets = planchets;
       }
@@ -187,7 +187,7 @@ async function processTipImpl(
     });
   }
 
-  tipRecord = await oneShotGet(ws.db, Stores.tips, tipId);
+  tipRecord = await ws.db.get(Stores.tips, tipId);
   if (!tipRecord) {
     throw Error("tip not in database");
   }
@@ -267,7 +267,7 @@ async function processTipImpl(
   };
 
 
-  await runWithWriteTransaction(ws.db, [Stores.tips, 
Stores.withdrawalSession], async (tx) => {
+  await ws.db.runWithWriteTransaction([Stores.tips, Stores.withdrawalSession], 
async (tx) => {
     const tr = await tx.get(Stores.tips, tipId);
     if (!tr) {
       return;
@@ -291,14 +291,14 @@ export async function acceptTip(
   ws: InternalWalletState,
   tipId: string,
 ): Promise<void> {
-  const tipRecord = await oneShotGet(ws.db, Stores.tips, tipId);
+  const tipRecord = await ws.db.get(Stores.tips, tipId);
   if (!tipRecord) {
     console.log("tip not found");
     return;
   }
 
   tipRecord.accepted = true;
-  await oneShotPut(ws.db, Stores.tips, tipRecord);
+  await ws.db.put(Stores.tips, tipRecord);
 
   await processTip(ws, tipId);
   return;
diff --git a/src/operations/withdraw.ts b/src/operations/withdraw.ts
index 4ecc321f..a34eec5a 100644
--- a/src/operations/withdraw.ts
+++ b/src/operations/withdraw.ts
@@ -39,12 +39,7 @@ import { InternalWalletState } from "./state";
 import { parseWithdrawUri } from "../util/taleruri";
 import { Logger } from "../util/logging";
 import {
-  oneShotGet,
-  oneShotPut,
-  oneShotIterIndex,
-  oneShotGetIndexed,
-  runWithWriteTransaction,
-  oneShotMutate,
+  Database
 } from "../util/query";
 import {
   updateExchangeFromUrl,
@@ -167,8 +162,7 @@ async function getPossibleDenoms(
   ws: InternalWalletState,
   exchangeBaseUrl: string,
 ): Promise<DenominationRecord[]> {
-  return await oneShotIterIndex(
-    ws.db,
+  return await ws.db.iterIndex(
     Stores.denominations.exchangeBaseUrlIndex,
     exchangeBaseUrl,
   ).filter(d => {
@@ -187,8 +181,7 @@ async function processPlanchet(
   withdrawalSessionId: string,
   coinIdx: number,
 ): Promise<void> {
-  const withdrawalSession = await oneShotGet(
-    ws.db,
+  const withdrawalSession = await ws.db.get(
     Stores.withdrawalSession,
     withdrawalSessionId,
   );
@@ -205,8 +198,7 @@ async function processPlanchet(
     console.log("processPlanchet: planchet not found");
     return;
   }
-  const exchange = await oneShotGet(
-    ws.db,
+  const exchange = await ws.db.get(
     Stores.exchanges,
     withdrawalSession.exchangeBaseUrl,
   );
@@ -215,7 +207,7 @@ async function processPlanchet(
     return;
   }
 
-  const denom = await oneShotGet(ws.db, Stores.denominations, [
+  const denom = await ws.db.get(Stores.denominations, [
     withdrawalSession.exchangeBaseUrl,
     planchet.denomPub,
   ]);
@@ -268,8 +260,7 @@ async function processPlanchet(
   let withdrawSessionFinished = false;
   let reserveDepleted = false;
 
-  const success = await runWithWriteTransaction(
-    ws.db,
+  const success = await ws.db.runWithWriteTransaction(
     [Stores.coins, Stores.withdrawalSession, Stores.reserves],
     async tx => {
       const ws = await tx.get(Stores.withdrawalSession, withdrawalSessionId);
@@ -346,7 +337,7 @@ export async function getVerifiedWithdrawDenomList(
   exchangeBaseUrl: string,
   amount: AmountJson,
 ): Promise<DenominationRecord[]> {
-  const exchange = await oneShotGet(ws.db, Stores.exchanges, exchangeBaseUrl);
+  const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
   if (!exchange) {
     console.log("exchange not found");
     throw Error(`exchange ${exchangeBaseUrl} not found`);
@@ -391,7 +382,7 @@ export async function getVerifiedWithdrawDenomList(
           denom.status = DenominationStatus.VerifiedGood;
           nextPossibleDenoms.push(denom);
         }
-        await oneShotPut(ws.db, Stores.denominations, denom);
+        await ws.db.put(Stores.denominations, denom);
       } else {
         nextPossibleDenoms.push(denom);
       }
@@ -408,8 +399,7 @@ async function makePlanchet(
   withdrawalSessionId: string,
   coinIndex: number,
 ): Promise<void> {
-  const withdrawalSession = await oneShotGet(
-    ws.db,
+  const withdrawalSession = await ws.db.get(
     Stores.withdrawalSession,
     withdrawalSessionId,
   );
@@ -420,11 +410,11 @@ async function makePlanchet(
   if (src.type !== "reserve") {
     throw Error("invalid state");
   }
-  const reserve = await oneShotGet(ws.db, Stores.reserves, src.reservePub);
+  const reserve = await ws.db.get(Stores.reserves, src.reservePub);
   if (!reserve) {
     return;
   }
-  const denom = await oneShotGet(ws.db, Stores.denominations, [
+  const denom = await ws.db.get(Stores.denominations, [
     withdrawalSession.exchangeBaseUrl,
     withdrawalSession.denoms[coinIndex],
   ]);
@@ -450,7 +440,7 @@ async function makePlanchet(
     reservePub: r.reservePub,
     withdrawSig: r.withdrawSig,
   };
-  await runWithWriteTransaction(ws.db, [Stores.withdrawalSession], async tx => 
{
+  await ws.db.runWithWriteTransaction([Stores.withdrawalSession], async tx => {
     const myWs = await tx.get(Stores.withdrawalSession, withdrawalSessionId);
     if (!myWs) {
       return;
@@ -469,8 +459,7 @@ async function processWithdrawCoin(
   coinIndex: number,
 ) {
   logger.trace("starting withdraw for coin", coinIndex);
-  const withdrawalSession = await oneShotGet(
-    ws.db,
+  const withdrawalSession = await ws.db.get(
     Stores.withdrawalSession,
     withdrawalSessionId,
   );
@@ -479,8 +468,7 @@ async function processWithdrawCoin(
     return;
   }
 
-  const coin = await oneShotGetIndexed(
-    ws.db,
+  const coin = await ws.db.getIndexed(
     Stores.coins.byWithdrawalWithIdx,
     [withdrawalSessionId, coinIndex],
   );
@@ -505,7 +493,7 @@ async function incrementWithdrawalRetry(
   withdrawalSessionId: string,
   err: OperationError | undefined,
 ): Promise<void> {
-  await runWithWriteTransaction(ws.db, [Stores.withdrawalSession], async tx => 
{
+  await ws.db.runWithWriteTransaction([Stores.withdrawalSession], async tx => {
     const wsr = await tx.get(Stores.withdrawalSession, withdrawalSessionId);
     if (!wsr) {
       return;
@@ -538,7 +526,7 @@ async function resetWithdrawSessionRetry(
   ws: InternalWalletState,
   withdrawalSessionId: string,
 ) {
-  await oneShotMutate(ws.db, Stores.withdrawalSession, withdrawalSessionId, 
(x) => {
+  await ws.db.mutate(Stores.withdrawalSession, withdrawalSessionId, (x) => {
     if (x.retryInfo.active) {
       x.retryInfo = initRetryInfo();
     }
@@ -555,8 +543,7 @@ async function processWithdrawSessionImpl(
   if (forceNow) {
     await resetWithdrawSessionRetry(ws, withdrawalSessionId);
   }
-  const withdrawalSession = await oneShotGet(
-    ws.db,
+  const withdrawalSession = await ws.db.get(
     Stores.withdrawalSession,
     withdrawalSessionId,
   );
@@ -615,15 +602,13 @@ export async function getExchangeWithdrawalInfo(
     }
   }
 
-  const possibleDenoms = await oneShotIterIndex(
-    ws.db,
+  const possibleDenoms = await ws.db.iterIndex(
     Stores.denominations.exchangeBaseUrlIndex,
     baseUrl,
   ).filter(d => d.isOffered);
 
   const trustedAuditorPubs = [];
-  const currencyRecord = await oneShotGet(
-    ws.db,
+  const currencyRecord = await ws.db.get(
     Stores.currencies,
     amount.currency,
   );
diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts
index ce2bb410..6a00a497 100644
--- a/src/types/dbTypes.ts
+++ b/src/types/dbTypes.ts
@@ -43,14 +43,6 @@ import {
   getTimestampNow,
 } from "./walletTypes";
 
-/**
- * Current database version, should be incremented
- * each time we do incompatible schema changes on the database.
- * In the future we might consider adding migration functions for
- * each version increment.
- */
-export const WALLET_DB_VERSION = 28;
-
 export enum ReserveRecordStatus {
   /**
    * Waiting for manual confirmation.
diff --git a/src/types/history.ts b/src/types/history.ts
index e925b0ff..0adc27d5 100644
--- a/src/types/history.ts
+++ b/src/types/history.ts
@@ -21,38 +21,697 @@ import { Timestamp } from "./walletTypes";
  */
 
 /**
- * Activity history record.
+ * Type tags for the history event types.
  */
-export interface HistoryEvent {
+export const enum HistoryEventType {
+  AuditorComplaintSent = "auditor-complained-sent",
+  AuditorComplaintProcessed = "auditor-complaint-processed",
+  AuditorTrustAdded = "auditor-trust-added",
+  AuditorTrustRemoved = "auditor-trust-removed",
+  ExchangeAdded = "exchange-added",
+  ExchangeTermsAccepted = "exchange-terms-accepted",
+  ExchangePolicyChanged = "exchange-policy-changed",
+  ExchangeTrustAdded = "exchange-trust-added",
+  ExchangeTrustRemoved = "exchange-trust-removed",
+  ExchangeUpdated = "exchange-updated",
+  FundsDepositedToSelf = "funds-deposited-to-self",
+  FundsRecouped = "funds-recouped",
+  OrderAccepted = "order-accepted",
+  OrderRedirected = "order-redirected",
+  OrderRefused = "order-refused",
+  PaymentAborted = "payment-aborted",
+  PaymentSent = "payment-sent",
+  Refreshed = "refreshed",
+  Refund = "refund",
+  ReserveBalanceUpdated = "reserve-balance-updated",
+  ReserveWithdrawAllocated = "reserve-withdraw-allocated",
+  ReserveConfirmed = "reserve-confirmed",
+  ReserveCreated = "reserve-created",
+  TipAccepted = "tip-accepted",
+  TipDeclined = "tip-declined",
+  Withdrawn = "withdrawn",
+}
+
+export const enum ReserveType {
+  /**
+   * Manually created.
+   */
+  Manual = "manual",
+  /**
+   * Withdrawn from a bank that has "tight" Taler integration
+   */
+  TalerBankWithdraw = "taler-bank-withdraw",
+}
+
+/**
+ * Short info about a reserve.  Enough to display in a list view and
+ * to query more information from the wallet.
+ */
+export interface ReserveShortInfo {
+  /**
+   * The exchange that the reserve will be at.
+   */
+  exchangeBaseUrl: string;
+
+  /**
+   * Key to query more details
+   */
+  reservePub: string;
+
+  /**
+   * Detail about how the reserve has been created.
+   */
+  reserveCreationDetail: ReserveCreationDetail;
+}
+
+export type ReserveCreationDetail =
+  | { type: ReserveType.Manual }
+  | { type: ReserveType.TalerBankWithdraw; bankUrl: string };
+
+export interface HistoryReserveCreatedEvent {
+  type: HistoryEventType.ReserveCreated;
+
   /**
-   * Type of the history event.
+   * Amount that the should appear in the reserve once its status
+   * is requested from the exchange.
    */
-  type: string;
+  expectedAmount: string;
 
   /**
-   * Time when the activity was recorded.
+   * Condensed information about the reserve.
+   */
+  reserveShortInfo: ReserveShortInfo;
+}
+
+/**
+ * The user (or some application / test case) or the bank has confirmed that 
the
+ * reserve will indeed become available after a while, because the
+ * funds are in the process of being transfered to the exchange.
+ */
+export interface HistoryReserveConfirmeEvent {
+  type: HistoryEventType.ReserveConfirmed;
+
+  /**
+   * Point in time when the reserve was confirmed.
    */
   timestamp: Timestamp;
 
   /**
-   * Details used when rendering the history record.
+   * Amount that the should appear in the reserve once its status
+   * is requested from the exchange.
    */
-  detail: any;
+  expectedAmount: string;
 
   /**
-   * Set to 'true' if the event has been explicitly created,
-   * and set to 'false' if the event has been derived from the
-   * state of the database.
+   * Condensed information about the reserve.
    */
-  explicit: boolean;
+  reserveShortInfo: ReserveShortInfo;
 }
 
+/**
+ * This event is emitted every time we ask the exchange for the status
+ * of the reserve, and the status has changed.
+ */
+export interface HistoryReserveBalanceUpdatedEvent {
+  type: HistoryEventType.ReserveBalanceUpdated;
+
+  /**
+   * Point in time when the reserve was confirmed.
+   */
+  timestamp: Timestamp;
+
+  /**
+   * Unique identifier to query more information about this update.
+   */
+  reserveUpdateId: string;
+
+  /**
+   * Condensed information about the reserve.
+   */
+  reserveShortInfo: ReserveShortInfo;
+
+  /**
+   * Amount currently left in the reserve.
+   */
+  amountReserveBalance: string;
+
+  /**
+   * Amount we expected to be in the reserve at that time,
+   * considering ongoing withdrawals from that reserve.
+   */
+  amountExpected: string;
+}
+
+/**
+ * This event is emitted every time we ask the exchange for the status
+ * of the reserve, and the status has changed.
+ */
+export interface HistoryReserveWithdrawAllocatedEvent {
+  type: HistoryEventType.ReserveWithdrawAllocated;
+
+  /**
+   * Point in time when the reserve was confirmed.
+   */
+  timestamp: Timestamp;
+
+  /**
+   * Unique identifier to query more information about the withdrawal.
+   */
+  withdrawalSessionId: string;
+
+  /**
+   * Condensed information about the reserve.
+   */
+  reserveShortInfo: ReserveShortInfo;
+
+  /**
+   * Amount that has been allocated for withdrawal from
+   * this reserve.
+   */
+  amountWithdrawAllocated: string;
+}
+
+/**
+ * History event to indicate that the user has accepted a tip.
+ */
+export interface HistoryTipAcceptedEvent {
+  type: HistoryEventType.TipAccepted;
+
+  /**
+   * Point in time when the tip has been accepted.
+   */
+  timestamp: Timestamp;
+
+  /**
+   * Unique identifier for the tip to query more information.
+   */
+  tipId: string;
+
+  /**
+   * Raw amount of the tip, without extra fees that apply.
+   */
+  tipRawAmount: string;
+
+  /**
+   * Amount that the user effectively adds to their balance when
+   * the tip is accepted.
+   */
+  tipEffectiveAmount: string;
+}
+
+/**
+ * History event to indicate that the user has accepted a tip.
+ */
+export interface HistoryTipDeclinedEvent {
+  type: HistoryEventType.TipAccepted;
+
+  /**
+   * Point in time when the tip has been declined.
+   */
+  timestamp: Timestamp;
+
+  /**
+   * Unique identifier for the tip to query more information.
+   */
+  tipId: string;
+
+  /**
+   * Raw amount of the tip, without extra fees that apply.
+   */
+  tipRawAmount: string;
+
+  /**
+   * Amount that the user effectively adds to their balance when
+   * the tip is accepted.
+   */
+  tipEffectiveAmount: string;
+}
+
+/**
+ * The wallet has send a complaint (typically with cryptographic evidence of
+ * something having gone wrong) to the auditor.
+ */
+export interface HistoryAuditorComplaintSentEvent {
+  type: HistoryEventType.AuditorComplaintSent;
+
+  auditorComplaintId: string;
+
+  /* FIXME: add fields once this feature is implemented */
+}
+
+/**
+ * The wallet has received a response from the auditor to a complaint.
+ */
+export interface HistoryAuditorComplaintProcessedEvent {
+  type: HistoryEventType.AuditorComplaintProcessed;
+
+  auditorComplaintId: string;
+
+  /* FIXME: add fields once this feature is implemented */
+}
+
+/**
+ * The wallet has added an auditor as a trusted auditor.
+ */
+export interface HistoryAuditorTrustAddedEvent {
+  type: HistoryEventType.AuditorTrustAdded;
+
+  /**
+   * Base URL of the auditor.
+   */
+  auditorBaseUrl: string;
+
+  /**
+   * If set to true, this auditor hasn't been added by the user,
+   * but is part of the pre-set trusted auditors in the wallet.
+   */
+  builtIn: boolean;
+}
+
+/**
+ * The wallet has added an auditor as a trusted auditor.
+ */
+export interface HistoryAuditorTrustRemovedEvent {
+  type: HistoryEventType.AuditorTrustRemoved;
+
+  /**
+   * Base URL of the auditor.
+   */
+  auditorBaseUrl: string;
+}
+
+/**
+ * An exchange has been added to the wallet.  The wallet only
+ * downloads information about the exchange, but does not necessarily
+ * trust it yet.
+ */
+export interface HistoryExchangeAddedEvent {
+  type: HistoryEventType.ExchangeAdded;
+
+  exchangeBaseUrl: string;
+
+  /**
+   * Set to true if the exchange was pre-configured
+   * by the wallet.
+   */
+  builtIn: boolean;
+}
+
+/**
+ * History event to indicate that the wallet now trusts
+ * an exchange.
+ */
+export interface HistoryExchangeTrustAddedEvent {
+  type: HistoryEventType.ExchangeTrustAdded;
+
+  exchangeBaseUrl: string;
+
+  /**
+   * Set to true if the exchange was pre-configured
+   * by the wallet.
+   */
+  builtIn: boolean;
+}
+
+/**
+ * History event to indicate that the wallet doesn't trust
+ * an exchange anymore.
+ */
+export interface HistoryExchangeTrustRemovedEvent {
+  type: HistoryEventType.ExchangeTrustRemoved;
+
+  exchangeBaseUrl: string;
+}
+
+/**
+ * This event indicates that the user accepted the terms of service
+ * of the exchange.
+ */
+export interface HistoryExchangeTermsAcceptedEvent {
+  type: HistoryEventType.ExchangeTermsAccepted;
+
+  exchangeBaseUrl: string;
+
+  /**
+   * Etag that the exchange sent with the terms of service.
+   * Identifies the version of the terms of service.
+   */
+  termsEtag: string | undefined;
+}
+
+/**
+ * The exchange has changed the terms of service or privacy
+ * policy.
+ */
+export interface HistoryExchangePolicyChangedEvent {
+  type: HistoryEventType.ExchangePolicyChanged;
+
+  exchangeBaseUrl: string;
+
+  /**
+   * Etag that the exchange sent with the terms of service.
+   * Identifies the version of the terms of service.
+   */
+  termsEtag: string | undefined;
+
+  /**
+   * Etag that the exchange sent with the privacy policy.
+   * Identifies the version of the privacy policy.
+   */
+  privacyPolicyEtag: string | undefined;
+}
+
+/**
+ * This history event indicates that the exchange has updated
+ * the /keys or /wire information.  The event is only emitted if
+ * this information changed since the last time the wallet checked it.
+ */
+export interface HistoryExchangeUpdatedEvent {
+  type: HistoryEventType.ExchangeUpdated;
+
+  exchangeBaseUrl: string;
+}
+
+/**
+ * History event to indicate that the user sent back digital cash from
+ * their wallet back to their own bank account (basically acting as a 
merchant).
+ */
+export interface HistoryFundsDepositedToSelfEvent {
+  type: HistoryEventType.FundsDepositedToSelf;
+
+  /**
+   * Amount that got sent back.
+   */
+  amount: string;
+
+  /**
+   * Account that received the funds.
+   */
+  receiverPaytoUri: string;
+}
+
+/**
+ * History event to indicate that the exchange marked
+ * some denominations as "compromised", and the wallet has
+ * converted funds in these denominations to new funds.
+ */
+export interface HistoryFundsRecoupedEvent {
+  type: HistoryEventType.FundsDepositedToSelf;
+
+  exchangeBaseUrl: string;
+
+  /**
+   * Amount that the wallet managed to recover.
+   */
+  amountRecouped: string;
+
+  /**
+   * Amount that was lost due to fees.
+   */
+  amountLost: string;
+}
+
+/**
+ * Condensed information about an order, enough to display in a list view
+ * and to query more details from the wallet for a detail view.
+ */
+export interface OrderShortInfo {
+  /**
+   * Wallet-internal identifier of the proposal.
+   */
+  proposalId: string;
+
+  /**
+   * Order ID, uniquely identifies the order within a merchant instance.
+   */
+  orderId: string;
+
+  /**
+   * Base URL of the merchant.
+   */
+  merchantBaseUrl: string;
+
+  /**
+   * Amount that must be paid for the contract.
+   */
+  amountRequested: string;
+
+  /**
+   * Amount that would be subtracted from the wallet when paying,
+   * includes fees and funds lost due to refreshing or left-over
+   * amounts too small to refresh.
+   */
+  amountEffective: string;
+
+  /**
+   * Summary of the proposal, given by the merchant.
+   */
+  summary: string;
+}
+
+/**
+ * The user has accepted purchasing something.
+ */
+export interface HistoryOrderAcceptedEvent {
+  /**
+   * Type tag.
+   */
+  type: HistoryEventType.OrderAccepted;
+
+  /**
+   * Condensed info about the order.
+   */
+  orderShortInfo: OrderShortInfo;
+}
+
+/**
+ * The customer refused to pay.
+ */
+export interface HistoryOrderRefusedEvent {
+  /**
+   * Type tag.
+   */
+  type: HistoryEventType.OrderRefused;
+
+  /**
+   * Condensed info about the order.
+   */
+  orderShortInfo: OrderShortInfo;
+}
+
+/**
+ * The wallet has claimed an order.
+ */
+export interface HistoryOrderRedirectedEvent {
+  /**
+   * Type tag.
+   */
+  type: HistoryEventType.OrderRedirected;
+
+  /**
+   * Condensed info about the new order that contains a
+   * product (identified by the fulfillment URL) that we've already paid for.
+   */
+  newOrderShortInfo: OrderShortInfo;
+
+  /**
+   * Condensed info about the order that we already paid for.
+   */
+  alreadyPaidOrderShortInfo: OrderShortInfo;
+}
+
+/**
+ * The user aborted a pending, partially submitted payment.
+ */
+export interface HistoryPaymentAbortedEvent {
+  /**
+   * Type tag.
+   */
+  type: HistoryEventType.PaymentAborted;
+
+  /**
+   * Condensed info about the order that we already paid for.
+   */
+  orderShortInfo: OrderShortInfo;
+
+  /**
+   * Amount that was lost due to refund and refreshing fees.
+   */
+  amountLost: string;
+}
+
+/**
+ * History event to indicate that a payment has been (re-)submitted
+ * to the merchant.
+ */
+export interface HistoryPaymentSent {
+  /**
+   * Type tag.
+   */
+  type: HistoryEventType.PaymentAborted;
+
+  /**
+   * Condensed info about the order that we already paid for.
+   */
+  orderShortInfo: OrderShortInfo;
 
-export interface HistoryQuery {
   /**
-   * Verbosity of history events.
-   * Level 0: Only withdraw, pay, tip and refund events.
-   * Level 1: All events.
+   * Set to true if the payment has been previously sent
+   * to the merchant successfully, possibly with a different session ID.
    */
-  level: number;
-}
\ No newline at end of file
+  replay: boolean;
+
+  /**
+   * Session ID that the payment was (re-)submitted under.
+   */
+  sessionId: string | undefined;
+}
+
+/**
+ * A refund has been applied.
+ */
+export interface HistoryRefund {
+  /**
+   * Type tag.
+   */
+  type: HistoryEventType.Refund;
+
+  timestamp: Timestamp;
+
+  orderShortInfo: OrderShortInfo;
+
+  /**
+   * Unique identifier for this refund.
+   * (Identifies multiple refund permissions that were obtained at once.)
+   */
+  refundId: string;
+
+  /**
+   * Part of the refund that couldn't be applied because
+   * the refund permissions were expired.
+   */
+  amountRefundedInvalid: string;
+
+  /**
+   * Amount that has been refunded by the merchant.
+   */
+  amountRefundedRaw: string;
+
+  /**
+   * Amount will be added to the wallet's balance after fees and refreshing.
+   */
+  amountRefundedEffective: string;
+}
+
+/**
+ * Reasons for why a coin is being refreshed.
+ */
+export const enum RefreshReason {
+  Manual = "manual",
+  Pay = "pay",
+  Refund = "refund",
+  AbortPay = "abort-pay",
+}
+
+/**
+ * Event to indicate that a refresh operation completed.
+ */
+export interface HistoryRefreshedEvent {
+  /**
+   * Type tag.
+   */
+  type: HistoryEventType.Refreshed;
+
+  /**
+   * Amount that is now available again because it has
+   * been refreshed.
+   */
+  amountRefreshed: string;
+
+  /**
+   * Why was the refresh done?
+   */
+  refreshReason: RefreshReason;
+
+  /**
+   * Refresh session ID, to find out more details.
+   */
+  refreshSessionId: string;
+}
+
+/**
+ * A withdrawal has completed.
+ */
+export interface HistoryWithdrawnEvent {
+  type: HistoryEventType.Withdrawn;
+
+  /**
+   * Exchange that was withdrawn from.
+   */
+  exchangeBaseUrl: string;
+
+  /**
+   * Unique identifier for the withdrawal session, can be used to
+   * query more detailed information from the wallet.
+   */
+  withdrawSessionId: string;
+
+  /**
+   * Amount that has been subtracted from the reserve's balance
+   * for this withdrawal.
+   */
+  amountWithdrawnRaw: string;
+
+  /**
+   * Amount that actually was added to the wallet's balance.
+   */
+  amountWithdrawnEffective: string;
+}
+
+/**
+ * Common fields that all history events need to have.
+ */
+export interface HistoryEventBase {
+  type: HistoryEventType;
+
+  /**
+   * Main timestamp of the history event.
+   */
+  timestamp: Timestamp;
+}
+
+/**
+ * Union of all history detail types, discriminated by their type.
+ */
+export type HistoryEvent = HistoryEventBase &
+  (
+    | HistoryAuditorComplaintSentEvent
+    | HistoryAuditorComplaintProcessedEvent
+    | HistoryAuditorTrustAddedEvent
+    | HistoryAuditorTrustRemovedEvent
+    | HistoryExchangeAddedEvent
+    | HistoryExchangeTermsAcceptedEvent
+    | HistoryExchangePolicyChangedEvent
+    | HistoryExchangeTrustAddedEvent
+    | HistoryExchangeTrustRemovedEvent
+    | HistoryExchangeUpdatedEvent
+    | HistoryFundsDepositedToSelfEvent
+    | HistoryFundsRecoupedEvent
+    | HistoryOrderAcceptedEvent
+    | HistoryOrderRedirectedEvent
+    | HistoryOrderRefusedEvent
+    | HistoryPaymentAbortedEvent
+    | HistoryPaymentSent
+    | HistoryRefreshedEvent
+    | HistoryRefund
+    | HistoryReserveBalanceUpdatedEvent
+    | HistoryReserveConfirmeEvent
+    | HistoryReserveCreatedEvent
+    | HistoryTipAcceptedEvent
+    | HistoryTipDeclinedEvent
+    | HistoryWithdrawnEvent
+  );
+
+export interface HistoryQuery {
+  // TBD
+}
diff --git a/src/types/walletTypes.ts b/src/types/walletTypes.ts
index a9bf2061..903852b1 100644
--- a/src/types/walletTypes.ts
+++ b/src/types/walletTypes.ts
@@ -25,20 +25,16 @@
 /**
  * Imports.
  */
+import { AmountJson } from "../util/amounts";
 import { Checkable } from "../util/checkable";
 import * as LibtoolVersion from "../util/libtoolVersion";
-
-import { AmountJson } from "../util/amounts";
-
 import {
   CoinRecord,
   DenominationRecord,
   ExchangeRecord,
   ExchangeWireInfo,
-  WithdrawalSource,
-  RetryInfo,
 } from "./dbTypes";
-import { CoinPaySig, ContractTerms, PayReq } from "./talerTypes";
+import { CoinPaySig, ContractTerms } from "./talerTypes";
 
 /**
  * Response for the create reserve request to the wallet.
diff --git a/src/util/query.ts b/src/util/query.ts
index e05656bb..08a8fec0 100644
--- a/src/util/query.ts
+++ b/src/util/query.ts
@@ -25,22 +25,6 @@
  */
 import { openPromise } from "./promiseUtils";
 
-/**
- * Result of an inner join.
- */
-export interface JoinResult<L, R> {
-  left: L;
-  right: R;
-}
-
-/**
- * Result of a left outer join.
- */
-export interface JoinLeftResult<L, R> {
-  left: L;
-  right?: R;
-}
-
 /**
  * Definition of an object store.
  */
@@ -95,46 +79,6 @@ function transactionToPromise(tx: IDBTransaction): 
Promise<void> {
   });
 }
 
-export async function oneShotGet<T>(
-  db: IDBDatabase,
-  store: Store<T>,
-  key: any,
-): Promise<T | undefined> {
-  const tx = db.transaction([store.name], "readonly");
-  const req = tx.objectStore(store.name).get(key);
-  const v = await requestToPromise(req);
-  await transactionToPromise(tx);
-  return v;
-}
-
-export async 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);
-  const v = await requestToPromise(req);
-  await transactionToPromise(tx);
-  return v;
-}
-
-export async 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);
-  const v = await requestToPromise(req);
-  await transactionToPromise(tx);
-  return v;
-}
-
 function applyMutation<T>(
   req: IDBRequest,
   f: (x: T) => T | undefined,
@@ -166,18 +110,6 @@ function applyMutation<T>(
   });
 }
 
-export async 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);
-  await applyMutation(req, f);
-  await transactionToPromise(tx);
-}
-
 type CursorResult<T> = CursorEmptyResult<T> | CursorValueResult<T>;
 
 interface CursorEmptyResult<T> {
@@ -294,28 +226,6 @@ class ResultStream<T> {
   }
 }
 
-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);
-}
-
-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);
-}
-
 export class TransactionHandle {
   constructor(private tx: IDBTransaction) {}
 
@@ -361,22 +271,6 @@ export class TransactionHandle {
   }
 }
 
-export function runWithReadTransaction<T>(
-  db: IDBDatabase,
-  stores: Store<any>[],
-  f: (t: TransactionHandle) => Promise<T>,
-): Promise<T> {
-  return runWithTransaction<T>(db, stores, f, "readonly");
-}
-
-export function runWithWriteTransaction<T>(
-  db: IDBDatabase,
-  stores: Store<any>[],
-  f: (t: TransactionHandle) => Promise<T>,
-): Promise<T> {
-  return runWithTransaction<T>(db, stores, f, "readwrite");
-}
-
 function runWithTransaction<T>(
   db: IDBDatabase,
   stores: Store<any>[],
@@ -470,7 +364,203 @@ export class Index<S extends IDBValidKey, T> {
   protected _dummyKey: S | undefined;
 }
 
+/**
+ * Return a promise that resolves
+ * to the taler wallet db.
+ */
+export function openDatabase(
+  idbFactory: IDBFactory,
+  databaseName: string,
+  databaseVersion: number,
+  schema: any,
+  onVersionChange: () => void,
+  onUpgradeUnsupported: (oldVersion: number, newVersion: number) => void,
+): Promise<IDBDatabase> {
+  return new Promise<IDBDatabase>((resolve, reject) => {
+    const req = idbFactory.open(databaseName, databaseVersion);
+    req.onerror = e => {
+      console.log("taler database error", e);
+      reject(new Error("database error"));
+    };
+    req.onsuccess = e => {
+      req.result.onversionchange = (evt: IDBVersionChangeEvent) => {
+        console.log(
+          `handling live db version change from ${evt.oldVersion} to 
${evt.newVersion}`,
+        );
+        req.result.close();
+        onVersionChange();
+      };
+      resolve(req.result);
+    };
+    req.onupgradeneeded = e => {
+      const db = req.result;
+      console.log(
+        `DB: upgrade needed: oldVersion=${e.oldVersion}, 
newVersion=${e.newVersion}`,
+      );
+      switch (e.oldVersion) {
+        case 0: // DB does not exist yet
+          for (const n in schema) {
+            if (schema[n] instanceof Store) {
+              const si: Store<any> = schema[n];
+              const s = db.createObjectStore(si.name, si.storeParams);
+              for (const indexName in si as any) {
+                if ((si as any)[indexName] instanceof Index) {
+                  const ii: Index<any, any> = (si as any)[indexName];
+                  s.createIndex(ii.indexName, ii.keyPath, ii.options);
+                }
+              }
+            }
+          }
+          break;
+        default:
+          if (e.oldVersion !== databaseVersion) {
+            onUpgradeUnsupported(e.oldVersion, databaseVersion);
+            throw Error("incompatible DB");
+          }
+          break;
+      }
+    };
+  });
+}
+
 /**
  * Exception that should be thrown by client code to abort a transaction.
  */
 export const TransactionAbort = Symbol("transaction_abort");
+
+export class Database {
+  constructor(private db: IDBDatabase) {}
+
+  static deleteDatabase(idbFactory: IDBFactory, dbName: string) {
+    idbFactory.deleteDatabase(dbName);
+  }
+
+  async exportDatabase(): Promise<any> {
+    const db = this.db;
+    const dump = {
+      name: db.name,
+      stores: {} as { [s: string]: any },
+      version: db.version,
+    };
+  
+    return new Promise((resolve, reject) => {
+      const tx = db.transaction(Array.from(db.objectStoreNames));
+      tx.addEventListener("complete", () => {
+        resolve(dump);
+      });
+      // tslint:disable-next-line:prefer-for-of
+      for (let i = 0; i < db.objectStoreNames.length; i++) {
+        const name = db.objectStoreNames[i];
+        const storeDump = {} as { [s: string]: any };
+        dump.stores[name] = storeDump;
+        tx.objectStore(name)
+          .openCursor()
+          .addEventListener("success", (e: Event) => {
+            const cursor = (e.target as any).result;
+            if (cursor) {
+              storeDump[cursor.key] = cursor.value;
+              cursor.continue();
+            }
+          });
+      }
+    });
+  }
+
+  importDatabase(dump: any): Promise<void> {
+    const db = this.db;
+    console.log("importing db", dump);
+    return new Promise<void>((resolve, reject) => {
+      const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite");
+      if (dump.stores) {
+        for (const storeName in dump.stores) {
+          const objects = [];
+          const dumpStore = dump.stores[storeName];
+          for (const key in dumpStore) {
+            objects.push(dumpStore[key]);
+          }
+          console.log(`importing ${objects.length} records into ${storeName}`);
+          const store = tx.objectStore(storeName);
+          for (const obj of objects) {
+            store.put(obj);
+          }
+        }
+      }
+      tx.addEventListener("complete", () => {
+        resolve();
+      });
+    });
+  }
+  
+  async get<T>(store: Store<T>, key: any): Promise<T | undefined> {
+    const tx = this.db.transaction([store.name], "readonly");
+    const req = tx.objectStore(store.name).get(key);
+    const v = await requestToPromise(req);
+    await transactionToPromise(tx);
+    return v;
+  }
+
+  async getIndexed<S extends IDBValidKey, T>(
+    index: Index<S, T>,
+    key: any,
+  ): Promise<T | undefined> {
+    const tx = this.db.transaction([index.storeName], "readonly");
+    const req = tx
+      .objectStore(index.storeName)
+      .index(index.indexName)
+      .get(key);
+    const v = await requestToPromise(req);
+    await transactionToPromise(tx);
+    return v;
+  }
+
+  async put<T>(store: Store<T>, value: T, key?: any): Promise<any> {
+    const tx = this.db.transaction([store.name], "readwrite");
+    const req = tx.objectStore(store.name).put(value, key);
+    const v = await requestToPromise(req);
+    await transactionToPromise(tx);
+    return v;
+  }
+
+  async mutate<T>(
+    store: Store<T>,
+    key: any,
+    f: (x: T) => T | undefined,
+  ): Promise<void> {
+    const tx = this.db.transaction([store.name], "readwrite");
+    const req = tx.objectStore(store.name).openCursor(key);
+    await applyMutation(req, f);
+    await transactionToPromise(tx);
+  }
+
+  iter<T>(store: Store<T>): ResultStream<T> {
+    const tx = this.db.transaction([store.name], "readonly");
+    const req = tx.objectStore(store.name).openCursor();
+    return new ResultStream<T>(req);
+  }
+
+  iterIndex<S extends IDBValidKey, T>(
+    index: Index<S, T>,
+    query?: any,
+  ): ResultStream<T> {
+    const tx = this.db.transaction([index.storeName], "readonly");
+    const req = tx
+      .objectStore(index.storeName)
+      .index(index.indexName)
+      .openCursor(query);
+    return new ResultStream<T>(req);
+  }
+
+  async runWithReadTransaction<T>(
+    stores: Store<any>[],
+    f: (t: TransactionHandle) => Promise<T>,
+  ): Promise<T> {
+    return runWithTransaction<T>(this.db, stores, f, "readonly");
+  }
+
+  async runWithWriteTransaction<T>(
+    stores: Store<any>[],
+    f: (t: TransactionHandle) => Promise<T>,
+  ): Promise<T> {
+    return runWithTransaction<T>(this.db, stores, f, "readwrite");
+  }
+}
diff --git a/src/wallet.ts b/src/wallet.ts
index 1db458b3..e4088fab 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -25,11 +25,7 @@
 import { CryptoWorkerFactory } from "./crypto/workers/cryptoApi";
 import { HttpRequestLibrary } from "./util/http";
 import {
-  oneShotPut,
-  oneShotGet,
-  runWithWriteTransaction,
-  oneShotIter,
-  oneShotIterIndex,
+  Database
 } from "./util/query";
 
 import { AmountJson } from "./util/amounts";
@@ -148,12 +144,12 @@ export class Wallet {
   private stopped: boolean = false;
   private memoRunRetryLoop = new AsyncOpMemoSingle<void>();
 
-  get db(): IDBDatabase {
+  get db(): Database {
     return this.ws.db;
   }
 
   constructor(
-    db: IDBDatabase,
+    db: Database,
     http: HttpRequestLibrary,
     cryptoWorkerFactory: CryptoWorkerFactory,
   ) {
@@ -345,8 +341,7 @@ export class Wallet {
    * already been applied.
    */
   async fillDefaults() {
-    await runWithWriteTransaction(
-      this.db,
+    await this.db.runWithWriteTransaction(
       [Stores.config, Stores.currencies],
       async tx => {
         let applied = false;
@@ -381,7 +376,7 @@ export class Wallet {
    */
   async refreshDirtyCoins(): Promise<{ numRefreshed: number }> {
     let n = 0;
-    const coins = await oneShotIter(this.db, Stores.coins).toArray();
+    const coins = await this.db.iter(Stores.coins).toArray();
     for (let coin of coins) {
       if (coin.status == CoinStatus.Dirty) {
         try {
@@ -512,7 +507,7 @@ export class Wallet {
   async findExchange(
     exchangeBaseUrl: string,
   ): Promise<ExchangeRecord | undefined> {
-    return await oneShotGet(this.db, Stores.exchanges, exchangeBaseUrl);
+    return await this.db.get(Stores.exchanges, exchangeBaseUrl);
   }
 
   /**
@@ -540,8 +535,7 @@ export class Wallet {
   }
 
   async getDenoms(exchangeUrl: string): Promise<DenominationRecord[]> {
-    const denoms = await oneShotIterIndex(
-      this.db,
+    const denoms = await this.db.iterIndex(
       Stores.denominations.exchangeBaseUrlIndex,
       exchangeUrl,
     ).toArray();
@@ -549,37 +543,37 @@ export class Wallet {
   }
 
   async getProposal(proposalId: string): Promise<ProposalRecord | undefined> {
-    const proposal = await oneShotGet(this.db, Stores.proposals, proposalId);
+    const proposal = await this.db.get(Stores.proposals, proposalId);
     return proposal;
   }
 
   async getExchanges(): Promise<ExchangeRecord[]> {
-    return await oneShotIter(this.db, Stores.exchanges).toArray();
+    return await this.db.iter(Stores.exchanges).toArray();
   }
 
   async getCurrencies(): Promise<CurrencyRecord[]> {
-    return await oneShotIter(this.db, Stores.currencies).toArray();
+    return await this.db.iter(Stores.currencies).toArray();
   }
 
   async updateCurrency(currencyRecord: CurrencyRecord): Promise<void> {
     logger.trace("updating currency to", currencyRecord);
-    await oneShotPut(this.db, Stores.currencies, currencyRecord);
+    await this.db.put(Stores.currencies, currencyRecord);
   }
 
   async getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> {
-    return await oneShotIter(this.db, Stores.reserves).filter(
+    return await this.db.iter(Stores.reserves).filter(
       r => r.exchangeBaseUrl === exchangeBaseUrl,
     );
   }
 
   async getCoinsForExchange(exchangeBaseUrl: string): Promise<CoinRecord[]> {
-    return await oneShotIter(this.db, Stores.coins).filter(
+    return await this.db.iter(Stores.coins).filter(
       c => c.exchangeBaseUrl === exchangeBaseUrl,
     );
   }
 
   async getCoins(): Promise<CoinRecord[]> {
-    return await oneShotIter(this.db, Stores.coins).toArray();
+    return await this.db.iter(Stores.coins).toArray();
   }
 
   async payback(coinPub: string): Promise<void> {
@@ -587,7 +581,7 @@ export class Wallet {
   }
 
   async getPaybackReserves(): Promise<ReserveRecord[]> {
-    return await oneShotIter(this.db, Stores.reserves).filter(
+    return await this.db.iter(Stores.reserves).filter(
       r => r.hasPayback,
     );
   }
@@ -604,7 +598,7 @@ export class Wallet {
   async getSenderWireInfos(): Promise<SenderWireInfos> {
     const m: { [url: string]: Set<string> } = {};
 
-    await oneShotIter(this.db, Stores.exchanges).forEach(x => {
+    await this.db.iter(Stores.exchanges).forEach(x => {
       const wi = x.wireInfo;
       if (!wi) {
         return;
@@ -619,7 +613,7 @@ export class Wallet {
     });
 
     const senderWiresSet: Set<string> = new Set();
-    await oneShotIter(this.db, Stores.senderWires).forEach(x => {
+    await this.db.iter(Stores.senderWires).forEach(x => {
       senderWiresSet.add(x.paytoUri);
     });
 
@@ -649,7 +643,7 @@ export class Wallet {
   async getPurchase(
     contractTermsHash: string,
   ): Promise<PurchaseRecord | undefined> {
-    return oneShotGet(this.db, Stores.purchases, contractTermsHash);
+    return this.db.get(Stores.purchases, contractTermsHash);
   }
 
   async getFullRefundFees(
@@ -683,7 +677,7 @@ export class Wallet {
    * confirmation from the bank.).
    */
   public async handleNotifyReserve() {
-    const reserves = await oneShotIter(this.db, Stores.reserves).toArray();
+    const reserves = await this.db.iter(Stores.reserves).toArray();
     for (const r of reserves) {
       if (r.reserveStatus === ReserveRecordStatus.WAIT_CONFIRM_BANK) {
         try {
@@ -718,7 +712,7 @@ export class Wallet {
   }
 
   async getPurchaseDetails(hc: string): Promise<PurchaseDetails> {
-    const purchase = await oneShotGet(this.db, Stores.purchases, hc);
+    const purchase = await this.db.get(Stores.purchases, hc);
     if (!purchase) {
       throw Error("unknown purchase");
     }
diff --git a/src/webex/pages/popup.tsx b/src/webex/pages/popup.tsx
index 3a2856d6..b26e86e6 100644
--- a/src/webex/pages/popup.tsx
+++ b/src/webex/pages/popup.tsx
@@ -325,9 +325,10 @@ class WalletBalanceView extends React.Component<any, any> {
 }
 
 function formatHistoryItem(historyItem: HistoryEvent) {
-  const d = historyItem.detail;
+  const d = historyItem;
   console.log("hist item", historyItem);
   switch (historyItem.type) {
+    /*
     case "create-reserve":
       return (
         <i18n.Translate wrap="p">
@@ -414,6 +415,7 @@ function formatHistoryItem(historyItem: HistoryEvent) {
         </>
       );
     }
+    */
     default:
       return <p>{i18n.str`Unknown event (${historyItem.type})`}</p>;
   }
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index f3f4d80e..d5a08b5d 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -23,26 +23,20 @@
 /**
  * Imports.
  */
-import { BrowserHttpLib } from "../util/http";
+import { BrowserCryptoWorkerFactory } from "../crypto/workers/cryptoApi";
+import { deleteTalerDatabase, openTalerDatabase, WALLET_DB_VERSION } from 
"../db";
+import { ConfirmReserveRequest, CreateReserveRequest, ReturnCoinsRequest, 
WalletDiagnostics } from "../types/walletTypes";
 import { AmountJson } from "../util/amounts";
-import {
-  ConfirmReserveRequest,
-  CreateReserveRequest,
-  ReturnCoinsRequest,
-  WalletDiagnostics,
-} from "../types/walletTypes";
+import { BrowserHttpLib } from "../util/http";
+import { OpenedPromise, openPromise } from "../util/promiseUtils";
+import { classifyTalerUri, TalerUriType } from "../util/taleruri";
 import { Wallet } from "../wallet";
-import { isFirefox } from "./compat";
-import { WALLET_DB_VERSION } from "../types/dbTypes";
-import { openDatabase, exportDatabase, importDatabase, deleteDatabase } from 
"../db";
 import { ChromeBadge } from "./chromeBadge";
+import { isFirefox } from "./compat";
 import { MessageType } from "./messages";
 import * as wxApi from "./wxApi";
-import Port = chrome.runtime.Port;
 import MessageSender = chrome.runtime.MessageSender;
-import { BrowserCryptoWorkerFactory } from "../crypto/workers/cryptoApi";
-import { OpenedPromise, openPromise } from "../util/promiseUtils";
-import { classifyTalerUri, TalerUriType } from "../util/taleruri";
+import { Database } from "../util/query";
 
 const NeedsWallet = Symbol("NeedsWallet");
 
@@ -73,25 +67,17 @@ async function handleMessage(
     }
     case "dump-db": {
       const db = needsWallet().db;
-      return exportDatabase(db);
+      return db.exportDatabase()
     }
     case "import-db": {
       const db = needsWallet().db;
-      return importDatabase(db, detail.dump);
+      return db.importDatabase(detail.dump);
     }
     case "ping": {
       return Promise.resolve();
     }
     case "reset-db": {
-      if (currentWallet) {
-        const db = currentWallet.db;
-        const tx = db.transaction(Array.from(db.objectStoreNames), 
"readwrite");
-        // tslint:disable-next-line:prefer-for-of
-        for (let i = 0; i < db.objectStoreNames.length; i++) {
-          tx.objectStore(db.objectStoreNames[i]).clear();
-        }
-      }
-      deleteDatabase(indexedDB);
+      deleteTalerDatabase(indexedDB);
       setBadgeText({ text: "" });
       console.log("reset done");
       if (!currentWallet) {
@@ -423,7 +409,7 @@ async function reinitWallet() {
   setBadgeText({ text: "" });
   const badge = new ChromeBadge();
   try {
-    currentDatabase = await openDatabase(
+    currentDatabase = await openTalerDatabase(
       indexedDB,
       reinitWallet,
       handleUpgradeUnsupported,
@@ -436,7 +422,7 @@ async function reinitWallet() {
   const http = new BrowserHttpLib();
   console.log("setting wallet");
   const wallet = new Wallet(
-    currentDatabase,
+    new Database(currentDatabase),
     http,
     new BrowserCryptoWorkerFactory(),
   );

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



reply via email to

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