gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (59bd755f -> fa4621e7)


From: gnunet
Subject: [taler-wallet-core] branch master updated (59bd755f -> fa4621e7)
Date: Mon, 16 Dec 2019 12:53:30 +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 59bd755f oops, missing file
     new 1b9c5855 simplify /pay, add pay event
     new fa4621e7 history events WIP

The 2 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/crypto/workers/cryptoApi.ts            |   6 +-
 src/crypto/workers/cryptoImplementation.ts |  22 +-
 src/operations/exchanges.ts                |  75 +++++--
 src/operations/history.ts                  | 337 ++++++++++++++++++++++++++++-
 src/operations/pay.ts                      | 161 ++++----------
 src/operations/pending.ts                  |  18 +-
 src/operations/refresh.ts                  |   2 +-
 src/operations/refund.ts                   | 120 ++++++----
 src/operations/reserves.ts                 |  44 +++-
 src/operations/return.ts                   |  15 +-
 src/operations/state.ts                    |   2 -
 src/operations/tip.ts                      |   9 +-
 src/operations/withdraw.ts                 |   2 +-
 src/types/dbTypes.ts                       |  86 +++++++-
 src/types/history.ts                       |  46 ++--
 src/types/pending.ts                       |  19 ++
 src/types/talerTypes.ts                    |  30 +--
 src/types/walletTypes.ts                   |  24 +-
 src/util/amounts.ts                        |   7 -
 src/util/codec-test.ts                     |  26 +--
 src/util/codec.ts                          | 159 ++++++++------
 src/util/helpers.ts                        |  10 +
 src/util/query.ts                          |  11 +
 src/wallet.ts                              |  82 ++++---
 tsconfig.json                              |   2 +
 25 files changed, 898 insertions(+), 417 deletions(-)

diff --git a/src/crypto/workers/cryptoApi.ts b/src/crypto/workers/cryptoApi.ts
index 3c675867..da807cce 100644
--- a/src/crypto/workers/cryptoApi.ts
+++ b/src/crypto/workers/cryptoApi.ts
@@ -39,7 +39,7 @@ import { ContractTerms, PaybackRequest } from 
"../../types/talerTypes";
 import {
   BenchmarkResult,
   CoinWithDenom,
-  PayCoinInfo,
+  PaySigInfo,
   PlanchetCreationResult,
   PlanchetCreationRequest,
 } from "../../types/walletTypes";
@@ -387,8 +387,8 @@ export class CryptoApi {
     contractTerms: ContractTerms,
     cds: CoinWithDenom[],
     totalAmount: AmountJson,
-  ): Promise<PayCoinInfo> {
-    return this.doRpc<PayCoinInfo>(
+  ): Promise<PaySigInfo> {
+    return this.doRpc<PaySigInfo>(
       "signDeposit",
       3,
       contractTerms,
diff --git a/src/crypto/workers/cryptoImplementation.ts 
b/src/crypto/workers/cryptoImplementation.ts
index 01cd797b..0049a122 100644
--- a/src/crypto/workers/cryptoImplementation.ts
+++ b/src/crypto/workers/cryptoImplementation.ts
@@ -39,11 +39,12 @@ import { CoinPaySig, ContractTerms, PaybackRequest } from 
"../../types/talerType
 import {
   BenchmarkResult,
   CoinWithDenom,
-  PayCoinInfo,
+  PaySigInfo,
   Timestamp,
   PlanchetCreationResult,
   PlanchetCreationRequest,
   getTimestampNow,
+  CoinPayInfo,
 } from "../../types/walletTypes";
 import { canonicalJson, getTalerStampSec } from "../../util/helpers";
 import { AmountJson } from "../../util/amounts";
@@ -348,11 +349,9 @@ export class CryptoImplementation {
     contractTerms: ContractTerms,
     cds: CoinWithDenom[],
     totalAmount: AmountJson,
-  ): PayCoinInfo {
-    const ret: PayCoinInfo = {
-      originalCoins: [],
-      sigs: [],
-      updatedCoins: [],
+  ): PaySigInfo {
+    const ret: PaySigInfo = {
+      coinInfo: [],
     };
 
     const contractTermsHash = this.hashString(canonicalJson(contractTerms));
@@ -369,8 +368,6 @@ export class CryptoImplementation {
     let amountRemaining = total;
 
     for (const cd of cds) {
-      const originalCoin = { ...cd.coin };
-
       if (amountRemaining.value === 0 && amountRemaining.fraction === 0) {
         break;
       }
@@ -416,9 +413,12 @@ export class CryptoImplementation {
         exchange_url: cd.denom.exchangeBaseUrl,
         ub_sig: cd.coin.denomSig,
       };
-      ret.sigs.push(s);
-      ret.updatedCoins.push(cd.coin);
-      ret.originalCoins.push(originalCoin);
+      const coinInfo: CoinPayInfo = {
+        sig: s,
+        coinPub: cd.coin.coinPub,
+        subtractedAmount: coinSpend,
+      };
+      ret.coinInfo.push(coinInfo);
     }
     return ret;
   }
diff --git a/src/operations/exchanges.ts b/src/operations/exchanges.ts
index 6c4c1aa0..fc1a50f0 100644
--- a/src/operations/exchanges.ts
+++ b/src/operations/exchanges.ts
@@ -25,15 +25,15 @@ import {
   DenominationRecord,
   DenominationStatus,
   WireFee,
+  ExchangeUpdateReason,
+  ExchangeUpdatedEventRecord,
 } from "../types/dbTypes";
 import {
   canonicalizeBaseUrl,
   extractTalerStamp,
   extractTalerStampOrThrow,
 } from "../util/helpers";
-import {
-  Database
-} from "../util/query";
+import { Database } from "../util/query";
 import * as Amounts from "../util/amounts";
 import { parsePaytoUri } from "../util/payto";
 import {
@@ -78,7 +78,7 @@ async function setExchangeError(
     exchange.lastError = err;
     return exchange;
   };
-  await ws.db.mutate( Stores.exchanges, baseUrl, mut);
+  await ws.db.mutate(Stores.exchanges, baseUrl, mut);
 }
 
 /**
@@ -91,12 +91,9 @@ async function updateExchangeWithKeys(
   ws: InternalWalletState,
   baseUrl: string,
 ): Promise<void> {
-  const existingExchangeRecord = await ws.db.get(
-    Stores.exchanges,
-    baseUrl,
-  );
+  const existingExchangeRecord = await ws.db.get(Stores.exchanges, baseUrl);
 
-  if (existingExchangeRecord?.updateStatus != ExchangeUpdateStatus.FETCH_KEYS) 
{
+  if (existingExchangeRecord?.updateStatus != ExchangeUpdateStatus.FetchKeys) {
     return;
   }
   const keysUrl = new URL("keys", baseUrl);
@@ -194,7 +191,7 @@ async function updateExchangeWithKeys(
         masterPublicKey: exchangeKeysJson.master_public_key,
         protocolVersion: protocolVersion,
       };
-      r.updateStatus = ExchangeUpdateStatus.FETCH_WIRE;
+      r.updateStatus = ExchangeUpdateStatus.FetchWire;
       r.lastError = undefined;
       await tx.put(Stores.exchanges, r);
 
@@ -213,6 +210,38 @@ async function updateExchangeWithKeys(
   );
 }
 
+async function updateExchangeFinalize(
+  ws: InternalWalletState,
+  exchangeBaseUrl: string,
+) {
+  const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
+  if (!exchange) {
+    return;
+  }
+  if (exchange.updateStatus != ExchangeUpdateStatus.FinalizeUpdate) {
+    return;
+  }
+  await ws.db.runWithWriteTransaction(
+    [Stores.exchanges, Stores.exchangeUpdatedEvents],
+    async tx => {
+      const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
+      if (!r) {
+        return;
+      }
+      if (r.updateStatus != ExchangeUpdateStatus.FinalizeUpdate) {
+        return;
+      }
+      r.updateStatus = ExchangeUpdateStatus.Finished;
+      await tx.put(Stores.exchanges, r);
+      const updateEvent: ExchangeUpdatedEventRecord = {
+        exchangeBaseUrl: exchange.baseUrl,
+        timestamp: getTimestampNow(),
+      };
+      await tx.put(Stores.exchangeUpdatedEvents, updateEvent);
+    },
+  );
+}
+
 async function updateExchangeWithTermsOfService(
   ws: InternalWalletState,
   exchangeBaseUrl: string,
@@ -221,7 +250,7 @@ async function updateExchangeWithTermsOfService(
   if (!exchange) {
     return;
   }
-  if (exchange.updateStatus != ExchangeUpdateStatus.FETCH_TERMS) {
+  if (exchange.updateStatus != ExchangeUpdateStatus.FetchTerms) {
     return;
   }
   const reqUrl = new URL("terms", exchangeBaseUrl);
@@ -243,12 +272,12 @@ async function updateExchangeWithTermsOfService(
     if (!r) {
       return;
     }
-    if (r.updateStatus != ExchangeUpdateStatus.FETCH_TERMS) {
+    if (r.updateStatus != ExchangeUpdateStatus.FetchTerms) {
       return;
     }
     r.termsOfServiceText = tosText;
     r.termsOfServiceLastEtag = tosEtag;
-    r.updateStatus = ExchangeUpdateStatus.FINISHED;
+    r.updateStatus = ExchangeUpdateStatus.FinalizeUpdate;
     await tx.put(Stores.exchanges, r);
   });
 }
@@ -282,7 +311,7 @@ async function updateExchangeWithWireInfo(
   if (!exchange) {
     return;
   }
-  if (exchange.updateStatus != ExchangeUpdateStatus.FETCH_WIRE) {
+  if (exchange.updateStatus != ExchangeUpdateStatus.FetchWire) {
     return;
   }
   const details = exchange.details;
@@ -349,14 +378,14 @@ async function updateExchangeWithWireInfo(
     if (!r) {
       return;
     }
-    if (r.updateStatus != ExchangeUpdateStatus.FETCH_WIRE) {
+    if (r.updateStatus != ExchangeUpdateStatus.FetchWire) {
       return;
     }
     r.wireInfo = {
       accounts: wireInfo.accounts,
       feesForType: feesForType,
     };
-    r.updateStatus = ExchangeUpdateStatus.FETCH_TERMS;
+    r.updateStatus = ExchangeUpdateStatus.FetchTerms;
     r.lastError = undefined;
     await tx.put(Stores.exchanges, r);
   });
@@ -390,12 +419,13 @@ async function updateExchangeFromUrlImpl(
   const r = await ws.db.get(Stores.exchanges, baseUrl);
   if (!r) {
     const newExchangeRecord: ExchangeRecord = {
+      builtIn: false,
       baseUrl: baseUrl,
       details: undefined,
       wireInfo: undefined,
-      updateStatus: ExchangeUpdateStatus.FETCH_KEYS,
+      updateStatus: ExchangeUpdateStatus.FetchKeys,
       updateStarted: now,
-      updateReason: "initial",
+      updateReason: ExchangeUpdateReason.Initial,
       timestampAdded: getTimestampNow(),
       termsOfServiceAcceptedEtag: undefined,
       termsOfServiceAcceptedTimestamp: undefined,
@@ -409,14 +439,14 @@ async function updateExchangeFromUrlImpl(
       if (!rec) {
         return;
       }
-      if (rec.updateStatus != ExchangeUpdateStatus.FETCH_KEYS && !forceNow) {
+      if (rec.updateStatus != ExchangeUpdateStatus.FetchKeys && !forceNow) {
         return;
       }
-      if (rec.updateStatus != ExchangeUpdateStatus.FETCH_KEYS && forceNow) {
-        rec.updateReason = "forced";
+      if (rec.updateStatus != ExchangeUpdateStatus.FetchKeys && forceNow) {
+        rec.updateReason = ExchangeUpdateReason.Forced;
       }
       rec.updateStarted = now;
-      rec.updateStatus = ExchangeUpdateStatus.FETCH_KEYS;
+      rec.updateStatus = ExchangeUpdateStatus.FetchKeys;
       rec.lastError = undefined;
       t.put(Stores.exchanges, rec);
     });
@@ -425,6 +455,7 @@ async function updateExchangeFromUrlImpl(
   await updateExchangeWithKeys(ws, baseUrl);
   await updateExchangeWithWireInfo(ws, baseUrl);
   await updateExchangeWithTermsOfService(ws, baseUrl);
+  await updateExchangeFinalize(ws, baseUrl);
 
   const updatedExchange = await ws.db.get(Stores.exchanges, baseUrl);
 
diff --git a/src/operations/history.ts b/src/operations/history.ts
index 8b225ea0..7e985d21 100644
--- a/src/operations/history.ts
+++ b/src/operations/history.ts
@@ -18,10 +18,132 @@
  * Imports.
  */
 import { InternalWalletState } from "./state";
-import { Stores, TipRecord } from "../types/dbTypes";
+import {
+  Stores,
+  TipRecord,
+  ProposalStatus,
+  ProposalRecord,
+} from "../types/dbTypes";
 import * as Amounts from "../util/amounts";
 import { AmountJson } from "../util/amounts";
-import { HistoryQuery, HistoryEvent, HistoryEventType } from 
"../types/history";
+import {
+  HistoryQuery,
+  HistoryEvent,
+  HistoryEventType,
+  OrderShortInfo,
+  ReserveType,
+  ReserveCreationDetail,
+} from "../types/history";
+import { assertUnreachable } from "../util/assertUnreachable";
+import { TransactionHandle, Store } from "../util/query";
+import { ReserveTransactionType } from "../types/ReserveTransaction";
+
+/**
+ * Create an event ID from the type and the primary key for the event.
+ */
+function makeEventId(type: HistoryEventType, ...args: string[]) {
+  return type + ";" + args.map(x => encodeURIComponent(x)).join(";");
+}
+
+function getOrderShortInfo(
+  proposal: ProposalRecord,
+): OrderShortInfo | undefined {
+  const download = proposal.download;
+  if (!download) {
+    return undefined;
+  }
+  return {
+    amount: download.contractTerms.amount,
+    orderId: download.contractTerms.order_id,
+    merchantBaseUrl: download.contractTerms.merchant_base_url,
+    proposalId: proposal.proposalId,
+    summary: download.contractTerms.summary || "",
+  };
+}
+
+
+async function collectProposalHistory(
+  tx: TransactionHandle,
+  history: HistoryEvent[],
+  historyQuery?: HistoryQuery,
+) {
+  tx.iter(Stores.proposals).forEachAsync(async proposal => {
+    const status = proposal.proposalStatus;
+    switch (status) {
+      case ProposalStatus.ACCEPTED:
+        {
+          const shortInfo = getOrderShortInfo(proposal);
+          if (!shortInfo) {
+            break;
+          }
+          history.push({
+            type: HistoryEventType.OrderAccepted,
+            eventId: makeEventId(
+              HistoryEventType.OrderAccepted,
+              proposal.proposalId,
+            ),
+            orderShortInfo: shortInfo,
+            timestamp: proposal.timestamp,
+          });
+        }
+        break;
+      case ProposalStatus.DOWNLOADING:
+      case ProposalStatus.PROPOSED:
+        // no history event needed
+        break;
+      case ProposalStatus.REJECTED:
+        {
+          const shortInfo = getOrderShortInfo(proposal);
+          if (!shortInfo) {
+            break;
+          }
+          history.push({
+            type: HistoryEventType.OrderRefused,
+            eventId: makeEventId(
+              HistoryEventType.OrderRefused,
+              proposal.proposalId,
+            ),
+            orderShortInfo: shortInfo,
+            timestamp: proposal.timestamp,
+          });
+        }
+        break;
+      case ProposalStatus.REPURCHASE:
+        {
+          const alreadyPaidProposal = await tx.get(
+            Stores.proposals,
+            proposal.repurchaseProposalId,
+          );
+          if (!alreadyPaidProposal) {
+            break;
+          }
+          const alreadyPaidOrderShortInfo = getOrderShortInfo(
+            alreadyPaidProposal,
+          );
+          if (!alreadyPaidOrderShortInfo) {
+            break;
+          }
+          const newOrderShortInfo = getOrderShortInfo(proposal);
+          if (!newOrderShortInfo) {
+            break;
+          }
+          history.push({
+            type: HistoryEventType.OrderRedirected,
+            eventId: makeEventId(
+              HistoryEventType.OrderRedirected,
+              proposal.proposalId,
+            ),
+            alreadyPaidOrderShortInfo,
+            newOrderShortInfo,
+            timestamp: proposal.timestamp,
+          });
+        }
+        break;
+      default:
+        assertUnreachable(status);
+    }
+  });
+}
 
 /**
  * Retrive the full event history for this wallet.
@@ -40,19 +162,222 @@ export async function getHistory(
   await ws.db.runWithReadTransaction(
     [
       Stores.currencies,
-      Stores.coins,
-      Stores.denominations,
       Stores.exchanges,
+      Stores.exchangeUpdatedEvents,
       Stores.proposals,
       Stores.purchases,
       Stores.refreshGroups,
       Stores.reserves,
       Stores.tips,
       Stores.withdrawalSession,
+      Stores.payEvents,
+      Stores.refundEvents,
+      Stores.reserveUpdatedEvents,
     ],
     async tx => {
-      // FIXME: implement new history schema!!
-    }
+      tx.iter(Stores.exchanges).forEach(exchange => {
+        history.push({
+          type: HistoryEventType.ExchangeAdded,
+          builtIn: false,
+          eventId: makeEventId(
+            HistoryEventType.ExchangeAdded,
+            exchange.baseUrl,
+          ),
+          exchangeBaseUrl: exchange.baseUrl,
+          timestamp: exchange.timestampAdded,
+        });
+      });
+
+      tx.iter(Stores.exchangeUpdatedEvents).forEach(eu => {
+        history.push({
+          type: HistoryEventType.ExchangeUpdated,
+          eventId: makeEventId(
+            HistoryEventType.ExchangeUpdated,
+            eu.exchangeBaseUrl,
+          ),
+          exchangeBaseUrl: eu.exchangeBaseUrl,
+          timestamp: eu.timestamp,
+        });
+      });
+
+      tx.iter(Stores.withdrawalSession).forEach(wsr => {
+        if (wsr.finishTimestamp) {
+          history.push({
+            type: HistoryEventType.Withdrawn,
+            withdrawSessionId: wsr.withdrawSessionId,
+            eventId: makeEventId(
+              HistoryEventType.Withdrawn,
+              wsr.withdrawSessionId,
+            ),
+            amountWithdrawnEffective: Amounts.toString(wsr.totalCoinValue),
+            amountWithdrawnRaw: Amounts.toString(wsr.rawWithdrawalAmount),
+            exchangeBaseUrl: wsr.exchangeBaseUrl,
+            timestamp: wsr.finishTimestamp,
+          });
+        }
+      });
+
+      await collectProposalHistory(tx, history, historyQuery);
+
+      await tx.iter(Stores.payEvents).forEachAsync(async (pe) => {
+        const proposal = await tx.get(Stores.proposals, pe.proposalId);
+        if (!proposal) {
+          return;
+        }
+        const orderShortInfo = getOrderShortInfo(proposal);
+        if (!orderShortInfo) {
+          return;
+        }
+        history.push({
+          type: HistoryEventType.PaymentSent,
+          eventId: makeEventId(HistoryEventType.PaymentSent, pe.proposalId),
+          orderShortInfo,
+          replay: pe.isReplay,
+          sessionId: pe.sessionId,
+          timestamp: pe.timestamp,
+        });
+      });
+
+      await tx.iter(Stores.refreshGroups).forEachAsync(async (rg) => {
+        if (!rg.finishedTimestamp) {
+          return;
+        }
+        let numInputCoins = 0;
+        let numRefreshedInputCoins = 0;
+        let numOutputCoins = 0;
+        const amountsRaw: AmountJson[] = [];
+        const amountsEffective: AmountJson[] = [];
+        for (let i = 0; i < rg.refreshSessionPerCoin.length; i++) {
+          const session = rg.refreshSessionPerCoin[i];
+          numInputCoins++;
+          if (session) {
+            numRefreshedInputCoins++;
+            amountsRaw.push(session.valueWithFee);
+            amountsEffective.push(session.valueOutput);
+            numOutputCoins += session.newDenoms.length;
+          } else {
+            const c = await tx.get(Stores.coins, rg.oldCoinPubs[i]);
+            if (!c) {
+              continue;
+            }
+            amountsRaw.push(c.currentAmount);
+          }
+        }
+        let amountRefreshedRaw = Amounts.sum(amountsRaw).amount;
+        let amountRefreshedEffective: AmountJson;
+        if (amountsEffective.length == 0) {
+          amountRefreshedEffective = 
Amounts.getZero(amountRefreshedRaw.currency);
+        } else {
+          amountRefreshedEffective = Amounts.sum(amountsEffective).amount;
+        }
+        history.push({
+          type: HistoryEventType.Refreshed,
+          refreshGroupId: rg.refreshGroupId,
+          eventId: makeEventId(HistoryEventType.Refreshed, rg.refreshGroupId),
+          timestamp: rg.finishedTimestamp,
+          refreshReason: rg.reason,
+          amountRefreshedEffective: Amounts.toString(amountRefreshedEffective),
+          amountRefreshedRaw: Amounts.toString(amountRefreshedRaw),
+          numInputCoins,
+          numOutputCoins,
+          numRefreshedInputCoins,
+        });
+      });
+
+      tx.iter(Stores.reserveUpdatedEvents).forEachAsync(async (ru) => {
+        const reserve = await tx.get(Stores.reserves, ru.reservePub);
+        if (!reserve) {
+          return;
+        }
+        let reserveCreationDetail: ReserveCreationDetail;
+        if (reserve.bankWithdrawStatusUrl) {
+          reserveCreationDetail = {
+            type: ReserveType.TalerBankWithdraw,
+            bankUrl: reserve.bankWithdrawStatusUrl,
+          }
+        } else {
+          reserveCreationDetail = {
+            type: ReserveType.Manual,
+          }
+        }
+        history.push({
+          type: HistoryEventType.ReserveBalanceUpdated,
+          eventId: makeEventId(HistoryEventType.ReserveBalanceUpdated, 
ru.reserveUpdateId),
+          amountExpected: ru.amountExpected,
+          amountReserveBalance: ru.amountReserveBalance,
+          timestamp: reserve.created,
+          newHistoryTransactions: ru.newHistoryTransactions,
+          reserveShortInfo: {
+            exchangeBaseUrl: reserve.exchangeBaseUrl,
+            reserveCreationDetail,
+            reservePub: reserve.reservePub,
+          }
+        });
+      });
+
+      tx.iter(Stores.tips).forEach((tip) => {
+        if (tip.acceptedTimestamp) {
+          history.push({
+            type: HistoryEventType.TipAccepted,
+            eventId: makeEventId(HistoryEventType.TipAccepted, tip.tipId),
+            timestamp: tip.acceptedTimestamp,
+            tipId: tip.tipId,
+            tipAmount: Amounts.toString(tip.amount),
+          });
+        }
+      });
+
+      tx.iter(Stores.refundEvents).forEachAsync(async (re) => {
+        const proposal = await tx.get(Stores.proposals, re.proposalId);
+        if (!proposal) {
+          return;
+        }
+        const purchase = await tx.get(Stores.purchases, re.proposalId);
+        if (!purchase) {
+          return;
+        }
+        const orderShortInfo = getOrderShortInfo(proposal);
+        if (!orderShortInfo) {
+          return;
+        }
+        const purchaseAmount = 
Amounts.parseOrThrow(purchase.contractTerms.amount);
+        let amountRefundedRaw = Amounts.getZero(purchaseAmount.currency);
+        let amountRefundedInvalid = Amounts.getZero(purchaseAmount.currency);
+        let amountRefundedEffective = Amounts.getZero(purchaseAmount.currency);
+        Object.keys(purchase.refundState.refundsDone).forEach((x, i) => {
+          const r = purchase.refundState.refundsDone[x];
+          if (r.refundGroupId !== re.refundGroupId) {
+            return;
+          }
+          const refundAmount = Amounts.parseOrThrow(r.perm.refund_amount);
+          const refundFee = Amounts.parseOrThrow(r.perm.refund_fee);
+          amountRefundedRaw = Amounts.add(amountRefundedRaw, 
refundAmount).amount;
+          amountRefundedEffective = Amounts.add(amountRefundedEffective, 
refundAmount).amount;
+          amountRefundedEffective = Amounts.sub(amountRefundedEffective, 
refundFee).amount;
+        });
+        Object.keys(purchase.refundState.refundsFailed).forEach((x, i) => {
+          const r = purchase.refundState.refundsFailed[x];
+          if (r.refundGroupId !== re.refundGroupId) {
+            return;
+          }
+          const ra = Amounts.parseOrThrow(r.perm.refund_amount);
+          const refundFee = Amounts.parseOrThrow(r.perm.refund_fee);
+          amountRefundedRaw = Amounts.add(amountRefundedRaw, ra).amount;
+          amountRefundedInvalid = Amounts.add(amountRefundedInvalid, 
ra).amount;
+          amountRefundedEffective = Amounts.sub(amountRefundedEffective, 
refundFee).amount;
+        });
+        history.push({
+          type: HistoryEventType.Refund,
+          eventId: makeEventId(HistoryEventType.Refund, re.refundGroupId),
+          refundGroupId: re.refundGroupId,
+          orderShortInfo,
+          timestamp: re.timestamp,
+          amountRefundedEffective: Amounts.toString(amountRefundedEffective),
+          amountRefundedRaw: Amounts.toString(amountRefundedRaw),
+          amountRefundedInvalid: Amounts.toString(amountRefundedInvalid),
+        });
+      });
+    },
   );
 
   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 5ed29350..66452469 100644
--- a/src/operations/pay.ts
+++ b/src/operations/pay.ts
@@ -36,6 +36,7 @@ import {
   RefundReason,
   Stores,
   updateRetryInfoTimeout,
+  PayEventRecord,
 } from "../types/dbTypes";
 import { NotificationType } from "../types/notifications";
 import {
@@ -52,7 +53,7 @@ import {
   ConfirmPayResult,
   getTimestampNow,
   OperationError,
-  PayCoinInfo,
+  PaySigInfo,
   PreparePayResult,
   RefreshReason,
   Timestamp,
@@ -73,13 +74,6 @@ import { createRefreshGroup, getTotalRefreshCost } from 
"./refresh";
 import { acceptRefundResponse } from "./refund";
 import { InternalWalletState } from "./state";
 
-export interface SpeculativePayData {
-  payCoinInfo: PayCoinInfo;
-  exchangeUrl: string;
-  orderDownloadId: string;
-  proposal: ProposalRecord;
-}
-
 interface CoinsForPaymentArgs {
   allowedAuditors: Auditor[];
   allowedExchanges: ExchangeHandle[];
@@ -323,8 +317,7 @@ async function getCoinsForPayment(
 async function recordConfirmPay(
   ws: InternalWalletState,
   proposal: ProposalRecord,
-  payCoinInfo: PayCoinInfo,
-  chosenExchange: string,
+  payCoinInfo: PaySigInfo,
   sessionIdOverride: string | undefined,
 ): Promise<PurchaseRecord> {
   const d = proposal.download;
@@ -339,7 +332,7 @@ async function recordConfirmPay(
   }
   logger.trace(`recording payment with session ID ${sessionId}`);
   const payReq: PayReq = {
-    coins: payCoinInfo.sigs,
+    coins: payCoinInfo.coinInfo.map((x) => x.sig),
     merchant_pub: d.contractTerms.merchant_pub,
     mode: "pay",
     order_id: d.contractTerms.order_id,
@@ -374,7 +367,7 @@ async function recordConfirmPay(
   };
 
   await ws.db.runWithWriteTransaction(
-    [Stores.coins, Stores.purchases, Stores.proposals],
+    [Stores.coins, Stores.purchases, Stores.proposals, Stores.refreshGroups],
     async tx => {
       const p = await tx.get(Stores.proposals, proposal.proposalId);
       if (p) {
@@ -384,9 +377,21 @@ async function recordConfirmPay(
         await tx.put(Stores.proposals, p);
       }
       await tx.put(Stores.purchases, t);
-      for (let c of payCoinInfo.updatedCoins) {
-        await tx.put(Stores.coins, c);
+      for (let coinInfo of payCoinInfo.coinInfo) {
+        const coin = await tx.get(Stores.coins, coinInfo.coinPub);
+        if (!coin) {
+          throw Error("coin allocated for payment doesn't exist anymore");
+        }
+        coin.status = CoinStatus.Dormant;
+        const remaining = Amounts.sub(coin.currentAmount, 
coinInfo.subtractedAmount);
+        if (remaining.saturated) {
+          throw Error("not enough remaining balance on coin for payment");
+        }
+        coin.currentAmount = remaining.amount;
+        await tx.put(Stores.coins, coin);
       }
+      const refreshCoinPubs = payCoinInfo.coinInfo.map((x) => ({coinPub: 
x.coinPub}));
+      await createRefreshGroup(tx, refreshCoinPubs, RefreshReason.Pay);
     },
   );
 
@@ -707,6 +712,8 @@ export async function submitPay(
   const merchantResp = await resp.json();
   console.log("got success from pay URL", merchantResp);
 
+  const now = getTimestampNow();
+
   const merchantPub = purchase.contractTerms.merchant_pub;
   const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
     merchantResp.sig,
@@ -719,7 +726,7 @@ export async function submitPay(
     throw Error("merchant payment signature invalid");
   }
   const isFirst = purchase.firstSuccessfulPayTimestamp === undefined;
-  purchase.firstSuccessfulPayTimestamp = getTimestampNow();
+  purchase.firstSuccessfulPayTimestamp = now;
   purchase.paymentSubmitPending = false;
   purchase.lastPayError = undefined;
   purchase.payRetryInfo = initRetryInfo(false);
@@ -734,35 +741,23 @@ export async function submitPay(
         purchase.refundStatusRetryInfo = initRetryInfo();
         purchase.lastRefundStatusError = undefined;
         purchase.autoRefundDeadline = {
-          t_ms: getTimestampNow().t_ms + autoRefundDelay.d_ms,
+          t_ms: now.t_ms + autoRefundDelay.d_ms,
         };
       }
     }
   }
 
-  const modifiedCoins: CoinRecord[] = [];
-  for (const pc of purchase.payReq.coins) {
-    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");
-    }
-    c.status = CoinStatus.Dormant;
-    modifiedCoins.push(c);
-  }
-
   await ws.db.runWithWriteTransaction(
-    [Stores.coins, Stores.purchases, Stores.refreshGroups],
+    [Stores.purchases, Stores.payEvents],
     async tx => {
-      for (let c of modifiedCoins) {
-        await tx.put(Stores.coins, c);
-      }
-      await createRefreshGroup(
-        tx,
-        modifiedCoins.map(x => ({ coinPub: x.coinPub })),
-        RefreshReason.Pay,
-      );
       await tx.put(Stores.purchases, purchase);
+      const payEvent: PayEventRecord = {
+        proposalId,
+        sessionId,
+        timestamp: now,
+        isReplay: !isFirst,
+      };
+      await tx.put(Stores.payEvents, payEvent);
     },
   );
 
@@ -861,27 +856,6 @@ export async function preparePay(
       };
     }
 
-    // Only create speculative signature if we don't already have one for this 
proposal
-    if (
-      !ws.speculativePayData ||
-      (ws.speculativePayData &&
-        ws.speculativePayData.orderDownloadId !== proposalId)
-    ) {
-      const { exchangeUrl, cds, totalAmount } = res;
-      const payCoinInfo = await ws.cryptoApi.signDeposit(
-        contractTerms,
-        cds,
-        totalAmount,
-      );
-      ws.speculativePayData = {
-        exchangeUrl,
-        payCoinInfo,
-        proposal,
-        orderDownloadId: proposalId,
-      };
-      logger.trace("created speculative pay data for payment");
-    }
-
     return {
       status: "payment-possible",
       contractTerms: contractTerms,
@@ -901,43 +875,6 @@ export async function preparePay(
   };
 }
 
-/**
- * Get the speculative pay data, but only if coins have not changed in between.
- */
-async function getSpeculativePayData(
-  ws: InternalWalletState,
-  proposalId: string,
-): Promise<SpeculativePayData | undefined> {
-  const sp = ws.speculativePayData;
-  if (!sp) {
-    return;
-  }
-  if (sp.orderDownloadId !== proposalId) {
-    return;
-  }
-  const coinKeys = sp.payCoinInfo.updatedCoins.map(x => x.coinPub);
-  const coins: CoinRecord[] = [];
-  for (let coinKey of coinKeys) {
-    const cc = await ws.db.get(Stores.coins, coinKey);
-    if (cc) {
-      coins.push(cc);
-    }
-  }
-  for (let i = 0; i < coins.length; i++) {
-    const specCoin = sp.payCoinInfo.originalCoins[i];
-    const currentCoin = coins[i];
-
-    // Coin does not exist anymore!
-    if (!currentCoin) {
-      return;
-    }
-    if (Amounts.cmp(specCoin.currentAmount, currentCoin.currentAmount) !== 0) {
-      return;
-    }
-  }
-  return sp;
-}
-
 /**
  * Add a contract to the wallet and sign coins, and send them.
  */
@@ -1008,30 +945,18 @@ export async function confirmPay(
     throw Error("insufficient balance");
   }
 
-  const sd = await getSpeculativePayData(ws, proposalId);
-  if (!sd) {
-    const { exchangeUrl, cds, totalAmount } = res;
-    const payCoinInfo = await ws.cryptoApi.signDeposit(
-      d.contractTerms,
-      cds,
-      totalAmount,
-    );
-    purchase = await recordConfirmPay(
-      ws,
-      proposal,
-      payCoinInfo,
-      exchangeUrl,
-      sessionIdOverride,
-    );
-  } else {
-    purchase = await recordConfirmPay(
-      ws,
-      sd.proposal,
-      sd.payCoinInfo,
-      sd.exchangeUrl,
-      sessionIdOverride,
-    );
-  }
+  const { cds, totalAmount } = res;
+  const payCoinInfo = await ws.cryptoApi.signDeposit(
+    d.contractTerms,
+    cds,
+    totalAmount,
+  );
+  purchase = await recordConfirmPay(
+    ws,
+    proposal,
+    payCoinInfo,
+    sessionIdOverride
+  );
 
   logger.trace("confirmPay: submitting payment after creating purchase 
record");
   return submitPay(ws, proposalId);
diff --git a/src/operations/pending.ts b/src/operations/pending.ts
index b9b2c664..252c9e98 100644
--- a/src/operations/pending.ts
+++ b/src/operations/pending.ts
@@ -54,7 +54,7 @@ async function gatherExchangePending(
   }
   await tx.iter(Stores.exchanges).forEach(e => {
     switch (e.updateStatus) {
-      case ExchangeUpdateStatus.FINISHED:
+      case ExchangeUpdateStatus.Finished:
         if (e.lastError) {
           resp.pendingOperations.push({
             type: PendingOperationType.Bug,
@@ -89,7 +89,7 @@ async function gatherExchangePending(
           });
         }
         break;
-      case ExchangeUpdateStatus.FETCH_KEYS:
+      case ExchangeUpdateStatus.FetchKeys:
         resp.pendingOperations.push({
           type: PendingOperationType.ExchangeUpdate,
           givesLifeness: false,
@@ -99,7 +99,7 @@ async function gatherExchangePending(
           reason: e.updateReason || "unknown",
         });
         break;
-      case ExchangeUpdateStatus.FETCH_WIRE:
+      case ExchangeUpdateStatus.FetchWire:
         resp.pendingOperations.push({
           type: PendingOperationType.ExchangeUpdate,
           givesLifeness: false,
@@ -109,6 +109,16 @@ async function gatherExchangePending(
           reason: e.updateReason || "unknown",
         });
         break;
+      case ExchangeUpdateStatus.FinalizeUpdate:
+          resp.pendingOperations.push({
+            type: PendingOperationType.ExchangeUpdate,
+            givesLifeness: false,
+            stage: "finalize-update",
+            exchangeBaseUrl: e.baseUrl,
+            lastError: e.lastError,
+            reason: e.updateReason || "unknown",
+          });
+        break;
       default:
         resp.pendingOperations.push({
           type: PendingOperationType.Bug,
@@ -311,7 +321,7 @@ async function gatherTipPending(
     if (onlyDue && tip.retryInfo.nextRetry.t_ms > now.t_ms) {
       return;
     }
-    if (tip.accepted) {
+    if (tip.acceptedTimestamp) {
       resp.pendingOperations.push({
         type: PendingOperationType.TipPickup,
         givesLifeness: true,
diff --git a/src/operations/refresh.ts b/src/operations/refresh.ts
index be23a5bb..d9a080bd 100644
--- a/src/operations/refresh.ts
+++ b/src/operations/refresh.ts
@@ -548,7 +548,7 @@ export async function createRefreshGroup(
     finishedTimestamp: undefined,
     finishedPerCoin: oldCoinPubs.map(x => false),
     lastError: undefined,
-    lastErrorPerCoin: oldCoinPubs.map(x => undefined),
+    lastErrorPerCoin: {},
     oldCoinPubs: oldCoinPubs.map(x => x.coinPub),
     reason,
     refreshGroupId,
diff --git a/src/operations/refund.ts b/src/operations/refund.ts
index a2b4dbe2..58941857 100644
--- a/src/operations/refund.ts
+++ b/src/operations/refund.ts
@@ -28,6 +28,7 @@ import {
   OperationError,
   getTimestampNow,
   RefreshReason,
+  CoinPublicKey,
 } from "../types/walletTypes";
 import {
   Stores,
@@ -36,6 +37,7 @@ import {
   CoinStatus,
   RefundReason,
   RefundEventRecord,
+  RefundInfo,
 } from "../types/dbTypes";
 import { NotificationType } from "../types/notifications";
 import { parseRefundUri } from "../util/taleruri";
@@ -214,13 +216,6 @@ export async function acceptRefundResponse(
         timestampQueried: now,
         reason,
       });
-
-      const refundEvent: RefundEventRecord = {
-        proposalId,
-        refundGroupId,
-        timestamp: now,
-      };
-      await tx.put(Stores.refundEvents, refundEvent);
     }
 
     await tx.put(Stores.purchases, p);
@@ -406,6 +401,9 @@ async function processPurchaseApplyRefundImpl(
     console.log("no pending refunds");
     return;
   }
+
+  const newRefundsDone: { [sig: string]: RefundInfo } = {};
+  const newRefundsFailed: { [sig: string]: RefundInfo } = {};
   for (const pk of pendingKeys) {
     const info = purchase.refundState.refundsPending[pk];
     const perm = info.perm;
@@ -424,13 +422,13 @@ async function processPurchaseApplyRefundImpl(
     const reqUrl = new URL("refund", exchangeUrl);
     const resp = await ws.http.postJson(reqUrl.href, req);
     console.log("sent refund permission");
-    let refundGone = false;
     switch (resp.status) {
       case HttpResponseStatus.Ok:
+        newRefundsDone[pk] = info;
         break;
       case HttpResponseStatus.Gone:
         // We're too late, refund is expired.
-        refundGone = true;
+        newRefundsFailed[pk] = info;
         break;
       default:
         let body: string | null = null;
@@ -446,53 +444,89 @@ async function processPurchaseApplyRefundImpl(
           },
         });
     }
+  }
+  let allRefundsProcessed = false;
+  await ws.db.runWithWriteTransaction(
+    [Stores.purchases, Stores.coins, Stores.refreshGroups, 
Stores.refundEvents],
+    async tx => {
+      const p = await tx.get(Stores.purchases, proposalId);
+      if (!p) {
+        return;
+      }
 
-    let allRefundsProcessed = false;
+      // Groups that failed/succeeded
+      let groups: { [refundGroupId: string]: boolean } = {};
 
-    await ws.db.runWithWriteTransaction(
-      [Stores.purchases, Stores.coins, Stores.refreshGroups],
-      async tx => {
-        const p = await tx.get(Stores.purchases, proposalId);
-        if (!p) {
-          return;
-        }
-        if (p.refundState.refundsPending[pk]) {
-          if (refundGone) {
-            p.refundState.refundsFailed[pk] = p.refundState.refundsPending[pk];
-          } else {
-            p.refundState.refundsDone[pk] = p.refundState.refundsPending[pk];
-          }
-          delete p.refundState.refundsPending[pk];
-        }
-        if (Object.keys(p.refundState.refundsPending).length === 0) {
-          p.refundStatusRetryInfo = initRetryInfo();
-          p.lastRefundStatusError = undefined;
-          allRefundsProcessed = true;
-        }
-        await tx.put(Stores.purchases, p);
+      // Avoid duplicates
+      const refreshCoinsMap: { [coinPub: string]: CoinPublicKey } = {};
+
+      const modCoin = async (perm: MerchantRefundPermission) => {
         const c = await tx.get(Stores.coins, perm.coin_pub);
         if (!c) {
           console.warn("coin not found, can't apply refund");
           return;
         }
+        refreshCoinsMap[c.coinPub] = { coinPub: c.coinPub };
         const refundAmount = Amounts.parseOrThrow(perm.refund_amount);
         const refundFee = Amounts.parseOrThrow(perm.refund_fee);
         c.status = CoinStatus.Dormant;
         c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
         c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
         await tx.put(Stores.coins, c);
-        await createRefreshGroup(
-          tx,
-          [{ coinPub: perm.coin_pub }],
-          RefreshReason.Refund,
-        );
-      },
-    );
-    if (allRefundsProcessed) {
-      ws.notify({
-        type: NotificationType.RefundFinished,
-      });
-    }
+      };
+
+      for (const pk of Object.keys(newRefundsFailed)) {
+        const r = newRefundsFailed[pk];
+        groups[r.refundGroupId] = true;
+        delete p.refundState.refundsPending[pk];
+        p.refundState.refundsFailed[pk] = r;
+        await modCoin(r.perm);
+      }
+
+      for (const pk of Object.keys(newRefundsDone)) {
+        const r = newRefundsDone[pk];
+        groups[r.refundGroupId] = true;
+        delete p.refundState.refundsPending[pk];
+        p.refundState.refundsDone[pk] = r;
+        await modCoin(r.perm);
+      }
+
+      const now = getTimestampNow();
+      for (const g of Object.keys(groups)) {
+        let groupDone = true;
+        for (const pk of Object.keys(p.refundState.refundsPending)) {
+          const r  = p.refundState.refundsPending[pk];
+          if (r.refundGroupId == g) {
+            groupDone = false;
+          }
+        }
+        if (groupDone) {
+          const refundEvent: RefundEventRecord = {
+            proposalId,
+            refundGroupId: g,
+            timestamp: now,
+          }
+          await tx.put(Stores.refundEvents, refundEvent);
+        }
+      }
+
+      if (Object.keys(p.refundState.refundsPending).length === 0) {
+        p.refundStatusRetryInfo = initRetryInfo();
+        p.lastRefundStatusError = undefined;
+        allRefundsProcessed = true;
+      }
+      await tx.put(Stores.purchases, p);
+      await createRefreshGroup(
+        tx,
+        Object.values(refreshCoinsMap),
+        RefreshReason.Refund,
+      );
+    },
+  );
+  if (allRefundsProcessed) {
+    ws.notify({
+      type: NotificationType.RefundFinished,
+    });
   }
 
   ws.notify({
diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts
index 559d3ab0..56e9c25d 100644
--- a/src/operations/reserves.ts
+++ b/src/operations/reserves.ts
@@ -31,17 +31,17 @@ import {
   WithdrawalSessionRecord,
   initRetryInfo,
   updateRetryInfoTimeout,
+  ReserveUpdatedEventRecord,
 } from "../types/dbTypes";
 import {
-  Database,
   TransactionAbort,
 } from "../util/query";
 import { Logger } from "../util/logging";
 import * as Amounts from "../util/amounts";
 import { updateExchangeFromUrl, getExchangeTrust } from "./exchanges";
-import { WithdrawOperationStatusResponse, ReserveStatus } from 
"../types/talerTypes";
+import { WithdrawOperationStatusResponse } from "../types/talerTypes";
 import { assertUnreachable } from "../util/assertUnreachable";
-import { encodeCrock } from "../crypto/talerCrypto";
+import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
 import { randomBytes } from "../crypto/primitives/nacl-fast";
 import {
   getVerifiedWithdrawDenomList,
@@ -49,6 +49,7 @@ import {
 } from "./withdraw";
 import { guardOperationException, OperationFailedAndReportedError } from 
"./errors";
 import { NotificationType } from "../types/notifications";
+import { codecForReserveStatus } from "../types/ReserveStatus";
 
 const logger = new Logger("reserves.ts");
 
@@ -94,6 +95,7 @@ export async function createReserve(
     lastSuccessfulStatusQuery: undefined,
     retryInfo: initRetryInfo(),
     lastError: undefined,
+    reserveTransactions: [],
   };
 
   const senderWire = req.senderWire;
@@ -393,17 +395,35 @@ async function updateReserve(
     });
     throw new OperationFailedAndReportedError(m);
   }
-  const reserveInfo = ReserveStatus.checked(await resp.json());
+  const respJson = await resp.json();
+  const reserveInfo = codecForReserveStatus.decode(respJson);
   const balance = Amounts.parseOrThrow(reserveInfo.balance);
-  await ws.db.mutate(Stores.reserves, reserve.reservePub, r => {
+  await ws.db.runWithWriteTransaction([Stores.reserves, 
Stores.reserveUpdatedEvents], async (tx) => {
+    const r = await tx.get(Stores.reserves, reservePub);
+    if (!r) {
+      return;
+    }
     if (r.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
       return;
     }
 
+    const newHistoryTransactions = 
reserveInfo.history.slice(r.reserveTransactions.length);
+
+    const reserveUpdateId = encodeCrock(getRandomBytes(32));
+
     // FIXME: check / compare history!
     if (!r.lastSuccessfulStatusQuery) {
       // FIXME: check if this matches initial expectations
       r.withdrawRemainingAmount = balance;
+      const reserveUpdate: ReserveUpdatedEventRecord = {
+        reservePub: r.reservePub,
+        timestamp: getTimestampNow(),
+        amountReserveBalance: Amounts.toString(balance),
+        amountExpected: Amounts.toString(reserve.initiallyRequestedAmount),
+        newHistoryTransactions,
+        reserveUpdateId,
+      };
+      await tx.put(Stores.reserveUpdatedEvents, reserveUpdate);
     } else {
       const expectedBalance = Amounts.sub(
         r.withdrawAllocatedAmount,
@@ -423,11 +443,21 @@ async function updateReserve(
       } else {
         // We're missing some money.
       }
+      const reserveUpdate: ReserveUpdatedEventRecord = {
+        reservePub: r.reservePub,
+        timestamp: getTimestampNow(),
+        amountReserveBalance: Amounts.toString(balance),
+        amountExpected: Amounts.toString(expectedBalance.amount),
+        newHistoryTransactions,
+        reserveUpdateId,
+      };
+      await tx.put(Stores.reserveUpdatedEvents, reserveUpdate);
     }
     r.lastSuccessfulStatusQuery = getTimestampNow();
     r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
     r.retryInfo = initRetryInfo();
-    return r;
+    r.reserveTransactions = reserveInfo.history;
+    await tx.put(Stores.reserves, r);
   });
   ws.notify( { type: NotificationType.ReserveUpdated });
 }
@@ -561,7 +591,7 @@ async function depleteReserve(
     planchets: denomsForWithdraw.map(x => undefined),
     totalCoinValue,
     retryInfo: initRetryInfo(),
-    lastCoinErrors: denomsForWithdraw.map(x => undefined),
+    lastErrorPerCoin: {},
     lastError: undefined,
   };
 
diff --git a/src/operations/return.ts b/src/operations/return.ts
index 01d2802d..4238f6cd 100644
--- a/src/operations/return.ts
+++ b/src/operations/return.ts
@@ -176,7 +176,7 @@ export async function returnCoins(
 
   logger.trace("pci", payCoinInfo);
 
-  const coins = payCoinInfo.sigs.map(s => ({ coinPaySig: s }));
+  const coins = payCoinInfo.coinInfo.map(s => ({ coinPaySig: s.sig }));
 
   const coinsReturnRecord: CoinsReturnRecord = {
     coins,
@@ -191,8 +191,17 @@ export async function returnCoins(
     [Stores.coinsReturns, Stores.coins],
     async tx => {
       await tx.put(Stores.coinsReturns, coinsReturnRecord);
-      for (let c of payCoinInfo.updatedCoins) {
-        await tx.put(Stores.coins, c);
+      for (let coinInfo of payCoinInfo.coinInfo) {
+        const coin = await tx.get(Stores.coins, coinInfo.coinPub);
+        if (!coin) {
+          throw Error("coin allocated for deposit not in database anymore");
+        }
+        const remaining = Amounts.sub(coin.currentAmount, 
coinInfo.subtractedAmount);
+        if (remaining.saturated) {
+          throw Error("coin allocated for deposit does not have enough 
balance");
+        }
+        coin.currentAmount = remaining.amount;
+        await tx.put(Stores.coins, coin);
       }
     },
   );
diff --git a/src/operations/state.ts b/src/operations/state.ts
index 1e4b9036..3e4936c9 100644
--- a/src/operations/state.ts
+++ b/src/operations/state.ts
@@ -19,7 +19,6 @@ import {
   NextUrlResult,
   WalletBalance,
 } from "../types/walletTypes";
-import { SpeculativePayData } from "./pay";
 import { CryptoApi, CryptoWorkerFactory } from "../crypto/workers/cryptoApi";
 import { AsyncOpMemoMap, AsyncOpMemoSingle } from "../util/asyncMemo";
 import { Logger } from "../util/logging";
@@ -32,7 +31,6 @@ type NotificationListener = (n: WalletNotification) => void;
 const logger = new Logger("state.ts");
 
 export class InternalWalletState {
-  speculativePayData: SpeculativePayData | undefined = undefined;
   cachedNextUrl: { [fulfillmentUrl: string]: NextUrlResult } = {};
   memoProcessReserve: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
   memoMakePlanchet: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
diff --git a/src/operations/tip.ts b/src/operations/tip.ts
index f9953b51..ba4b8097 100644
--- a/src/operations/tip.ts
+++ b/src/operations/tip.ts
@@ -68,7 +68,8 @@ export async function getTipStatus(
 
     tipRecord = {
       tipId,
-      accepted: false,
+      acceptedTimestamp: undefined,
+      rejectedTimestamp: undefined,
       amount,
       deadline: extractTalerStampOrThrow(tipPickupStatus.stamp_expire),
       exchangeUrl: tipPickupStatus.exchange_url,
@@ -90,7 +91,7 @@ export async function getTipStatus(
   }
 
   const tipStatus: TipStatus = {
-    accepted: !!tipRecord && tipRecord.accepted,
+    accepted: !!tipRecord && !!tipRecord.acceptedTimestamp,
     amount: Amounts.parseOrThrow(tipPickupStatus.amount),
     amountLeft: Amounts.parseOrThrow(tipPickupStatus.amount_left),
     exchangeUrl: tipPickupStatus.exchange_url,
@@ -259,7 +260,7 @@ async function processTipImpl(
     rawWithdrawalAmount: tipRecord.amount,
     withdrawn: planchets.map((x) => false),
     totalCoinValue: Amounts.sum(planchets.map((p) => p.coinValue)).amount,
-    lastCoinErrors: planchets.map((x) => undefined),
+    lastErrorPerCoin: {},
     retryInfo: initRetryInfo(),
     finishTimestamp: undefined,
     lastError: undefined,
@@ -296,7 +297,7 @@ export async function acceptTip(
     return;
   }
 
-  tipRecord.accepted = true;
+  tipRecord.acceptedTimestamp = getTimestampNow();
   await ws.db.put(Stores.tips, tipRecord);
 
   await processTip(ws, tipId);
diff --git a/src/operations/withdraw.ts b/src/operations/withdraw.ts
index a34eec5a..c7c91494 100644
--- a/src/operations/withdraw.ts
+++ b/src/operations/withdraw.ts
@@ -272,7 +272,7 @@ async function processPlanchet(
         return false;
       }
       ws.withdrawn[coinIdx] = true;
-      ws.lastCoinErrors[coinIdx] = undefined;
+      delete ws.lastErrorPerCoin[coinIdx];
       let numDone = 0;
       for (let i = 0; i < ws.withdrawn.length; i++) {
         if (ws.withdrawn[i]) {
diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts
index 9d2f6fe5..897c3503 100644
--- a/src/types/dbTypes.ts
+++ b/src/types/dbTypes.ts
@@ -43,6 +43,7 @@ import {
   getTimestampNow,
   RefreshReason,
 } from "./walletTypes";
+import { ReserveTransaction } from "./ReserveTransaction";
 
 export enum ReserveRecordStatus {
   /**
@@ -130,6 +131,7 @@ export function initRetryInfo(
   return info;
 }
 
+
 /**
  * A reserve record as stored in the wallet's database.
  */
@@ -237,6 +239,8 @@ export interface ReserveRecord {
    * (either talking to the bank or the exchange).
    */
   lastError: OperationError | undefined;
+
+  reserveTransactions: ReserveTransaction[];
 }
 
 /**
@@ -449,10 +453,11 @@ export interface ExchangeDetails {
 }
 
 export const enum ExchangeUpdateStatus {
-  FETCH_KEYS = "fetch_keys",
-  FETCH_WIRE = "fetch_wire",
-  FETCH_TERMS = "fetch_terms",
-  FINISHED = "finished",
+  FetchKeys = "fetch-keys",
+  FetchWire = "fetch-wire",
+  FetchTerms = "fetch-terms",
+  FinalizeUpdate = "finalize-update",
+  Finished = "finished",
 }
 
 export interface ExchangeBankAccount {
@@ -464,6 +469,12 @@ export interface ExchangeWireInfo {
   accounts: ExchangeBankAccount[];
 }
 
+export const enum ExchangeUpdateReason {
+  Initial = "initial",
+  Forced = "forced",
+  Scheduled = "scheduled",
+}
+
 /**
  * Exchange record as stored in the wallet's database.
  */
@@ -473,6 +484,11 @@ export interface ExchangeRecord {
    */
   baseUrl: string;
 
+  /**
+   * Was the exchange added as a built-in exchange?
+   */
+  builtIn: boolean;
+
   /**
    * Details, once known.
    */
@@ -514,7 +530,7 @@ export interface ExchangeRecord {
    */
   updateStarted: Timestamp | undefined;
   updateStatus: ExchangeUpdateStatus;
-  updateReason?: "initial" | "forced";
+  updateReason?: ExchangeUpdateReason;
 
   lastError?: OperationError;
 }
@@ -660,7 +676,7 @@ export interface CoinRecord {
   status: CoinStatus;
 }
 
-export enum ProposalStatus {
+export const enum ProposalStatus {
   /**
    * Not downloaded yet.
    */
@@ -777,11 +793,17 @@ export class ProposalRecord {
  */
 export interface TipRecord {
   lastError: OperationError | undefined;
+
   /**
    * Has the user accepted the tip?  Only after the tip has been accepted coins
    * withdrawn from the tip may be used.
    */
-  accepted: boolean;
+  acceptedTimestamp: Timestamp | undefined;
+
+  /**
+   * Has the user rejected the tip?
+   */
+  rejectedTimestamp: Timestamp | undefined;
 
   /**
    * Have we picked up the tip record from the merchant already?
@@ -855,7 +877,7 @@ export interface RefreshGroupRecord {
 
   lastError: OperationError | undefined;
 
-  lastErrorPerCoin: (OperationError | undefined)[];
+  lastErrorPerCoin: { [coinIndex: number]: OperationError };
 
   refreshGroupId: string;
 
@@ -1059,6 +1081,31 @@ export interface PurchaseRefundState {
   refundsFailed: { [refundSig: string]: RefundInfo };
 }
 
+/**
+ * Record stored for every time we successfully submitted
+ * a payment to the merchant (both first time and re-play).
+ */
+export interface PayEventRecord {
+  proposalId: string;
+  sessionId: string | undefined;
+  isReplay: boolean;
+  timestamp: Timestamp;
+}
+
+export interface ExchangeUpdatedEventRecord {
+  exchangeBaseUrl: string;
+  timestamp: Timestamp;
+}
+
+export interface ReserveUpdatedEventRecord {
+  amountReserveBalance: string;
+  amountExpected: string;
+  reservePub: string;
+  timestamp: Timestamp;
+  reserveUpdateId: string;
+  newHistoryTransactions: ReserveTransaction[];
+}
+
 /**
  * Record that stores status information about one purchase, starting from when
  * the customer accepts a proposal.  Includes refund status if applicable.
@@ -1288,7 +1335,7 @@ export interface WithdrawalSessionRecord {
    * Last error per coin/planchet, or undefined if no error occured for
    * the coin/planchet.
    */
-  lastCoinErrors: (OperationError | undefined)[];
+  lastErrorPerCoin: { [coinIndex: number]: OperationError };
 
   lastError: OperationError | undefined;
 }
@@ -1432,6 +1479,24 @@ export namespace Stores {
     }
   }
 
+  class PayEventsStore extends Store<PayEventRecord> {
+    constructor() {
+      super("payEvents", { keyPath: "proposalId" });
+    }
+  }
+
+  class ExchangeUpdatedEventsStore extends Store<ExchangeUpdatedEventRecord> {
+    constructor() {
+      super("exchangeUpdatedEvents", { keyPath: "exchangeBaseUrl" });
+    }
+  }
+
+  class ReserveUpdatedEventsStore extends Store<ReserveUpdatedEventRecord> {
+    constructor() {
+      super("reserveUpdatedEvents", { keyPath: "reservePub" });
+    }
+  }
+
   class BankWithdrawUrisStore extends Store<BankWithdrawUriRecord> {
     constructor() {
       super("bankWithdrawUris", { keyPath: "talerWithdrawUri" });
@@ -1457,6 +1522,9 @@ export namespace Stores {
   export const withdrawalSession = new WithdrawalSessionsStore();
   export const bankWithdrawUris = new BankWithdrawUrisStore();
   export const refundEvents = new RefundEventsStore();
+  export const payEvents = new PayEventsStore();
+  export const reserveUpdatedEvents = new ReserveUpdatedEventsStore();
+  export const exchangeUpdatedEvents = new ExchangeUpdatedEventsStore();
 }
 
 /* tslint:enable:completed-docs */
diff --git a/src/types/history.ts b/src/types/history.ts
index 54004b12..21000631 100644
--- a/src/types/history.ts
+++ b/src/types/history.ts
@@ -1,4 +1,5 @@
 import { Timestamp, RefreshReason } from "./walletTypes";
+import { ReserveTransaction } from "./ReserveTransaction";
 
 /*
  This file is part of GNU Taler
@@ -140,10 +141,7 @@ export interface HistoryReserveBalanceUpdatedEvent {
    */
   timestamp: Timestamp;
 
-  /**
-   * Unique identifier to query more information about this update.
-   */
-  reserveUpdateId: string;
+  newHistoryTransactions: ReserveTransaction[];
 
   /**
    * Condensed information about the reserve.
@@ -210,13 +208,7 @@ export interface HistoryTipAcceptedEvent {
   /**
    * 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;
+  tipRaw: string;
 }
 
 /**
@@ -238,13 +230,7 @@ export interface HistoryTipDeclinedEvent {
   /**
    * 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;
+  tipAmount: string;
 }
 
 /**
@@ -454,14 +440,7 @@ export interface OrderShortInfo {
   /**
    * 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;
+  amount: string;
 
   /**
    * Summary of the proposal, given by the merchant.
@@ -548,7 +527,7 @@ export interface HistoryPaymentSent {
   /**
    * Type tag.
    */
-  type: HistoryEventType.PaymentAborted;
+  type: HistoryEventType.PaymentSent;
 
   /**
    * Condensed info about the order that we already paid for.
@@ -584,7 +563,7 @@ export interface HistoryRefund {
    * Unique identifier for this refund.
    * (Identifies multiple refund permissions that were obtained at once.)
    */
-  refundId: string;
+  refundGroupId: string;
 
   /**
    * Part of the refund that couldn't be applied because
@@ -616,13 +595,22 @@ export interface HistoryRefreshedEvent {
    * Amount that is now available again because it has
    * been refreshed.
    */
-  amountRefreshed: string;
+  amountRefreshedEffective: string;
+
+  /**
+   * Amount that we spent for refreshing.
+   */
+  amountRefreshedRaw: string;
 
   /**
    * Why was the refreshing done?
    */
   refreshReason: RefreshReason;
 
+  numInputCoins: number;
+  numRefreshedInputCoins: number;
+  numOutputCoins: number;
+
   /**
    * Identifier for a refresh group, contains one or
    * more refresh session IDs.
diff --git a/src/types/pending.ts b/src/types/pending.ts
index d08d2c54..53932e8f 100644
--- a/src/types/pending.ts
+++ b/src/types/pending.ts
@@ -32,6 +32,7 @@ export const enum PendingOperationType {
   ProposalDownload = "proposal-download",
   Refresh = "refresh",
   Reserve = "reserve",
+  Recoup = "recoup",
   RefundApply = "refund-apply",
   RefundQuery = "refund-query",
   TipChoice = "tip-choice",
@@ -53,6 +54,7 @@ export type PendingOperationInfo = PendingOperationInfoCommon 
&
     | PendingRefundApplyOperation
     | PendingRefundQueryOperation
     | PendingReserveOperation
+    | PendingTipChoiceOperation
     | PendingTipPickupOperation
     | PendingWithdrawOperation
   );
@@ -115,6 +117,13 @@ export interface PendingTipPickupOperation {
   merchantTipId: string;
 }
 
+export interface PendingTipChoiceOperation {
+  type: PendingOperationType.TipChoice;
+  tipId: string;
+  merchantBaseUrl: string;
+  merchantTipId: string;
+}
+
 export interface PendingPayOperation {
   type: PendingOperationType.Pay;
   proposalId: string;
@@ -147,8 +156,18 @@ export interface PendingWithdrawOperation {
   numCoinsTotal: number;
 }
 
+export interface PendingOperationFlags {
+  isWaitingUser: boolean;
+  isError: boolean;
+  givesLifeness: boolean;
+}
+
 export interface PendingOperationInfoCommon {
+  /**
+   * Type of the pending operation.
+   */
   type: PendingOperationType;
+
   givesLifeness: boolean;
 }
 
diff --git a/src/types/talerTypes.ts b/src/types/talerTypes.ts
index df89b997..bb286b64 100644
--- a/src/types/talerTypes.ts
+++ b/src/types/talerTypes.ts
@@ -639,28 +639,6 @@ export class ReserveSigSingleton {
   static checked: (obj: any) => ReserveSigSingleton;
 }
 
-/**
- * Response to /reserve/status
- */
-@Checkable.Class()
-export class ReserveStatus {
-  /**
-   * Reserve signature.
-   */
-  @Checkable.String()
-  balance: string;
-
-  /**
-   * Reserve history, currently not used by the wallet.
-   */
-  @Checkable.Any()
-  history: any;
-
-  /**
-   * Create a ReserveSigSingleton from untyped JSON.
-   */
-  static checked: (obj: any) => ReserveStatus;
-}
 
 /**
  * Response of the merchant
@@ -942,3 +920,11 @@ export class TipPickupGetResponse {
    */
   static checked: (obj: any) => TipPickupGetResponse;
 }
+
+
+export type AmountString = string;
+export type Base32String = string;
+export type EddsaSignatureString = string;
+export type EddsaPublicKeyString = string;
+export type CoinPublicKeyString = string;
+export type TimestampString = string;
\ No newline at end of file
diff --git a/src/types/walletTypes.ts b/src/types/walletTypes.ts
index eedae6f2..df19d8dc 100644
--- a/src/types/walletTypes.ts
+++ b/src/types/walletTypes.ts
@@ -195,14 +195,30 @@ export interface WalletBalanceEntry {
   pendingIncomingDirty: AmountJson;
 }
 
+export interface CoinPayInfo {
+  /**
+   * Amount that will be subtracted from the coin when the payment is 
finalized.
+   */
+  subtractedAmount: AmountJson;
+
+  /**
+   * Public key of the coin that is being spent.
+   */
+  coinPub: string;
+
+  /**
+   * Signature together with the other information needed by the merchant,
+   * directly in the format expected by the merchant.
+   */
+  sig: CoinPaySig;
+}
+
 /**
  * Coins used for a payment, with signatures authorizing the payment and the
  * coins with remaining value updated to accomodate for a payment.
  */
-export interface PayCoinInfo {
-  originalCoins: CoinRecord[];
-  updatedCoins: CoinRecord[];
-  sigs: CoinPaySig[];
+export interface PaySigInfo {
+  coinInfo: CoinPayInfo[];
 }
 
 /**
diff --git a/src/util/amounts.ts b/src/util/amounts.ts
index 26cee7f8..c8fb7679 100644
--- a/src/util/amounts.ts
+++ b/src/util/amounts.ts
@@ -22,7 +22,6 @@
  * Imports.
  */
 import { Checkable } from "./checkable";
-import { objectCodec, numberCodec, stringCodec, Codec } from "./codec";
 
 /**
  * Number of fractional units that one value unit represents.
@@ -68,12 +67,6 @@ export class AmountJson {
   static checked: (obj: any) => AmountJson;
 }
 
-const amountJsonCodec: Codec<AmountJson> = objectCodec<AmountJson>()
-  .property("value", numberCodec)
-  .property("fraction", numberCodec)
-  .property("currency", stringCodec)
-  .build("AmountJson");
-
 /**
  * Result of a possibly overflowing operation.
  */
diff --git a/src/util/codec-test.ts b/src/util/codec-test.ts
index 22f6a0a9..7c7c93c7 100644
--- a/src/util/codec-test.ts
+++ b/src/util/codec-test.ts
@@ -19,13 +19,7 @@
  */
 
 import test from "ava";
-import {
-  stringCodec,
-  objectCodec,
-  unionCodec,
-  Codec,
-  stringConstCodec,
-} from "./codec";
+import { Codec, makeCodecForObject, makeCodecForConstString, codecForString, 
makeCodecForUnion } from "./codec";
 
 interface MyObj {
   foo: string;
@@ -44,8 +38,8 @@ interface AltTwo {
 type MyUnion = AltOne | AltTwo;
 
 test("basic codec", t => {
-  const myObjCodec = objectCodec<MyObj>()
-    .property("foo", stringCodec)
+  const myObjCodec = makeCodecForObject<MyObj>()
+    .property("foo", codecForString)
     .build("MyObj");
   const res = myObjCodec.decode({ foo: "hello" });
   t.assert(res.foo === "hello");
@@ -56,15 +50,15 @@ test("basic codec", t => {
 });
 
 test("union", t => {
-  const altOneCodec: Codec<AltOne> = objectCodec<AltOne>()
-    .property("type", stringConstCodec("one"))
-    .property("foo", stringCodec)
+  const altOneCodec: Codec<AltOne> = makeCodecForObject<AltOne>()
+    .property("type", makeCodecForConstString("one"))
+    .property("foo", codecForString)
     .build("AltOne");
-  const altTwoCodec: Codec<AltTwo> = objectCodec<AltTwo>()
-    .property("type", stringConstCodec("two"))
-    .property("bar", stringCodec)
+  const altTwoCodec: Codec<AltTwo> = makeCodecForObject<AltTwo>()
+    .property("type", makeCodecForConstString("two"))
+    .property("bar", codecForString)
     .build("AltTwo");
-  const myUnionCodec: Codec<MyUnion> = unionCodec<MyUnion>()
+  const myUnionCodec: Codec<MyUnion> = makeCodecForUnion<MyUnion>()
     .discriminateOn("type")
     .alternative("one", altOneCodec)
     .alternative("two", altTwoCodec)
diff --git a/src/util/codec.ts b/src/util/codec.ts
index 0215ce79..a13816c5 100644
--- a/src/util/codec.ts
+++ b/src/util/codec.ts
@@ -74,16 +74,16 @@ interface Alternative {
   codec: Codec<any>;
 }
 
-class ObjectCodecBuilder<T, TC> {
+class ObjectCodecBuilder<OutputType, PartialOutputType> {
   private propList: Prop[] = [];
 
   /**
    * Define a property for the object.
    */
-  property<K extends keyof T & string, V extends T[K]>(
+  property<K extends keyof OutputType & string, V extends OutputType[K]>(
     x: K,
     codec: Codec<V>,
-  ): ObjectCodecBuilder<T, TC & SingletonRecord<K, V>> {
+  ): ObjectCodecBuilder<OutputType, PartialOutputType & SingletonRecord<K, V>> 
{
     this.propList.push({ name: x, codec: codec });
     return this as any;
   }
@@ -94,10 +94,10 @@ class ObjectCodecBuilder<T, TC> {
    * @param objectDisplayName name of the object that this codec operates on,
    *   used in error messages.
    */
-  build(objectDisplayName: string): Codec<TC> {
+  build(objectDisplayName: string): Codec<PartialOutputType> {
     const propList = this.propList;
     return {
-      decode(x: any, c?: Context): TC {
+      decode(x: any, c?: Context): PartialOutputType {
         if (!c) {
           c = {
             path: [`(${objectDisplayName})`],
@@ -112,24 +112,37 @@ class ObjectCodecBuilder<T, TC> {
           );
           obj[prop.name] = propVal;
         }
-        return obj as TC;
+        return obj as PartialOutputType;
       },
     };
   }
 }
 
-class UnionCodecBuilder<T, D extends keyof T, B, TC> {
+class UnionCodecBuilder<
+  TargetType,
+  TagPropertyLabel extends keyof TargetType,
+  CommonBaseType,
+  PartialTargetType
+> {
   private alternatives = new Map<any, Alternative>();
 
-  constructor(private discriminator: D, private baseCodec?: Codec<B>) {}
+  constructor(
+    private discriminator: TagPropertyLabel,
+    private baseCodec?: Codec<CommonBaseType>,
+  ) {}
 
   /**
    * Define a property for the object.
    */
   alternative<V>(
-    tagValue: T[D],
+    tagValue: TargetType[TagPropertyLabel],
     codec: Codec<V>,
-  ): UnionCodecBuilder<T, D, B, TC | V> {
+  ): UnionCodecBuilder<
+    TargetType,
+    TagPropertyLabel,
+    CommonBaseType,
+    PartialTargetType | V
+  > {
     this.alternatives.set(tagValue, { codec, tagValue });
     return this as any;
   }
@@ -140,7 +153,9 @@ class UnionCodecBuilder<T, D extends keyof T, B, TC> {
    * @param objectDisplayName name of the object that this codec operates on,
    *   used in error messages.
    */
-  build<R extends TC & B>(objectDisplayName: string): Codec<R> {
+  build<R extends PartialTargetType & CommonBaseType = never>(
+    objectDisplayName: string,
+  ): Codec<R> {
     const alternatives = this.alternatives;
     const discriminator = this.discriminator;
     const baseCodec = this.baseCodec;
@@ -174,50 +189,50 @@ class UnionCodecBuilder<T, D extends keyof T, B, TC> {
   }
 }
 
+export class UnionCodecPreBuilder<T> {
+  discriminateOn<D extends keyof T, B = {}>(
+    discriminator: D,
+    baseCodec?: Codec<B>,
+  ): UnionCodecBuilder<T, D, B, never> {
+    return new UnionCodecBuilder<T, D, B, never>(discriminator, baseCodec);
+  }
+}
+
 /**
- * Return a codec for a value that must be a string.
+ * Return a builder for a codec that decodes an object with properties.
  */
-export const stringCodec: Codec<string> = {
-  decode(x: any, c?: Context): string {
-    if (typeof x === "string") {
-      return x;
-    }
-    throw new DecodingError(`expected string at ${renderContext(c)}`);
-  },
-};
+export function makeCodecForObject<T>(): ObjectCodecBuilder<T, {}> {
+  return new ObjectCodecBuilder<T, {}>();
+}
+
+export function makeCodecForUnion<T>(): UnionCodecPreBuilder<T> {
+  return new UnionCodecPreBuilder<T>();
+}
 
 /**
- * Return a codec for a value that must be a string.
+ * Return a codec for a mapping from a string to values described by the inner 
codec.
  */
-export function stringConstCodec<V extends string>(s: V): Codec<V> {
+export function makeCodecForMap<T>(
+  innerCodec: Codec<T>,
+): Codec<{ [x: string]: T }> {
   return {
-    decode(x: any, c?: Context): V {
-      if (x === s) {
-        return x;
+    decode(x: any, c?: Context): { [x: string]: T } {
+      const map: { [x: string]: T } = {};
+      if (typeof x !== "object") {
+        throw new DecodingError(`expected object at ${renderContext(c)}`);
       }
-      throw new DecodingError(
-        `expected string constant "${s}" at ${renderContext(c)}`,
-      );
+      for (const i in x) {
+        map[i] = innerCodec.decode(x[i], joinContext(c, `[${i}]`));
+      }
+      return map;
     },
   };
 }
 
-/**
- * Return a codec for a value that must be a number.
- */
-export const numberCodec: Codec<number> = {
-  decode(x: any, c?: Context): number {
-    if (typeof x === "number") {
-      return x;
-    }
-    throw new DecodingError(`expected number at ${renderContext(c)}`);
-  },
-};
-
 /**
  * Return a codec for a list, containing values described by the inner codec.
  */
-export function listCodec<T>(innerCodec: Codec<T>): Codec<T[]> {
+export function makeCodecForList<T>(innerCodec: Codec<T>): Codec<T[]> {
   return {
     decode(x: any, c?: Context): T[] {
       const arr: T[] = [];
@@ -233,39 +248,45 @@ export function listCodec<T>(innerCodec: Codec<T>): 
Codec<T[]> {
 }
 
 /**
- * Return a codec for a mapping from a string to values described by the inner 
codec.
+ * Return a codec for a value that must be a number.
  */
-export function mapCodec<T>(innerCodec: Codec<T>): Codec<{ [x: string]: T }> {
-  return {
-    decode(x: any, c?: Context): { [x: string]: T } {
-      const map: { [x: string]: T } = {};
-      if (typeof x !== "object") {
-        throw new DecodingError(`expected object at ${renderContext(c)}`);
-      }
-      for (const i in x) {
-        map[i] = innerCodec.decode(x[i], joinContext(c, `[${i}]`));
-      }
-      return map;
-    },
-  };
-}
+export const codecForNumber: Codec<number> = {
+  decode(x: any, c?: Context): number {
+    if (typeof x === "number") {
+      return x;
+    }
+    throw new DecodingError(`expected number at ${renderContext(c)}`);
+  },
+};
 
-export class UnionCodecPreBuilder<T> {
-  discriminateOn<D extends keyof T, B>(
-    discriminator: D,
-    baseCodec?: Codec<B>,
-  ): UnionCodecBuilder<T, D, B, never> {
-    return new UnionCodecBuilder<T, D, B, never>(discriminator, baseCodec);
-  }
-}
+/**
+ * Return a codec for a value that must be a string.
+ */
+export const codecForString: Codec<string> = {
+  decode(x: any, c?: Context): string {
+    if (typeof x === "string") {
+      return x;
+    }
+    throw new DecodingError(`expected string at ${renderContext(c)}`);
+  },
+};
 
 /**
- * Return a builder for a codec that decodes an object with properties.
+ * Return a codec for a value that must be a string.
  */
-export function objectCodec<T>(): ObjectCodecBuilder<T, {}> {
-  return new ObjectCodecBuilder<T, {}>();
+export function makeCodecForConstString<V extends string>(s: V): Codec<V> {
+  return {
+    decode(x: any, c?: Context): V {
+      if (x === s) {
+        return x;
+      }
+      throw new DecodingError(
+        `expected string constant "${s}" at ${renderContext(c)}`,
+      );
+    },
+  };
 }
 
-export function unionCodec<T>(): UnionCodecPreBuilder<T> {
-  return new UnionCodecPreBuilder<T>();
+export function typecheckedCodec<T = undefined>(c: Codec<T>): Codec<T> {
+  return c;
 }
diff --git a/src/util/helpers.ts b/src/util/helpers.ts
index 99d046f0..8136f44f 100644
--- a/src/util/helpers.ts
+++ b/src/util/helpers.ts
@@ -214,3 +214,13 @@ export function strcmp(s1: string, s2: string): number {
   }
   return 0;
 }
+
+/**
+ * Run a function and return its result.
+ * 
+ * Used as a nicer-looking way to do immediately invoked function
+ * expressions (IFFEs).
+ */
+export function runBlock<T>(f: () => T) {
+  return f();
+}
\ No newline at end of file
diff --git a/src/util/query.ts b/src/util/query.ts
index 08a8fec0..217c0674 100644
--- a/src/util/query.ts
+++ b/src/util/query.ts
@@ -176,6 +176,17 @@ class ResultStream<T> {
     return arr;
   }
 
+  async forEachAsync(f: (x: T) => Promise<void>): Promise<void> {
+    while (true) {
+      const x = await this.next();
+      if (x.hasValue) {
+        await f(x.value);
+      } else {
+        break;
+      }
+    }
+  }
+
   async forEach(f: (x: T) => void): Promise<void> {
     while (true) {
       const x = await this.next();
diff --git a/src/wallet.ts b/src/wallet.ts
index aca8a18a..3d28d089 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -24,9 +24,7 @@
  */
 import { CryptoWorkerFactory } from "./crypto/workers/cryptoApi";
 import { HttpRequestLibrary } from "./util/http";
-import {
-  Database
-} from "./util/query";
+import { Database } from "./util/query";
 
 import { AmountJson } from "./util/amounts";
 import * as Amounts from "./util/amounts";
@@ -99,10 +97,19 @@ import { payback } from "./operations/payback";
 import { TimerGroup } from "./util/timer";
 import { AsyncCondition } from "./util/promiseUtils";
 import { AsyncOpMemoSingle } from "./util/asyncMemo";
-import { PendingOperationInfo, PendingOperationsResponse, PendingOperationType 
} from "./types/pending";
+import {
+  PendingOperationInfo,
+  PendingOperationsResponse,
+  PendingOperationType,
+} from "./types/pending";
 import { WalletNotification, NotificationType } from "./types/notifications";
 import { HistoryQuery, HistoryEvent } from "./types/history";
-import { processPurchaseQueryRefund, processPurchaseApplyRefund, 
getFullRefundFees, applyRefund } from "./operations/refund";
+import {
+  processPurchaseQueryRefund,
+  processPurchaseApplyRefund,
+  getFullRefundFees,
+  applyRefund,
+} from "./operations/refund";
 
 /**
  * Wallet protocol version spoken with the exchange
@@ -184,11 +191,7 @@ export class Wallet {
         await updateExchangeFromUrl(this.ws, pending.exchangeBaseUrl, 
forceNow);
         break;
       case PendingOperationType.Refresh:
-        await processRefreshGroup(
-          this.ws,
-          pending.refreshGroupId,
-          forceNow,
-        );
+        await processRefreshGroup(this.ws, pending.refreshGroupId, forceNow);
         break;
       case PendingOperationType.Reserve:
         await processReserve(this.ws, pending.reservePub, forceNow);
@@ -203,9 +206,12 @@ export class Wallet {
       case PendingOperationType.ProposalChoice:
         // Nothing to do, user needs to accept/reject
         break;
-        case PendingOperationType.ProposalDownload:
+      case PendingOperationType.ProposalDownload:
         await processDownloadProposal(this.ws, pending.proposalId, forceNow);
         break;
+      case PendingOperationType.TipChoice:
+        // Nothing to do, user needs to accept/reject
+        break;
       case PendingOperationType.TipPickup:
         await processTip(this.ws, pending.tipId, forceNow);
         break;
@@ -470,9 +476,16 @@ export class Wallet {
 
   async refresh(oldCoinPub: string): Promise<void> {
     try {
-      const refreshGroupId = await 
this.db.runWithWriteTransaction([Stores.refreshGroups], async (tx) => {
-        return await createRefreshGroup(tx, [{ coinPub: oldCoinPub }], 
RefreshReason.Manual);
-      });
+      const refreshGroupId = await this.db.runWithWriteTransaction(
+        [Stores.refreshGroups],
+        async tx => {
+          return await createRefreshGroup(
+            tx,
+            [{ coinPub: oldCoinPub }],
+            RefreshReason.Manual,
+          );
+        },
+      );
       await processRefreshGroup(this.ws, refreshGroupId.refreshGroupId);
     } catch (e) {
       this.latch.trigger();
@@ -510,10 +523,9 @@ export class Wallet {
   }
 
   async getDenoms(exchangeUrl: string): Promise<DenominationRecord[]> {
-    const denoms = await this.db.iterIndex(
-      Stores.denominations.exchangeBaseUrlIndex,
-      exchangeUrl,
-    ).toArray();
+    const denoms = await this.db
+      .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeUrl)
+      .toArray();
     return denoms;
   }
 
@@ -536,15 +548,15 @@ export class Wallet {
   }
 
   async getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> {
-    return await this.db.iter(Stores.reserves).filter(
-      r => r.exchangeBaseUrl === exchangeBaseUrl,
-    );
+    return await this.db
+      .iter(Stores.reserves)
+      .filter(r => r.exchangeBaseUrl === exchangeBaseUrl);
   }
 
   async getCoinsForExchange(exchangeBaseUrl: string): Promise<CoinRecord[]> {
-    return await this.db.iter(Stores.coins).filter(
-      c => c.exchangeBaseUrl === exchangeBaseUrl,
-    );
+    return await this.db
+      .iter(Stores.coins)
+      .filter(c => c.exchangeBaseUrl === exchangeBaseUrl);
   }
 
   async getCoins(): Promise<CoinRecord[]> {
@@ -556,9 +568,7 @@ export class Wallet {
   }
 
   async getPaybackReserves(): Promise<ReserveRecord[]> {
-    return await this.db.iter(Stores.reserves).filter(
-      r => r.hasPayback,
-    );
+    return await this.db.iter(Stores.reserves).filter(r => r.hasPayback);
   }
 
   /**
@@ -691,9 +701,9 @@ export class Wallet {
     if (!purchase) {
       throw Error("unknown purchase");
     }
-    const refundsDoneAmounts = 
Object.values(purchase.refundState.refundsDone).map(x =>
-      Amounts.parseOrThrow(x.perm.refund_amount),
-    );
+    const refundsDoneAmounts = Object.values(
+      purchase.refundState.refundsDone,
+    ).map(x => Amounts.parseOrThrow(x.perm.refund_amount));
     const refundsPendingAmounts = Object.values(
       purchase.refundState.refundsPending,
     ).map(x => Amounts.parseOrThrow(x.perm.refund_amount));
@@ -701,12 +711,12 @@ export class Wallet {
       ...refundsDoneAmounts,
       ...refundsPendingAmounts,
     ]).amount;
-    const refundsDoneFees = 
Object.values(purchase.refundState.refundsDone).map(x =>
-      Amounts.parseOrThrow(x.perm.refund_amount),
-    );
-    const refundsPendingFees = 
Object.values(purchase.refundState.refundsPending).map(x =>
-      Amounts.parseOrThrow(x.perm.refund_amount),
-    );
+    const refundsDoneFees = Object.values(
+      purchase.refundState.refundsDone,
+    ).map(x => Amounts.parseOrThrow(x.perm.refund_amount));
+    const refundsPendingFees = Object.values(
+      purchase.refundState.refundsPending,
+    ).map(x => Amounts.parseOrThrow(x.perm.refund_amount));
     const totalRefundFees = Amounts.sum([
       ...refundsDoneFees,
       ...refundsPendingFees,
diff --git a/tsconfig.json b/tsconfig.json
index 81e529fa..ab2c42e1 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -60,6 +60,8 @@
     "src/operations/state.ts",
     "src/operations/tip.ts",
     "src/operations/withdraw.ts",
+    "src/types/ReserveStatus.ts",
+    "src/types/ReserveTransaction.ts",
     "src/types/dbTypes.ts",
     "src/types/history.ts",
     "src/types/notifications.ts",

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



reply via email to

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