gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (63cf4376 -> ef0acf06)


From: gnunet
Subject: [taler-wallet-core] branch master updated (63cf4376 -> ef0acf06)
Date: Thu, 02 Apr 2020 17:04:46 +0200

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

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

    from 63cf4376 quick fix for DEPOSIT->CREDIT rename
     new 62de27d2 helpers and tests for reserve reconciliation
     new 1728e501 split reconciliation and summary of reserve history
     new ef0acf06 model reserve history in the exchange, improve reserve 
handling logic

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/crypto/workers/cryptoApi.ts            |   4 +-
 src/crypto/workers/cryptoImplementation.ts |  12 +-
 src/headless/helpers.ts                    |   6 +-
 src/headless/integrationtest.ts            |  38 +--
 src/headless/taler-wallet-cli.ts           |  32 ++-
 src/operations/balance.ts                  |   4 +-
 src/operations/exchanges.ts                |   2 +-
 src/operations/history.ts                  |  46 ++--
 src/operations/pending.ts                  |   7 +-
 src/operations/recoup.ts                   |   4 +-
 src/operations/refresh.ts                  |   5 +-
 src/operations/refund.ts                   |   6 +-
 src/operations/reserves.ts                 | 235 ++++++++++--------
 src/operations/tip.ts                      |  22 +-
 src/operations/withdraw.ts                 | 202 ++++-----------
 src/types/ReserveTransaction.ts            |  22 +-
 src/types/dbTypes.ts                       | 138 ++++++++---
 src/types/history.ts                       |  16 +-
 src/types/notifications.ts                 |  28 +--
 src/types/pending.ts                       |   3 +-
 src/types/types-test.ts                    |  18 +-
 src/types/walletTypes.ts                   |   1 +
 src/util/amounts.ts                        |  20 +-
 src/util/helpers.ts                        |   5 +
 src/util/reserveHistoryUtil-test.ts        | 286 +++++++++++++++++++++
 src/util/reserveHistoryUtil.ts             | 384 +++++++++++++++++++++++++++++
 src/wallet.ts                              |  29 ++-
 src/webex/pages/popup.tsx                  |   4 -
 src/webex/pages/return-coins.tsx           |   4 +-
 tsconfig.json                              |   2 +
 30 files changed, 1150 insertions(+), 435 deletions(-)
 create mode 100644 src/util/reserveHistoryUtil-test.ts
 create mode 100644 src/util/reserveHistoryUtil.ts

diff --git a/src/crypto/workers/cryptoApi.ts b/src/crypto/workers/cryptoApi.ts
index ab97e127..d3b12e26 100644
--- a/src/crypto/workers/cryptoApi.ts
+++ b/src/crypto/workers/cryptoApi.ts
@@ -359,8 +359,8 @@ export class CryptoApi {
     return this.doRpc<string>("hashString", 1, str);
   }
 
-  hashDenomPub(denomPub: string): Promise<string> {
-    return this.doRpc<string>("hashDenomPub", 1, denomPub);
+  hashEncoded(encodedBytes: string): Promise<string> {
+    return this.doRpc<string>("hashEncoded", 1, encodedBytes);
   }
 
   isValidDenom(denom: DenominationRecord, masterPub: string): Promise<boolean> 
{
diff --git a/src/crypto/workers/cryptoImplementation.ts 
b/src/crypto/workers/cryptoImplementation.ts
index 156c72ba..eef8f595 100644
--- a/src/crypto/workers/cryptoImplementation.ts
+++ b/src/crypto/workers/cryptoImplementation.ts
@@ -49,8 +49,7 @@ import {
   PlanchetCreationRequest,
   DepositInfo,
 } from "../../types/walletTypes";
-import { AmountJson } from "../../util/amounts";
-import * as Amounts from "../../util/amounts";
+import { AmountJson, Amounts } from "../../util/amounts";
 import * as timer from "../../util/timer";
 import {
   encodeCrock,
@@ -199,6 +198,7 @@ export class CryptoImplementation {
       denomPubHash: encodeCrock(denomPubHash),
       reservePub: encodeCrock(reservePub),
       withdrawSig: encodeCrock(sig),
+      coinEvHash: encodeCrock(evHash),
     };
     return planchet;
   }
@@ -367,7 +367,7 @@ export class CryptoImplementation {
     const s: CoinDepositPermission = {
       coin_pub: depositInfo.coinPub,
       coin_sig: encodeCrock(coinSig),
-      contribution: Amounts.toString(depositInfo.spendAmount),
+      contribution: Amounts.stringify(depositInfo.spendAmount),
       denom_pub: depositInfo.denomPub,
       exchange_url: depositInfo.exchangeBaseUrl,
       ub_sig: depositInfo.denomSig,
@@ -491,10 +491,10 @@ export class CryptoImplementation {
   }
 
   /**
-   * Hash a denomination public key.
+   * Hash a crockford encoded value.
    */
-  hashDenomPub(denomPub: string): string {
-    return encodeCrock(hash(decodeCrock(denomPub)));
+  hashEncoded(encodedBytes: string): string {
+    return encodeCrock(hash(decodeCrock(encodedBytes)));
   }
 
   signCoinLink(
diff --git a/src/headless/helpers.ts b/src/headless/helpers.ts
index fb3d800d..92452e78 100644
--- a/src/headless/helpers.ts
+++ b/src/headless/helpers.ts
@@ -35,6 +35,7 @@ import { Database } from "../util/query";
 import { NodeHttpLib } from "./NodeHttpLib";
 import { Logger } from "../util/logging";
 import { SynchronousCryptoWorkerFactory } from 
"../crypto/workers/synchronousWorker";
+import { WithdrawalSourceType } from "../types/dbTypes";
 
 const logger = new Logger("helpers.ts");
 
@@ -165,8 +166,9 @@ export async function withdrawTestBalance(
     });
     myWallet.addNotificationListener((n) => {
       if (
-        n.type === NotificationType.ReserveDepleted &&
-        n.reservePub === reservePub
+        n.type === NotificationType.WithdrawGroupFinished &&
+        n.withdrawalSource.type === WithdrawalSourceType.Reserve &&
+        n.withdrawalSource.reservePub === reservePub
       ) {
         resolve();
       }
diff --git a/src/headless/integrationtest.ts b/src/headless/integrationtest.ts
index 191e48ff..6e45b76e 100644
--- a/src/headless/integrationtest.ts
+++ b/src/headless/integrationtest.ts
@@ -22,9 +22,9 @@ import { getDefaultNodeWallet, withdrawTestBalance } from 
"./helpers";
 import { MerchantBackendConnection } from "./merchant";
 import { Logger } from "../util/logging";
 import { NodeHttpLib } from "./NodeHttpLib";
-import * as Amounts from "../util/amounts";
 import { Wallet } from "../wallet";
 import { Configuration } from "../util/talerconfig";
+import { Amounts, AmountJson } from "../util/amounts";
 
 const logger = new Logger("integrationtest.ts");
 
@@ -127,31 +127,31 @@ export async function runIntegrationTest(args: 
IntegrationTestArgs) {
   await myWallet.runUntilDone();
 
   console.log("withdrawing test balance for refund");
-  const withdrawAmountTwo: Amounts.AmountJson = {
+  const withdrawAmountTwo: AmountJson = {
     currency,
     value: 18,
     fraction: 0,
   };
-  const spendAmountTwo: Amounts.AmountJson = {
+  const spendAmountTwo: AmountJson = {
     currency,
     value: 7,
     fraction: 0,
   };
 
-  const refundAmount: Amounts.AmountJson = {
+  const refundAmount: AmountJson = {
     currency,
     value: 6,
     fraction: 0,
   };
 
-  const spendAmountThree: Amounts.AmountJson = {
+  const spendAmountThree: AmountJson = {
     currency,
     value: 3,
     fraction: 0,
   };
   await withdrawTestBalance(
     myWallet,
-    Amounts.toString(withdrawAmountTwo),
+    Amounts.stringify(withdrawAmountTwo),
     args.bankBaseUrl,
     args.exchangeBaseUrl,
   );
@@ -162,14 +162,14 @@ export async function runIntegrationTest(args: 
IntegrationTestArgs) {
   let { orderId: refundOrderId } = await makePayment(
     myWallet,
     myMerchant,
-    Amounts.toString(spendAmountTwo),
+    Amounts.stringify(spendAmountTwo),
     "order that will be refunded",
   );
 
   const refundUri = await myMerchant.refund(
     refundOrderId,
     "test refund",
-    Amounts.toString(refundAmount),
+    Amounts.stringify(refundAmount),
   );
 
   console.log("refund URI", refundUri);
@@ -182,7 +182,7 @@ export async function runIntegrationTest(args: 
IntegrationTestArgs) {
   await makePayment(
     myWallet,
     myMerchant,
-    Amounts.toString(spendAmountThree),
+    Amounts.stringify(spendAmountThree),
     "payment after refund",
   );
 
@@ -240,7 +240,7 @@ export async function runIntegrationTestBasic(cfg: 
Configuration) {
   logger.info("withdrawing test balance");
   await withdrawTestBalance(
     myWallet,
-    Amounts.toString(parsedWithdrawAmount),
+    Amounts.stringify(parsedWithdrawAmount),
     bankBaseUrl,
     exchangeBaseUrl,
   );
@@ -258,7 +258,7 @@ export async function runIntegrationTestBasic(cfg: 
Configuration) {
   await makePayment(
     myWallet,
     myMerchant,
-    Amounts.toString(parsedSpendAmount),
+    Amounts.stringify(parsedSpendAmount),
     "hello world",
   );
 
@@ -266,24 +266,24 @@ export async function runIntegrationTestBasic(cfg: 
Configuration) {
   await myWallet.runUntilDone();
 
   console.log("withdrawing test balance for refund");
-  const withdrawAmountTwo: Amounts.AmountJson = {
+  const withdrawAmountTwo: AmountJson = {
     currency,
     value: 18,
     fraction: 0,
   };
-  const spendAmountTwo: Amounts.AmountJson = {
+  const spendAmountTwo: AmountJson = {
     currency,
     value: 7,
     fraction: 0,
   };
 
-  const refundAmount: Amounts.AmountJson = {
+  const refundAmount: AmountJson = {
     currency,
     value: 6,
     fraction: 0,
   };
 
-  const spendAmountThree: Amounts.AmountJson = {
+  const spendAmountThree: AmountJson = {
     currency,
     value: 3,
     fraction: 0,
@@ -291,7 +291,7 @@ export async function runIntegrationTestBasic(cfg: 
Configuration) {
 
   await withdrawTestBalance(
     myWallet,
-    Amounts.toString(withdrawAmountTwo),
+    Amounts.stringify(withdrawAmountTwo),
     bankBaseUrl,
     exchangeBaseUrl,
   );
@@ -302,14 +302,14 @@ export async function runIntegrationTestBasic(cfg: 
Configuration) {
   let { orderId: refundOrderId } = await makePayment(
     myWallet,
     myMerchant,
-    Amounts.toString(spendAmountTwo),
+    Amounts.stringify(spendAmountTwo),
     "order that will be refunded",
   );
 
   const refundUri = await myMerchant.refund(
     refundOrderId,
     "test refund",
-    Amounts.toString(refundAmount),
+    Amounts.stringify(refundAmount),
   );
 
   console.log("refund URI", refundUri);
@@ -322,7 +322,7 @@ export async function runIntegrationTestBasic(cfg: 
Configuration) {
   await makePayment(
     myWallet,
     myMerchant,
-    Amounts.toString(spendAmountThree),
+    Amounts.stringify(spendAmountThree),
     "payment after refund",
   );
 
diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
index 45ab819a..d183ef31 100644
--- a/src/headless/taler-wallet-cli.ts
+++ b/src/headless/taler-wallet-cli.ts
@@ -24,7 +24,7 @@ import qrcodeGenerator = require("qrcode-generator");
 import * as clk from "./clk";
 import { BridgeIDBFactory, MemoryBackend } from "idb-bridge";
 import { Logger } from "../util/logging";
-import * as Amounts from "../util/amounts";
+import { Amounts } from "../util/amounts";
 import { decodeCrock } from "../crypto/talerCrypto";
 import { OperationFailedAndReportedError } from "../operations/errors";
 import { Bank } from "./bank";
@@ -190,7 +190,7 @@ walletCli
       } else {
         const currencies = Object.keys(balance.byCurrency).sort();
         for (const c of currencies) {
-          console.log(Amounts.toString(balance.byCurrency[c].available));
+          console.log(Amounts.stringify(balance.byCurrency[c].available));
         }
       }
     });
@@ -356,6 +356,32 @@ advancedCli
     fs.writeFileSync(1, decodeCrock(enc.trim()));
   });
 
+const reservesCli = advancedCli.subcommand("reserves", "reserves", {
+  help: "Manage reserves.",
+});
+
+reservesCli
+  .subcommand("list", "list", {
+    help: "List reserves.",
+  })
+  .action(async (args) => {
+    await withWallet(args, async (wallet) => {
+      const reserves = await wallet.getReserves();
+      console.log(JSON.stringify(reserves, undefined, 2));
+    });
+  });
+
+reservesCli
+  .subcommand("update", "update", {
+    help: "Update reserve status via exchange.",
+  })
+  .requiredArgument("reservePub", clk.STRING)
+  .action(async (args) => {
+    await withWallet(args, async (wallet) => {
+      await wallet.updateReserve(args.update.reservePub);
+    });
+  });
+
 advancedCli
   .subcommand("payPrepare", "pay-prepare", {
     help: "Claim an order but don't pay yet.",
@@ -464,7 +490,7 @@ advancedCli
         console.log(` exchange ${coin.exchangeBaseUrl}`);
         console.log(` denomPubHash ${coin.denomPubHash}`);
         console.log(
-          ` remaining amount ${Amounts.toString(coin.currentAmount)}`,
+          ` remaining amount ${Amounts.stringify(coin.currentAmount)}`,
         );
       }
     });
diff --git a/src/operations/balance.ts b/src/operations/balance.ts
index 8858e8b4..7c2d0e3f 100644
--- a/src/operations/balance.ts
+++ b/src/operations/balance.ts
@@ -106,7 +106,7 @@ export async function getBalancesInsideTransaction(
     }
   });
 
-  await tx.iter(Stores.withdrawalSession).forEach((wds) => {
+  await tx.iter(Stores.withdrawalGroups).forEach((wds) => {
     let w = wds.totalCoinValue;
     for (let i = 0; i < wds.planchets.length; i++) {
       if (wds.withdrawn[i]) {
@@ -150,7 +150,7 @@ export async function getBalances(
       Stores.refreshGroups,
       Stores.reserves,
       Stores.purchases,
-      Stores.withdrawalSession,
+      Stores.withdrawalGroups,
     ],
     async (tx) => {
       return getBalancesInsideTransaction(ws, tx);
diff --git a/src/operations/exchanges.ts b/src/operations/exchanges.ts
index 3aaf7746..b9806bb6 100644
--- a/src/operations/exchanges.ts
+++ b/src/operations/exchanges.ts
@@ -53,7 +53,7 @@ async function denominationRecordFromKeys(
   exchangeBaseUrl: string,
   denomIn: Denomination,
 ): Promise<DenominationRecord> {
-  const denomPubHash = await ws.cryptoApi.hashDenomPub(denomIn.denom_pub);
+  const denomPubHash = await ws.cryptoApi.hashEncoded(denomIn.denom_pub);
   const d: DenominationRecord = {
     denomPub: denomIn.denom_pub,
     denomPubHash,
diff --git a/src/operations/history.ts b/src/operations/history.ts
index 1b417252..84873933 100644
--- a/src/operations/history.ts
+++ b/src/operations/history.ts
@@ -26,7 +26,7 @@ import {
   PlanchetRecord,
   CoinRecord,
 } from "../types/dbTypes";
-import * as Amounts from "../util/amounts";
+import { Amounts } from "../util/amounts";
 import { AmountJson } from "../util/amounts";
 import {
   HistoryQuery,
@@ -42,6 +42,7 @@ import {
 import { assertUnreachable } from "../util/assertUnreachable";
 import { TransactionHandle, Store } from "../util/query";
 import { timestampCmp } from "../util/time";
+import { summarizeReserveHistory } from "../util/reserveHistoryUtil";
 
 /**
  * Create an event ID from the type and the primary key for the event.
@@ -58,7 +59,7 @@ function getOrderShortInfo(
     return undefined;
   }
   return {
-    amount: Amounts.toString(download.contractData.amount),
+    amount: Amounts.stringify(download.contractData.amount),
     fulfillmentUrl: download.contractData.fulfillmentUrl,
     orderId: download.contractData.orderId,
     merchantBaseUrl: download.contractData.merchantBaseUrl,
@@ -176,7 +177,7 @@ export async function getHistory(
       Stores.refreshGroups,
       Stores.reserves,
       Stores.tips,
-      Stores.withdrawalSession,
+      Stores.withdrawalGroups,
       Stores.payEvents,
       Stores.refundEvents,
       Stores.reserveUpdatedEvents,
@@ -208,7 +209,7 @@ export async function getHistory(
         });
       });
 
-      tx.iter(Stores.withdrawalSession).forEach((wsr) => {
+      tx.iter(Stores.withdrawalGroups).forEach((wsr) => {
         if (wsr.timestampFinish) {
           const cs: PlanchetRecord[] = [];
           wsr.planchets.forEach((x) => {
@@ -221,7 +222,7 @@ export async function getHistory(
           if (historyQuery?.extraDebug) {
             verboseDetails = {
               coins: cs.map((x) => ({
-                value: Amounts.toString(x.coinValue),
+                value: Amounts.stringify(x.coinValue),
                 denomPub: x.denomPub,
               })),
             };
@@ -229,13 +230,13 @@ export async function getHistory(
 
           history.push({
             type: HistoryEventType.Withdrawn,
-            withdrawSessionId: wsr.withdrawSessionId,
+            withdrawalGroupId: wsr.withdrawalGroupId,
             eventId: makeEventId(
               HistoryEventType.Withdrawn,
-              wsr.withdrawSessionId,
+              wsr.withdrawalGroupId,
             ),
-            amountWithdrawnEffective: Amounts.toString(wsr.totalCoinValue),
-            amountWithdrawnRaw: Amounts.toString(wsr.rawWithdrawalAmount),
+            amountWithdrawnEffective: Amounts.stringify(wsr.totalCoinValue),
+            amountWithdrawnRaw: Amounts.stringify(wsr.rawWithdrawalAmount),
             exchangeBaseUrl: wsr.exchangeBaseUrl,
             timestamp: wsr.timestampFinish,
             withdrawalSource: wsr.source,
@@ -283,7 +284,7 @@ export async function getHistory(
             coins.push({
               contribution: x.contribution,
               denomPub: c.denomPub,
-              value: Amounts.toString(d.value),
+              value: Amounts.stringify(d.value),
             });
           }
           verboseDetails = { coins };
@@ -301,7 +302,7 @@ export async function getHistory(
           sessionId: pe.sessionId,
           timestamp: pe.timestamp,
           numCoins: purchase.payReq.coins.length,
-          amountPaidWithFees: Amounts.toString(amountPaidWithFees),
+          amountPaidWithFees: Amounts.stringify(amountPaidWithFees),
           verboseDetails,
         });
       });
@@ -364,7 +365,7 @@ export async function getHistory(
               }
               outputCoins.push({
                 denomPub: d.denomPub,
-                value: Amounts.toString(d.value),
+                value: Amounts.stringify(d.value),
               });
             }
           }
@@ -378,8 +379,8 @@ export async function getHistory(
           eventId: makeEventId(HistoryEventType.Refreshed, rg.refreshGroupId),
           timestamp: rg.timestampFinished,
           refreshReason: rg.reason,
-          amountRefreshedEffective: Amounts.toString(amountRefreshedEffective),
-          amountRefreshedRaw: Amounts.toString(amountRefreshedRaw),
+          amountRefreshedEffective: 
Amounts.stringify(amountRefreshedEffective),
+          amountRefreshedRaw: Amounts.stringify(amountRefreshedRaw),
           numInputCoins,
           numOutputCoins,
           numRefreshedInputCoins,
@@ -403,21 +404,22 @@ export async function getHistory(
             type: ReserveType.Manual,
           };
         }
+        const s = summarizeReserveHistory(reserve.reserveTransactions, 
reserve.currency);
         history.push({
           type: HistoryEventType.ReserveBalanceUpdated,
           eventId: makeEventId(
             HistoryEventType.ReserveBalanceUpdated,
             ru.reserveUpdateId,
           ),
-          amountExpected: ru.amountExpected,
-          amountReserveBalance: ru.amountReserveBalance,
           timestamp: ru.timestamp,
-          newHistoryTransactions: ru.newHistoryTransactions,
           reserveShortInfo: {
             exchangeBaseUrl: reserve.exchangeBaseUrl,
             reserveCreationDetail,
             reservePub: reserve.reservePub,
           },
+          reserveAwaitedAmount: Amounts.stringify(s.awaitedReserveAmount),
+          reserveBalance: Amounts.stringify(s.computedReserveBalance),
+          reserveUnclaimedAmount: Amounts.stringify(s.unclaimedReserveAmount),
         });
       });
 
@@ -428,7 +430,7 @@ export async function getHistory(
             eventId: makeEventId(HistoryEventType.TipAccepted, tip.tipId),
             timestamp: tip.acceptedTimestamp,
             tipId: tip.tipId,
-            tipAmountRaw: Amounts.toString(tip.amount),
+            tipAmountRaw: Amounts.stringify(tip.amount),
           });
         }
       });
@@ -488,9 +490,9 @@ export async function getHistory(
           refundGroupId: re.refundGroupId,
           orderShortInfo,
           timestamp: re.timestamp,
-          amountRefundedEffective: Amounts.toString(amountRefundedEffective),
-          amountRefundedRaw: Amounts.toString(amountRefundedRaw),
-          amountRefundedInvalid: Amounts.toString(amountRefundedInvalid),
+          amountRefundedEffective: Amounts.stringify(amountRefundedEffective),
+          amountRefundedRaw: Amounts.stringify(amountRefundedRaw),
+          amountRefundedInvalid: Amounts.stringify(amountRefundedInvalid),
         });
       });
 
@@ -499,7 +501,7 @@ export async function getHistory(
           let verboseDetails: any = undefined;
           if (historyQuery?.extraDebug) {
             verboseDetails = {
-              oldAmountPerCoin: rg.oldAmountPerCoin.map(Amounts.toString),
+              oldAmountPerCoin: rg.oldAmountPerCoin.map(Amounts.stringify),
             };
           }
 
diff --git a/src/operations/pending.ts b/src/operations/pending.ts
index adf47b15..b0bb9a7c 100644
--- a/src/operations/pending.ts
+++ b/src/operations/pending.ts
@@ -243,7 +243,7 @@ async function gatherWithdrawalPending(
   resp: PendingOperationsResponse,
   onlyDue: boolean = false,
 ): Promise<void> {
-  await tx.iter(Stores.withdrawalSession).forEach((wsr) => {
+  await tx.iter(Stores.withdrawalGroups).forEach((wsr) => {
     if (wsr.timestampFinish) {
       return;
     }
@@ -266,7 +266,8 @@ async function gatherWithdrawalPending(
       numCoinsTotal,
       numCoinsWithdrawn,
       source: wsr.source,
-      withdrawSessionId: wsr.withdrawSessionId,
+      withdrawalGroupId: wsr.withdrawalGroupId,
+      lastError: wsr.lastError,
     });
   });
 }
@@ -444,7 +445,7 @@ export async function getPendingOperations(
       Stores.reserves,
       Stores.refreshGroups,
       Stores.coins,
-      Stores.withdrawalSession,
+      Stores.withdrawalGroups,
       Stores.proposals,
       Stores.tips,
       Stores.purchases,
diff --git a/src/operations/recoup.ts b/src/operations/recoup.ts
index 4c6eaf3b..e13ae3c1 100644
--- a/src/operations/recoup.ts
+++ b/src/operations/recoup.ts
@@ -42,7 +42,7 @@ import { codecForRecoupConfirmation } from 
"../types/talerTypes";
 import { NotificationType } from "../types/notifications";
 import { forceQueryReserve } from "./reserves";
 
-import * as Amounts from "../util/amounts";
+import { Amounts } from "../util/amounts";
 import { createRefreshGroup, processRefreshGroup } from "./refresh";
 import { RefreshReason, OperationError } from "../types/walletTypes";
 import { TransactionHandle } from "../util/query";
@@ -266,7 +266,7 @@ async function recoupRefreshCoin(
       ).amount;
       console.log(
         "recoup: setting old coin amount to",
-        Amounts.toString(oldCoin.currentAmount),
+        Amounts.stringify(oldCoin.currentAmount),
       );
       recoupGroup.scheduleRefreshCoins.push(oldCoin.coinPub);
       await tx.put(Stores.coins, revokedCoin);
diff --git a/src/operations/refresh.ts b/src/operations/refresh.ts
index 5628263e..be4f5c5a 100644
--- a/src/operations/refresh.ts
+++ b/src/operations/refresh.ts
@@ -14,8 +14,7 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { AmountJson } from "../util/amounts";
-import * as Amounts from "../util/amounts";
+import { Amounts, AmountJson } from "../util/amounts";
 import {
   DenominationRecord,
   Stores,
@@ -239,7 +238,7 @@ async function refreshMelt(
     denom_pub_hash: coin.denomPubHash,
     denom_sig: coin.denomSig,
     rc: refreshSession.hash,
-    value_with_fee: Amounts.toString(refreshSession.amountRefreshInput),
+    value_with_fee: Amounts.stringify(refreshSession.amountRefreshInput),
   };
   logger.trace(`melt request for coin:`, meltReq);
   const resp = await ws.http.postJson(reqUrl.href, meltReq);
diff --git a/src/operations/refund.ts b/src/operations/refund.ts
index 7552fc11..f0fec406 100644
--- a/src/operations/refund.ts
+++ b/src/operations/refund.ts
@@ -41,7 +41,7 @@ import {
 import { NotificationType } from "../types/notifications";
 import { parseRefundUri } from "../util/taleruri";
 import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
-import * as Amounts from "../util/amounts";
+import { Amounts } from "../util/amounts";
 import {
   MerchantRefundPermission,
   MerchantRefundResponse,
@@ -476,7 +476,7 @@ async function processPurchaseApplyRefundImpl(
           `commiting refund ${perm.merchant_sig} to coin ${c.coinPub}`,
         );
         logger.trace(
-          `coin amount before is ${Amounts.toString(c.currentAmount)}`,
+          `coin amount before is ${Amounts.stringify(c.currentAmount)}`,
         );
         logger.trace(`refund amount (via merchant) is ${perm.refund_amount}`);
         logger.trace(`refund fee (via merchant) is ${perm.refund_fee}`);
@@ -486,7 +486,7 @@ async function processPurchaseApplyRefundImpl(
         c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
         c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
         logger.trace(
-          `coin amount after is ${Amounts.toString(c.currentAmount)}`,
+          `coin amount after is ${Amounts.stringify(c.currentAmount)}`,
         );
         await tx.put(Stores.coins, c);
       };
diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts
index 5cf189d3..2ef902ef 100644
--- a/src/operations/reserves.ts
+++ b/src/operations/reserves.ts
@@ -28,14 +28,17 @@ import {
   ReserveRecord,
   CurrencyRecord,
   Stores,
-  WithdrawalSessionRecord,
+  WithdrawalGroupRecord,
   initRetryInfo,
   updateRetryInfoTimeout,
   ReserveUpdatedEventRecord,
+  WalletReserveHistoryItemType,
+  DenominationRecord,
+  PlanchetRecord,
+  WithdrawalSourceType,
 } from "../types/dbTypes";
-import { TransactionAbort } from "../util/query";
 import { Logger } from "../util/logging";
-import * as Amounts from "../util/amounts";
+import { Amounts } from "../util/amounts";
 import {
   updateExchangeFromUrl,
   getExchangeTrust,
@@ -50,7 +53,7 @@ import { encodeCrock, getRandomBytes } from 
"../crypto/talerCrypto";
 import { randomBytes } from "../crypto/primitives/nacl-fast";
 import {
   getVerifiedWithdrawDenomList,
-  processWithdrawSession,
+  processWithdrawGroup,
   getBankWithdrawalInfo,
 } from "./withdraw";
 import {
@@ -61,6 +64,10 @@ import {
 import { NotificationType } from "../types/notifications";
 import { codecForReserveStatus } from "../types/ReserveStatus";
 import { getTimestampNow } from "../util/time";
+import {
+  reconcileReserveHistory,
+  summarizeReserveHistory,
+} from "../util/reserveHistoryUtil";
 
 const logger = new Logger("reserves.ts");
 
@@ -98,11 +105,7 @@ export async function createReserve(
 
   const reserveRecord: ReserveRecord = {
     timestampCreated: now,
-    amountWithdrawAllocated: Amounts.getZero(currency),
-    amountWithdrawCompleted: Amounts.getZero(currency),
-    amountWithdrawRemaining: Amounts.getZero(currency),
     exchangeBaseUrl: canonExchange,
-    amountInitiallyRequested: req.amount,
     reservePriv: keypair.priv,
     reservePub: keypair.pub,
     senderWire: req.senderWire,
@@ -115,8 +118,14 @@ export async function createReserve(
     retryInfo: initRetryInfo(),
     lastError: undefined,
     reserveTransactions: [],
+    currency: req.amount.currency,
   };
 
+  reserveRecord.reserveTransactions.push({
+    type: WalletReserveHistoryItemType.Credit,
+    expectedAmount: req.amount,
+  });
+
   const senderWire = req.senderWire;
   if (senderWire) {
     const rec = {
@@ -460,6 +469,7 @@ async function updateReserve(
   const respJson = await resp.json();
   const reserveInfo = codecForReserveStatus().decode(respJson);
   const balance = Amounts.parseOrThrow(reserveInfo.balance);
+  const currency = balance.currency;
   await ws.db.runWithWriteTransaction(
     [Stores.reserves, Stores.reserveUpdatedEvents],
     async (tx) => {
@@ -477,60 +487,41 @@ async function updateReserve(
 
       const reserveUpdateId = encodeCrock(getRandomBytes(32));
 
-      // FIXME: check / compare history!
-      if (!r.lastSuccessfulStatusQuery) {
-        // FIXME: check if this matches initial expectations
-        r.amountWithdrawRemaining = balance;
+      const reconciled = reconcileReserveHistory(
+        r.reserveTransactions,
+        reserveInfo.history,
+      );
+
+      console.log("reconciled history:", JSON.stringify(reconciled, undefined, 
2));
+
+      const summary = summarizeReserveHistory(
+        reconciled.updatedLocalHistory,
+        currency,
+      );
+      console.log("summary", summary);
+
+      if (
+        reconciled.newAddedItems.length + reconciled.newMatchedItems.length !=
+        0
+      ) {
         const reserveUpdate: ReserveUpdatedEventRecord = {
           reservePub: r.reservePub,
           timestamp: getTimestampNow(),
-          amountReserveBalance: Amounts.toString(balance),
-          amountExpected: Amounts.toString(reserve.amountInitiallyRequested),
+          amountReserveBalance: Amounts.stringify(balance),
+          amountExpected: Amounts.stringify(summary.awaitedReserveAmount),
           newHistoryTransactions,
           reserveUpdateId,
         };
         await tx.put(Stores.reserveUpdatedEvents, reserveUpdate);
         r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
+        r.retryInfo = initRetryInfo();
       } else {
-        const expectedBalance = Amounts.add(
-          r.amountWithdrawRemaining,
-          Amounts.sub(r.amountWithdrawAllocated, r.amountWithdrawCompleted)
-            .amount,
-        );
-        const cmp = Amounts.cmp(balance, expectedBalance.amount);
-        if (cmp == 0) {
-          // Nothing changed, go back to sleep!
-          r.reserveStatus = ReserveRecordStatus.DORMANT;
-        } else if (cmp > 0) {
-          const extra = Amounts.sub(balance, expectedBalance.amount).amount;
-          r.amountWithdrawRemaining = Amounts.add(
-            r.amountWithdrawRemaining,
-            extra,
-          ).amount;
-          r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
-        } else {
-          // We're missing some money.
-          r.reserveStatus = ReserveRecordStatus.DORMANT;
-        }
-        if (r.reserveStatus !== ReserveRecordStatus.DORMANT) {
-          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();
-      if (r.reserveStatus == ReserveRecordStatus.DORMANT) {
+        r.reserveStatus = ReserveRecordStatus.DORMANT;
         r.retryInfo = initRetryInfo(false);
-      } else {
-        r.retryInfo = initRetryInfo();
       }
-      r.reserveTransactions = reserveInfo.history;
+      r.lastSuccessfulStatusQuery = getTimestampNow();
+      r.reserveTransactions = reconciled.updatedLocalHistory;
+      r.lastError = undefined;
       await tx.put(Stores.reserves, r);
     },
   );
@@ -607,6 +598,33 @@ export async function confirmReserve(
   });
 }
 
+async function makePlanchet(
+  ws: InternalWalletState,
+  reserve: ReserveRecord,
+  denom: DenominationRecord,
+): Promise<PlanchetRecord> {
+  const r = await ws.cryptoApi.createPlanchet({
+    denomPub: denom.denomPub,
+    feeWithdraw: denom.feeWithdraw,
+    reservePriv: reserve.reservePriv,
+    reservePub: reserve.reservePub,
+    value: denom.value,
+  });
+  return {
+    blindingKey: r.blindingKey,
+    coinEv: r.coinEv,
+    coinPriv: r.coinPriv,
+    coinPub: r.coinPub,
+    coinValue: r.coinValue,
+    denomPub: r.denomPub,
+    denomPubHash: r.denomPubHash,
+    isFromTip: false,
+    reservePub: r.reservePub,
+    withdrawSig: r.withdrawSig,
+    coinEvHash: r.coinEvHash,
+  };
+}
+
 /**
  * Withdraw coins from a reserve until it is empty.
  *
@@ -626,7 +644,12 @@ async function depleteReserve(
   }
   logger.trace(`depleting reserve ${reservePub}`);
 
-  const withdrawAmount = reserve.amountWithdrawRemaining;
+  const summary = summarizeReserveHistory(
+    reserve.reserveTransactions,
+    reserve.currency,
+  );
+
+  const withdrawAmount = summary.unclaimedReserveAmount;
 
   logger.trace(`getting denom list`);
 
@@ -637,36 +660,47 @@ async function depleteReserve(
   );
   logger.trace(`got denom list`);
   if (denomsForWithdraw.length === 0) {
-    const m = `Unable to withdraw from reserve, no denominations are available 
to withdraw.`;
-    const opErr = {
-      type: "internal",
-      message: m,
-      details: {},
-    };
-    await incrementReserveRetry(ws, reserve.reservePub, opErr);
-    console.log(m);
-    throw new OperationFailedAndReportedError(opErr);
+    // Only complain about inability to withdraw if we
+    // didn't withdraw before.
+    if (Amounts.isZero(summary.withdrawnAmount)) {
+      const m = `Unable to withdraw from reserve, no denominations are 
available to withdraw.`;
+      const opErr = {
+        type: "internal",
+        message: m,
+        details: {},
+      };
+      await incrementReserveRetry(ws, reserve.reservePub, opErr);
+      console.log(m);
+      throw new OperationFailedAndReportedError(opErr);
+    }
+    return;
   }
 
   logger.trace("selected denominations");
 
-  const withdrawalSessionId = encodeCrock(randomBytes(32));
+  const withdrawalGroupId = encodeCrock(randomBytes(32));
 
   const totalCoinValue = Amounts.sum(denomsForWithdraw.map((x) => x.value))
     .amount;
 
-  const withdrawalRecord: WithdrawalSessionRecord = {
-    withdrawSessionId: withdrawalSessionId,
+  const planchets: PlanchetRecord[] = [];
+  for (const d of denomsForWithdraw) {
+    const p = await makePlanchet(ws, reserve, d);
+    planchets.push(p);
+  }
+
+  const withdrawalRecord: WithdrawalGroupRecord = {
+    withdrawalGroupId: withdrawalGroupId,
     exchangeBaseUrl: reserve.exchangeBaseUrl,
     source: {
-      type: "reserve",
+      type: WithdrawalSourceType.Reserve,
       reservePub: reserve.reservePub,
     },
     rawWithdrawalAmount: withdrawAmount,
     timestampStart: getTimestampNow(),
     denoms: denomsForWithdraw.map((x) => x.denomPub),
     withdrawn: denomsForWithdraw.map((x) => false),
-    planchets: denomsForWithdraw.map((x) => undefined),
+    planchets,
     totalCoinValue,
     retryInfo: initRetryInfo(),
     lastErrorPerCoin: {},
@@ -679,53 +713,54 @@ async function depleteReserve(
   const totalWithdrawAmount = Amounts.add(totalCoinValue, totalCoinWithdrawFee)
     .amount;
 
-  function mutateReserve(r: ReserveRecord): ReserveRecord {
-    const remaining = Amounts.sub(
-      r.amountWithdrawRemaining,
-      totalWithdrawAmount,
-    );
-    if (remaining.saturated) {
-      console.error("can't create planchets, saturated");
-      throw TransactionAbort;
-    }
-    const allocated = Amounts.add(
-      r.amountWithdrawAllocated,
-      totalWithdrawAmount,
-    );
-    if (allocated.saturated) {
-      console.error("can't create planchets, saturated");
-      throw TransactionAbort;
-    }
-    r.amountWithdrawRemaining = remaining.amount;
-    r.amountWithdrawAllocated = allocated.amount;
-    r.reserveStatus = ReserveRecordStatus.DORMANT;
-    r.retryInfo = initRetryInfo(false);
-    return r;
-  }
-
   const success = await ws.db.runWithWriteTransaction(
-    [Stores.withdrawalSession, Stores.reserves],
+    [Stores.withdrawalGroups, Stores.reserves],
     async (tx) => {
-      const myReserve = await tx.get(Stores.reserves, reservePub);
-      if (!myReserve) {
+      const newReserve = await tx.get(Stores.reserves, reservePub);
+      if (!newReserve) {
         return false;
       }
-      if (myReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
+      if (newReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
         return false;
       }
-      await tx.mutate(Stores.reserves, reserve.reservePub, mutateReserve);
-      await tx.put(Stores.withdrawalSession, withdrawalRecord);
+      const newSummary = summarizeReserveHistory(
+        newReserve.reserveTransactions,
+        newReserve.currency,
+      );
+      if (
+        Amounts.cmp(newSummary.unclaimedReserveAmount, totalWithdrawAmount) < 0
+      ) {
+        // Something must have happened concurrently!
+        logger.error(
+          "aborting withdrawal session, likely concurrent withdrawal happened",
+        );
+        return false;
+      }
+      for (let i = 0; i < planchets.length; i++) {
+        const amt = Amounts.add(
+          denomsForWithdraw[i].value,
+          denomsForWithdraw[i].feeWithdraw,
+        ).amount;
+        newReserve.reserveTransactions.push({
+          type: WalletReserveHistoryItemType.Withdraw,
+          expectedAmount: amt,
+        });
+      }
+      newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
+      newReserve.retryInfo = initRetryInfo(false);
+      await tx.put(Stores.reserves, newReserve);
+      await tx.put(Stores.withdrawalGroups, withdrawalRecord);
       return true;
     },
   );
 
   if (success) {
-    console.log("processing new withdraw session");
+    console.log("processing new withdraw group");
     ws.notify({
-      type: NotificationType.WithdrawSessionCreated,
-      withdrawSessionId: withdrawalSessionId,
+      type: NotificationType.WithdrawGroupCreated,
+      withdrawalGroupId: withdrawalGroupId,
     });
-    await processWithdrawSession(ws, withdrawalSessionId);
+    await processWithdrawGroup(ws, withdrawalGroupId);
   } else {
     console.trace("withdraw session already existed");
   }
diff --git a/src/operations/tip.ts b/src/operations/tip.ts
index 3636dd24..d3c98d28 100644
--- a/src/operations/tip.ts
+++ b/src/operations/tip.ts
@@ -28,14 +28,15 @@ import * as Amounts from "../util/amounts";
 import {
   Stores,
   PlanchetRecord,
-  WithdrawalSessionRecord,
+  WithdrawalGroupRecord,
   initRetryInfo,
   updateRetryInfoTimeout,
+  WithdrawalSourceType,
 } from "../types/dbTypes";
 import {
   getExchangeWithdrawalInfo,
   getVerifiedWithdrawDenomList,
-  processWithdrawSession,
+  processWithdrawGroup,
 } from "./withdraw";
 import { updateExchangeFromUrl } from "./exchanges";
 import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
@@ -246,8 +247,10 @@ async function processTipImpl(
 
   const planchets: PlanchetRecord[] = [];
 
+
   for (let i = 0; i < tipRecord.planchets.length; i++) {
     const tipPlanchet = tipRecord.planchets[i];
+    const coinEvHash = await ws.cryptoApi.hashEncoded(tipPlanchet.coinEv);
     const planchet: PlanchetRecord = {
       blindingKey: tipPlanchet.blindingKey,
       coinEv: tipPlanchet.coinEv,
@@ -259,22 +262,23 @@ async function processTipImpl(
       reservePub: response.reserve_pub,
       withdrawSig: response.reserve_sigs[i].reserve_sig,
       isFromTip: true,
+      coinEvHash,
     };
     planchets.push(planchet);
   }
 
-  const withdrawalSessionId = encodeCrock(getRandomBytes(32));
+  const withdrawalGroupId = encodeCrock(getRandomBytes(32));
 
-  const withdrawalSession: WithdrawalSessionRecord = {
+  const withdrawalGroup: WithdrawalGroupRecord = {
     denoms: planchets.map((x) => x.denomPub),
     exchangeBaseUrl: tipRecord.exchangeUrl,
     planchets: planchets,
     source: {
-      type: "tip",
+      type: WithdrawalSourceType.Tip,
       tipId: tipRecord.tipId,
     },
     timestampStart: getTimestampNow(),
-    withdrawSessionId: withdrawalSessionId,
+    withdrawalGroupId: withdrawalGroupId,
     rawWithdrawalAmount: tipRecord.amount,
     withdrawn: planchets.map((x) => false),
     totalCoinValue: Amounts.sum(planchets.map((p) => p.coinValue)).amount,
@@ -285,7 +289,7 @@ async function processTipImpl(
   };
 
   await ws.db.runWithWriteTransaction(
-    [Stores.tips, Stores.withdrawalSession],
+    [Stores.tips, Stores.withdrawalGroups],
     async (tx) => {
       const tr = await tx.get(Stores.tips, tipId);
       if (!tr) {
@@ -298,11 +302,11 @@ async function processTipImpl(
       tr.retryInfo = initRetryInfo(false);
 
       await tx.put(Stores.tips, tr);
-      await tx.put(Stores.withdrawalSession, withdrawalSession);
+      await tx.put(Stores.withdrawalGroups, withdrawalGroup);
     },
   );
 
-  await processWithdrawSession(ws, withdrawalSessionId);
+  await processWithdrawGroup(ws, withdrawalGroupId);
 
   return;
 }
diff --git a/src/operations/withdraw.ts b/src/operations/withdraw.ts
index 4d8af9fc..48d70db2 100644
--- a/src/operations/withdraw.ts
+++ b/src/operations/withdraw.ts
@@ -52,6 +52,7 @@ import {
   timestampCmp,
   timestampSubtractDuraction,
 } from "../util/time";
+import { summarizeReserveHistory, ReserveHistorySummary } from 
"../util/reserveHistoryUtil";
 
 const logger = new Logger("withdraw.ts");
 
@@ -158,29 +159,29 @@ async function getPossibleDenoms(
  */
 async function processPlanchet(
   ws: InternalWalletState,
-  withdrawalSessionId: string,
+  withdrawalGroupId: string,
   coinIdx: number,
 ): Promise<void> {
-  const withdrawalSession = await ws.db.get(
-    Stores.withdrawalSession,
-    withdrawalSessionId,
+  const withdrawalGroup = await ws.db.get(
+    Stores.withdrawalGroups,
+    withdrawalGroupId,
   );
-  if (!withdrawalSession) {
+  if (!withdrawalGroup) {
     return;
   }
-  if (withdrawalSession.withdrawn[coinIdx]) {
+  if (withdrawalGroup.withdrawn[coinIdx]) {
     return;
   }
-  if (withdrawalSession.source.type === "reserve") {
+  if (withdrawalGroup.source.type === "reserve") {
   }
-  const planchet = withdrawalSession.planchets[coinIdx];
+  const planchet = withdrawalGroup.planchets[coinIdx];
   if (!planchet) {
     console.log("processPlanchet: planchet not found");
     return;
   }
   const exchange = await ws.db.get(
     Stores.exchanges,
-    withdrawalSession.exchangeBaseUrl,
+    withdrawalGroup.exchangeBaseUrl,
   );
   if (!exchange) {
     console.error("db inconsistent: exchange for planchet not found");
@@ -188,7 +189,7 @@ async function processPlanchet(
   }
 
   const denom = await ws.db.get(Stores.denominations, [
-    withdrawalSession.exchangeBaseUrl,
+    withdrawalGroup.exchangeBaseUrl,
     planchet.denomPub,
   ]);
 
@@ -232,24 +233,24 @@ async function processPlanchet(
     denomPub: planchet.denomPub,
     denomPubHash: planchet.denomPubHash,
     denomSig,
-    exchangeBaseUrl: withdrawalSession.exchangeBaseUrl,
+    exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
     status: CoinStatus.Fresh,
     coinSource: {
       type: CoinSourceType.Withdraw,
       coinIndex: coinIdx,
       reservePub: planchet.reservePub,
-      withdrawSessionId: withdrawalSessionId,
+      withdrawalGroupId: withdrawalGroupId,
     },
     suspended: false,
   };
 
-  let withdrawSessionFinished = false;
-  let reserveDepleted = false;
+  let withdrawalGroupFinished = false;
+  let summary: ReserveHistorySummary | undefined = undefined;
 
   const success = await ws.db.runWithWriteTransaction(
-    [Stores.coins, Stores.withdrawalSession, Stores.reserves],
+    [Stores.coins, Stores.withdrawalGroups, Stores.reserves],
     async (tx) => {
-      const ws = await tx.get(Stores.withdrawalSession, withdrawalSessionId);
+      const ws = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
       if (!ws) {
         return false;
       }
@@ -269,23 +270,13 @@ async function processPlanchet(
         ws.timestampFinish = getTimestampNow();
         ws.lastError = undefined;
         ws.retryInfo = initRetryInfo(false);
-        withdrawSessionFinished = true;
+        withdrawalGroupFinished = true;
       }
-      await tx.put(Stores.withdrawalSession, ws);
+      await tx.put(Stores.withdrawalGroups, ws);
       if (!planchet.isFromTip) {
         const r = await tx.get(Stores.reserves, planchet.reservePub);
         if (r) {
-          r.amountWithdrawCompleted = Amounts.add(
-            r.amountWithdrawCompleted,
-            Amounts.add(denom.value, denom.feeWithdraw).amount,
-          ).amount;
-          if (
-            Amounts.cmp(r.amountWithdrawCompleted, r.amountWithdrawAllocated) 
==
-            0
-          ) {
-            reserveDepleted = true;
-          }
-          await tx.put(Stores.reserves, r);
+          summary = summarizeReserveHistory(r.reserveTransactions, r.currency);
         }
       }
       await tx.add(Stores.coins, coin);
@@ -299,17 +290,10 @@ async function processPlanchet(
     });
   }
 
-  if (withdrawSessionFinished) {
+  if (withdrawalGroupFinished) {
     ws.notify({
-      type: NotificationType.WithdrawSessionFinished,
-      withdrawSessionId: withdrawalSessionId,
-    });
-  }
-
-  if (reserveDepleted && withdrawalSession.source.type === "reserve") {
-    ws.notify({
-      type: NotificationType.ReserveDepleted,
-      reservePub: withdrawalSession.source.reservePub,
+      type: NotificationType.WithdrawGroupFinished,
+      withdrawalSource: withdrawalGroup.source,
     });
   }
 }
@@ -383,113 +367,15 @@ export async function getVerifiedWithdrawDenomList(
   return selectedDenoms;
 }
 
-async function makePlanchet(
-  ws: InternalWalletState,
-  withdrawalSessionId: string,
-  coinIndex: number,
-): Promise<void> {
-  const withdrawalSession = await ws.db.get(
-    Stores.withdrawalSession,
-    withdrawalSessionId,
-  );
-  if (!withdrawalSession) {
-    return;
-  }
-  const src = withdrawalSession.source;
-  if (src.type !== "reserve") {
-    throw Error("invalid state");
-  }
-  const reserve = await ws.db.get(Stores.reserves, src.reservePub);
-  if (!reserve) {
-    return;
-  }
-  const denom = await ws.db.get(Stores.denominations, [
-    withdrawalSession.exchangeBaseUrl,
-    withdrawalSession.denoms[coinIndex],
-  ]);
-  if (!denom) {
-    return;
-  }
-  const r = await ws.cryptoApi.createPlanchet({
-    denomPub: denom.denomPub,
-    feeWithdraw: denom.feeWithdraw,
-    reservePriv: reserve.reservePriv,
-    reservePub: reserve.reservePub,
-    value: denom.value,
-  });
-  const newPlanchet: PlanchetRecord = {
-    blindingKey: r.blindingKey,
-    coinEv: r.coinEv,
-    coinPriv: r.coinPriv,
-    coinPub: r.coinPub,
-    coinValue: r.coinValue,
-    denomPub: r.denomPub,
-    denomPubHash: r.denomPubHash,
-    isFromTip: false,
-    reservePub: r.reservePub,
-    withdrawSig: r.withdrawSig,
-  };
-  await ws.db.runWithWriteTransaction(
-    [Stores.withdrawalSession],
-    async (tx) => {
-      const myWs = await tx.get(Stores.withdrawalSession, withdrawalSessionId);
-      if (!myWs) {
-        return;
-      }
-      if (myWs.planchets[coinIndex]) {
-        return;
-      }
-      myWs.planchets[coinIndex] = newPlanchet;
-      await tx.put(Stores.withdrawalSession, myWs);
-    },
-  );
-}
-
-async function processWithdrawCoin(
-  ws: InternalWalletState,
-  withdrawalSessionId: string,
-  coinIndex: number,
-) {
-  logger.trace("starting withdraw for coin", coinIndex);
-  const withdrawalSession = await ws.db.get(
-    Stores.withdrawalSession,
-    withdrawalSessionId,
-  );
-  if (!withdrawalSession) {
-    console.log("ws doesn't exist");
-    return;
-  }
-
-  const planchet = withdrawalSession.planchets[coinIndex];
-
-  if (planchet) {
-    const coin = await ws.db.get(Stores.coins, planchet.coinPub);
-
-    if (coin) {
-      console.log("coin already exists");
-      return;
-    }
-  }
-
-  if (!withdrawalSession.planchets[coinIndex]) {
-    const key = `${withdrawalSessionId}-${coinIndex}`;
-    await ws.memoMakePlanchet.memo(key, async () => {
-      logger.trace("creating planchet for coin", coinIndex);
-      return makePlanchet(ws, withdrawalSessionId, coinIndex);
-    });
-  }
-  await processPlanchet(ws, withdrawalSessionId, coinIndex);
-}
-
 async function incrementWithdrawalRetry(
   ws: InternalWalletState,
-  withdrawalSessionId: string,
+  withdrawalGroupId: string,
   err: OperationError | undefined,
 ): Promise<void> {
   await ws.db.runWithWriteTransaction(
-    [Stores.withdrawalSession],
+    [Stores.withdrawalGroups],
     async (tx) => {
-      const wsr = await tx.get(Stores.withdrawalSession, withdrawalSessionId);
+      const wsr = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
       if (!wsr) {
         return;
       }
@@ -499,30 +385,30 @@ async function incrementWithdrawalRetry(
       wsr.retryInfo.retryCounter++;
       updateRetryInfoTimeout(wsr.retryInfo);
       wsr.lastError = err;
-      await tx.put(Stores.withdrawalSession, wsr);
+      await tx.put(Stores.withdrawalGroups, wsr);
     },
   );
   ws.notify({ type: NotificationType.WithdrawOperationError });
 }
 
-export async function processWithdrawSession(
+export async function processWithdrawGroup(
   ws: InternalWalletState,
-  withdrawalSessionId: string,
+  withdrawalGroupId: string,
   forceNow: boolean = false,
 ): Promise<void> {
   const onOpErr = (e: OperationError) =>
-    incrementWithdrawalRetry(ws, withdrawalSessionId, e);
+    incrementWithdrawalRetry(ws, withdrawalGroupId, e);
   await guardOperationException(
-    () => processWithdrawSessionImpl(ws, withdrawalSessionId, forceNow),
+    () => processWithdrawGroupImpl(ws, withdrawalGroupId, forceNow),
     onOpErr,
   );
 }
 
-async function resetWithdrawSessionRetry(
+async function resetWithdrawalGroupRetry(
   ws: InternalWalletState,
-  withdrawalSessionId: string,
+  withdrawalGroupId: string,
 ) {
-  await ws.db.mutate(Stores.withdrawalSession, withdrawalSessionId, (x) => {
+  await ws.db.mutate(Stores.withdrawalGroups, withdrawalGroupId, (x) => {
     if (x.retryInfo.active) {
       x.retryInfo = initRetryInfo();
     }
@@ -530,26 +416,26 @@ async function resetWithdrawSessionRetry(
   });
 }
 
-async function processWithdrawSessionImpl(
+async function processWithdrawGroupImpl(
   ws: InternalWalletState,
-  withdrawalSessionId: string,
+  withdrawalGroupId: string,
   forceNow: boolean,
 ): Promise<void> {
-  logger.trace("processing withdraw session", withdrawalSessionId);
+  logger.trace("processing withdraw group", withdrawalGroupId);
   if (forceNow) {
-    await resetWithdrawSessionRetry(ws, withdrawalSessionId);
+    await resetWithdrawalGroupRetry(ws, withdrawalGroupId);
   }
-  const withdrawalSession = await ws.db.get(
-    Stores.withdrawalSession,
-    withdrawalSessionId,
+  const withdrawalGroup = await ws.db.get(
+    Stores.withdrawalGroups,
+    withdrawalGroupId,
   );
-  if (!withdrawalSession) {
+  if (!withdrawalGroup) {
     logger.trace("withdraw session doesn't exist");
     return;
   }
 
-  const ps = withdrawalSession.denoms.map((d, i) =>
-    processWithdrawCoin(ws, withdrawalSessionId, i),
+  const ps = withdrawalGroup.denoms.map((d, i) =>
+    processPlanchet(ws, withdrawalGroupId, i),
   );
   await Promise.all(ps);
   return;
diff --git a/src/types/ReserveTransaction.ts b/src/types/ReserveTransaction.ts
index 3b2553de..cebccd2d 100644
--- a/src/types/ReserveTransaction.ts
+++ b/src/types/ReserveTransaction.ts
@@ -39,7 +39,7 @@ import { Timestamp, codecForTimestamp } from "../util/time";
 
 export const enum ReserveTransactionType {
   Withdraw = "WITHDRAW",
-  Deposit = "CREDIT",
+  Credit = "CREDIT",
   Recoup = "RECOUP",
   Closing = "CLOSING",
 }
@@ -74,8 +74,8 @@ export interface ReserveWithdrawTransaction {
   withdraw_fee: AmountString;
 }
 
-export interface ReserveDepositTransaction {
-  type: ReserveTransactionType.Deposit;
+export interface ReserveCreditTransaction {
+  type: ReserveTransactionType.Credit;
 
   /**
    * Amount withdrawn.
@@ -175,7 +175,7 @@ export interface ReserveRecoupTransaction {
  */
 export type ReserveTransaction =
   | ReserveWithdrawTransaction
-  | ReserveDepositTransaction
+  | ReserveCreditTransaction
   | ReserveClosingTransaction
   | ReserveRecoupTransaction;
 
@@ -194,15 +194,15 @@ export const codecForReserveWithdrawTransaction = () =>
       .build("ReserveWithdrawTransaction"),
   );
 
-export const codecForReserveDepositTransaction = () =>
-  typecheckedCodec<ReserveDepositTransaction>(
-    makeCodecForObject<ReserveDepositTransaction>()
+export const codecForReserveCreditTransaction = () =>
+  typecheckedCodec<ReserveCreditTransaction>(
+    makeCodecForObject<ReserveCreditTransaction>()
       .property("amount", codecForString)
       .property("sender_account_url", codecForString)
       .property("timestamp", codecForTimestamp)
       .property("wire_reference", codecForString)
-      .property("type", 
makeCodecForConstString(ReserveTransactionType.Deposit))
-      .build("ReserveDepositTransaction"),
+      .property("type", makeCodecForConstString(ReserveTransactionType.Credit))
+      .build("ReserveCreditTransaction"),
   );
 
 export const codecForReserveClosingTransaction = () =>
@@ -248,8 +248,8 @@ export const codecForReserveTransaction = () =>
         codecForReserveRecoupTransaction(),
       )
       .alternative(
-        ReserveTransactionType.Deposit,
-        codecForReserveDepositTransaction(),
+        ReserveTransactionType.Credit,
+        codecForReserveCreditTransaction(),
       )
       .build<ReserveTransaction>("ReserveTransaction"),
   );
diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts
index db71db71..b87ada11 100644
--- a/src/types/dbTypes.ts
+++ b/src/types/dbTypes.ts
@@ -35,8 +35,15 @@ import {
 
 import { Index, Store } from "../util/query";
 import { OperationError, RefreshReason } from "./walletTypes";
-import { ReserveTransaction } from "./ReserveTransaction";
+import {
+  ReserveTransaction,
+  ReserveCreditTransaction,
+  ReserveWithdrawTransaction,
+  ReserveClosingTransaction,
+  ReserveRecoupTransaction,
+} from "./ReserveTransaction";
 import { Timestamp, Duration, getTimestampNow } from "../util/time";
+import { Wallet } from "../wallet";
 
 export enum ReserveRecordStatus {
   /**
@@ -131,6 +138,79 @@ export function initRetryInfo(
   return info;
 }
 
+export const enum WalletReserveHistoryItemType {
+  Credit = "credit",
+  Withdraw = "withdraw",
+  Closing = "closing",
+  Recoup = "recoup",
+}
+
+export interface WalletReserveHistoryCreditItem {
+  type: WalletReserveHistoryItemType.Credit;
+
+  /**
+   * Amount we expect to see credited.
+   */
+  expectedAmount?: AmountJson;
+
+  /**
+   * Item from the reserve transaction history that this
+   * wallet reserve history item matches up with.
+   */
+  matchedExchangeTransaction?: ReserveCreditTransaction;
+}
+
+export interface WalletReserveHistoryWithdrawItem {
+  expectedAmount?: AmountJson;
+
+  /**
+   * Hash of the blinded coin.
+   * 
+   * When this value is set, it indicates that a withdrawal is active
+   * in the wallet for the 
+   */
+  expectedCoinEvHash?: string;
+
+  type: WalletReserveHistoryItemType.Withdraw;
+
+  /**
+   * Item from the reserve transaction history that this
+   * wallet reserve history item matches up with.
+   */
+  matchedExchangeTransaction?: ReserveWithdrawTransaction;
+}
+
+export interface WalletReserveHistoryClosingItem {
+  type: WalletReserveHistoryItemType.Closing;
+
+  /**
+   * Item from the reserve transaction history that this
+   * wallet reserve history item matches up with.
+   */
+  matchedExchangeTransaction?: ReserveClosingTransaction;
+}
+
+export interface WalletReserveHistoryRecoupItem {
+  type: WalletReserveHistoryItemType.Recoup;
+
+  /**
+   * Amount we expect to see recouped.
+   */
+  expectedAmount?: AmountJson;
+
+  /**
+   * Item from the reserve transaction history that this
+   * wallet reserve history item matches up with.
+   */
+  matchedExchangeTransaction?: ReserveRecoupTransaction;
+}
+
+export type WalletReserveHistoryItem =
+  | WalletReserveHistoryCreditItem
+  | WalletReserveHistoryWithdrawItem
+  | WalletReserveHistoryRecoupItem
+  | WalletReserveHistoryClosingItem;
+
 /**
  * A reserve record as stored in the wallet's database.
  */
@@ -150,6 +230,11 @@ export interface ReserveRecord {
    */
   exchangeBaseUrl: string;
 
+  /**
+   * Currency of the reserve.
+   */
+  currency: string;
+
   /**
    * Time when the reserve was created.
    */
@@ -165,34 +250,13 @@ export interface ReserveRecord {
   timestampReserveInfoPosted: Timestamp | undefined;
 
   /**
-   * Time when the reserve was confirmed.
+   * Time when the reserve was confirmed, either manually by the user
+   * or by the bank.
    *
-   * Set to 0 if not confirmed yet.
+   * Set to undefined if not confirmed yet.
    */
   timestampConfirmed: Timestamp | undefined;
 
-  /**
-   * Amount that's still available for withdrawing
-   * from this reserve.
-   */
-  amountWithdrawRemaining: AmountJson;
-
-  /**
-   * Amount allocated for withdrawing.
-   * The corresponding withdraw operation may or may not
-   * have been completed yet.
-   */
-  amountWithdrawAllocated: AmountJson;
-
-  amountWithdrawCompleted: AmountJson;
-
-  /**
-   * Amount requested when the reserve was created.
-   * When a reserve is re-used (rare!)  the current_amount can
-   * be higher than the requested_amount
-   */
-  amountInitiallyRequested: AmountJson;
-
   /**
    * Wire information (as payto URI) for the bank account that
    * transfered funds for this reserve.
@@ -233,7 +297,7 @@ export interface ReserveRecord {
    */
   lastError: OperationError | undefined;
 
-  reserveTransactions: ReserveTransaction[];
+  reserveTransactions: WalletReserveHistoryItem[];
 }
 
 /**
@@ -555,6 +619,7 @@ export interface PlanchetRecord {
   blindingKey: string;
   withdrawSig: string;
   coinEv: string;
+  coinEvHash: string;
   coinValue: AmountJson;
   isFromTip: boolean;
 }
@@ -603,7 +668,7 @@ export const enum CoinSourceType {
 
 export interface WithdrawCoinSource {
   type: CoinSourceType.Withdraw;
-  withdrawSessionId: string;
+  withdrawalGroupId: string;
 
   /**
    * Index of the coin in the withdrawal session.
@@ -1290,20 +1355,25 @@ export interface CoinsReturnRecord {
   wire: any;
 }
 
+export const enum WithdrawalSourceType {
+  Tip = "tip",
+  Reserve = "reserve",
+}
+
 export interface WithdrawalSourceTip {
-  type: "tip";
+  type: WithdrawalSourceType.Tip;
   tipId: string;
 }
 
 export interface WithdrawalSourceReserve {
-  type: "reserve";
+  type: WithdrawalSourceType.Reserve;
   reservePub: string;
 }
 
 export type WithdrawalSource = WithdrawalSourceTip | WithdrawalSourceReserve;
 
-export interface WithdrawalSessionRecord {
-  withdrawSessionId: string;
+export interface WithdrawalGroupRecord {
+  withdrawalGroupId: string;
 
   /**
    * Withdrawal source.  Fields that don't apply to the respective
@@ -1564,9 +1634,9 @@ export namespace Stores {
     }
   }
 
-  class WithdrawalSessionsStore extends Store<WithdrawalSessionRecord> {
+  class WithdrawalGroupsStore extends Store<WithdrawalGroupRecord> {
     constructor() {
-      super("withdrawals", { keyPath: "withdrawSessionId" });
+      super("withdrawals", { keyPath: "withdrawalGroupId" });
     }
   }
 
@@ -1625,7 +1695,7 @@ export namespace Stores {
   export const purchases = new PurchasesStore();
   export const tips = new TipsStore();
   export const senderWires = new SenderWiresStore();
-  export const withdrawalSession = new WithdrawalSessionsStore();
+  export const withdrawalGroups = new WithdrawalGroupsStore();
   export const bankWithdrawUris = new BankWithdrawUrisStore();
   export const refundEvents = new RefundEventsStore();
   export const payEvents = new PayEventsStore();
diff --git a/src/types/history.ts b/src/types/history.ts
index f4f3872c..8179f626 100644
--- a/src/types/history.ts
+++ b/src/types/history.ts
@@ -119,8 +119,6 @@ export interface HistoryReserveBalanceUpdatedEvent {
    */
   timestamp: Timestamp;
 
-  newHistoryTransactions: ReserveTransaction[];
-
   /**
    * Condensed information about the reserve.
    */
@@ -129,13 +127,17 @@ export interface HistoryReserveBalanceUpdatedEvent {
   /**
    * Amount currently left in the reserve.
    */
-  amountReserveBalance: string;
+  reserveBalance: string;
+
+  /**
+   * Amount we still expect to be added to the reserve.
+   */
+  reserveAwaitedAmount: string;
 
   /**
-   * Amount we expected to be in the reserve at that time,
-   * considering ongoing withdrawals from that reserve.
+   * Amount that hasn't been withdrawn yet.
    */
-  amountExpected: string;
+  reserveUnclaimedAmount: string;
 }
 
 /**
@@ -612,7 +614,7 @@ export interface HistoryWithdrawnEvent {
    * Unique identifier for the withdrawal session, can be used to
    * query more detailed information from the wallet.
    */
-  withdrawSessionId: string;
+  withdrawalGroupId: string;
 
   withdrawalSource: WithdrawalSource;
 
diff --git a/src/types/notifications.ts b/src/types/notifications.ts
index 39930dcc..05d3c273 100644
--- a/src/types/notifications.ts
+++ b/src/types/notifications.ts
@@ -1,4 +1,5 @@
 import { OperationError } from "./walletTypes";
+import { WithdrawCoinSource, WithdrawalSource } from "./dbTypes";
 
 /*
  This file is part of GNU Taler
@@ -34,10 +35,9 @@ export const enum NotificationType {
   RefreshUnwarranted = "refresh-unwarranted",
   ReserveUpdated = "reserve-updated",
   ReserveConfirmed = "reserve-confirmed",
-  ReserveDepleted = "reserve-depleted",
   ReserveCreated = "reserve-created",
-  WithdrawSessionCreated = "withdraw-session-created",
-  WithdrawSessionFinished = "withdraw-session-finished",
+  WithdrawGroupCreated = "withdraw-group-created",
+  WithdrawGroupFinished = "withdraw-group-finished",
   WaitingForRetry = "waiting-for-retry",
   RefundStarted = "refund-started",
   RefundQueried = "refund-queried",
@@ -114,19 +114,14 @@ export interface ReserveConfirmedNotification {
   type: NotificationType.ReserveConfirmed;
 }
 
-export interface WithdrawSessionCreatedNotification {
-  type: NotificationType.WithdrawSessionCreated;
-  withdrawSessionId: string;
+export interface WithdrawalGroupCreatedNotification {
+  type: NotificationType.WithdrawGroupCreated;
+  withdrawalGroupId: string;
 }
 
-export interface WithdrawSessionFinishedNotification {
-  type: NotificationType.WithdrawSessionFinished;
-  withdrawSessionId: string;
-}
-
-export interface ReserveDepletedNotification {
-  type: NotificationType.ReserveDepleted;
-  reservePub: string;
+export interface WithdrawalGroupFinishedNotification {
+  type: NotificationType.WithdrawGroupFinished;
+  withdrawalSource: WithdrawalSource;
 }
 
 export interface WaitingForRetryNotification {
@@ -210,13 +205,12 @@ export type WalletNotification =
   | ReserveUpdatedNotification
   | ReserveCreatedNotification
   | ReserveConfirmedNotification
-  | WithdrawSessionFinishedNotification
-  | ReserveDepletedNotification
+  | WithdrawalGroupFinishedNotification
   | WaitingForRetryNotification
   | RefundStartedNotification
   | RefundFinishedNotification
   | RefundQueriedNotification
-  | WithdrawSessionCreatedNotification
+  | WithdrawalGroupCreatedNotification
   | CoinWithdrawnNotification
   | WildcardNotification
   | RecoupOperationErrorNotification;
diff --git a/src/types/pending.ts b/src/types/pending.ts
index d9d17a3b..1471fa19 100644
--- a/src/types/pending.ts
+++ b/src/types/pending.ts
@@ -214,7 +214,8 @@ export interface PendingRecoupOperation {
 export interface PendingWithdrawOperation {
   type: PendingOperationType.Withdraw;
   source: WithdrawalSource;
-  withdrawSessionId: string;
+  lastError: OperationError | undefined;
+  withdrawalGroupId: string;
   numCoinsWithdrawn: number;
   numCoinsTotal: number;
 }
diff --git a/src/types/types-test.ts b/src/types/types-test.ts
index 885371a1..ce309249 100644
--- a/src/types/types-test.ts
+++ b/src/types/types-test.ts
@@ -15,14 +15,14 @@
  */
 
 import test from "ava";
-import * as Amounts from "../util/amounts";
-import { ContractTerms, codecForContractTerms } from "./talerTypes";
+import { Amounts, AmountJson } from "../util/amounts";
+import { codecForContractTerms } from "./talerTypes";
 
 const amt = (
   value: number,
   fraction: number,
   currency: string,
-): Amounts.AmountJson => ({ value, fraction, currency });
+): AmountJson => ({ value, fraction, currency });
 
 test("amount addition (simple)", (t) => {
   const a1 = amt(1, 0, "EUR");
@@ -118,13 +118,13 @@ test("amount parsing", (t) => {
 });
 
 test("amount stringification", (t) => {
-  t.is(Amounts.toString(amt(0, 0, "TESTKUDOS")), "TESTKUDOS:0");
-  t.is(Amounts.toString(amt(4, 94000000, "TESTKUDOS")), "TESTKUDOS:4.94");
-  t.is(Amounts.toString(amt(0, 10000000, "TESTKUDOS")), "TESTKUDOS:0.1");
-  t.is(Amounts.toString(amt(0, 1, "TESTKUDOS")), "TESTKUDOS:0.00000001");
-  t.is(Amounts.toString(amt(5, 0, "TESTKUDOS")), "TESTKUDOS:5");
+  t.is(Amounts.stringify(amt(0, 0, "TESTKUDOS")), "TESTKUDOS:0");
+  t.is(Amounts.stringify(amt(4, 94000000, "TESTKUDOS")), "TESTKUDOS:4.94");
+  t.is(Amounts.stringify(amt(0, 10000000, "TESTKUDOS")), "TESTKUDOS:0.1");
+  t.is(Amounts.stringify(amt(0, 1, "TESTKUDOS")), "TESTKUDOS:0.00000001");
+  t.is(Amounts.stringify(amt(5, 0, "TESTKUDOS")), "TESTKUDOS:5");
   // denormalized
-  t.is(Amounts.toString(amt(1, 100000000, "TESTKUDOS")), "TESTKUDOS:2");
+  t.is(Amounts.stringify(amt(1, 100000000, "TESTKUDOS")), "TESTKUDOS:2");
   t.pass();
 });
 
diff --git a/src/types/walletTypes.ts b/src/types/walletTypes.ts
index 7b58ba50..5d28c5ae 100644
--- a/src/types/walletTypes.ts
+++ b/src/types/walletTypes.ts
@@ -427,6 +427,7 @@ export interface PlanchetCreationResult {
   withdrawSig: string;
   coinEv: string;
   coinValue: AmountJson;
+  coinEvHash: string;
 }
 
 export interface PlanchetCreationRequest {
diff --git a/src/util/amounts.ts b/src/util/amounts.ts
index 8deeaecc..aee7b12b 100644
--- a/src/util/amounts.ts
+++ b/src/util/amounts.ts
@@ -299,7 +299,7 @@ export function fromFloat(floatVal: number, currency: 
string) {
  * Convert to standard human-readable string representation that's
  * also used in JSON formats.
  */
-export function toString(a: AmountJson): string {
+export function stringify(a: AmountJson): string {
   const av = a.value + Math.floor(a.fraction / fractionalBase);
   const af = a.fraction % fractionalBase;
   let s = av.toString();
@@ -322,7 +322,7 @@ export function toString(a: AmountJson): string {
 /**
  * Check if the argument is a valid amount in string form.
  */
-export function check(a: any): boolean {
+function check(a: any): boolean {
   if (typeof a !== "string") {
     return false;
   }
@@ -333,3 +333,19 @@ export function check(a: any): boolean {
     return false;
   }
 }
+
+// Export all amount-related functions here for better IDE experience.
+export const Amounts = {
+  stringify: stringify,
+  parse: parse,
+  parseOrThrow: parseOrThrow,
+  cmp: cmp,
+  add: add,
+  sum: sum,
+  sub: sub,
+  check: check,
+  getZero: getZero,
+  isZero: isZero,
+  maxAmountValue: maxAmountValue,
+  fromFloat: fromFloat,
+};
\ No newline at end of file
diff --git a/src/util/helpers.ts b/src/util/helpers.ts
index 130dcdae..0e19d7ab 100644
--- a/src/util/helpers.ts
+++ b/src/util/helpers.ts
@@ -102,6 +102,11 @@ export function deepEquals(x: any, y: any): boolean {
   );
 }
 
+export function deepCopy(x: any): any {
+  // FIXME: this has many issues ...
+  return JSON.parse(JSON.stringify(x));
+}
+
 /**
  * Map from a collection to a list or results and then
  * concatenate the results.
diff --git a/src/util/reserveHistoryUtil-test.ts 
b/src/util/reserveHistoryUtil-test.ts
new file mode 100644
index 00000000..910d6a01
--- /dev/null
+++ b/src/util/reserveHistoryUtil-test.ts
@@ -0,0 +1,286 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import test from "ava";
+import {
+  reconcileReserveHistory,
+  summarizeReserveHistory,
+} from "./reserveHistoryUtil";
+import {
+  WalletReserveHistoryItem,
+  WalletReserveHistoryItemType,
+} from "../types/dbTypes";
+import {
+  ReserveTransaction,
+  ReserveTransactionType,
+} from "../types/ReserveTransaction";
+import { Amounts } from "./amounts";
+
+test("basics", (t) => {
+  const r = reconcileReserveHistory([], []);
+  t.deepEqual(r.updatedLocalHistory, []);
+});
+
+test("unmatched credit", (t) => {
+  const localHistory: WalletReserveHistoryItem[] = [];
+  const remoteHistory: ReserveTransaction[] = [
+    {
+      type: ReserveTransactionType.Credit,
+      amount: "TESTKUDOS:100",
+      sender_account_url: "payto://void/",
+      timestamp: { t_ms: 42 },
+      wire_reference: "ABC01",
+    },
+  ];
+  const r = reconcileReserveHistory(localHistory, remoteHistory);
+  const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+  t.deepEqual(r.updatedLocalHistory.length, 1);
+  t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100");
+  t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
+  t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:100");
+});
+
+test("unmatched credit #2", (t) => {
+  const localHistory: WalletReserveHistoryItem[] = [];
+  const remoteHistory: ReserveTransaction[] = [
+    {
+      type: ReserveTransactionType.Credit,
+      amount: "TESTKUDOS:100",
+      sender_account_url: "payto://void/",
+      timestamp: { t_ms: 42 },
+      wire_reference: "ABC01",
+    },
+    {
+      type: ReserveTransactionType.Credit,
+      amount: "TESTKUDOS:50",
+      sender_account_url: "payto://void/",
+      timestamp: { t_ms: 42 },
+      wire_reference: "ABC02",
+    },
+  ];
+  const r = reconcileReserveHistory(localHistory, remoteHistory);
+  const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+  t.deepEqual(r.updatedLocalHistory.length, 2);
+  t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
+  t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
+  t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:150");
+});
+
+test("matched credit", (t) => {
+  const localHistory: WalletReserveHistoryItem[] = [
+    {
+      type: WalletReserveHistoryItemType.Credit,
+      expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+      matchedExchangeTransaction: {
+        type: ReserveTransactionType.Credit,
+        amount: "TESTKUDOS:100",
+        sender_account_url: "payto://void/",
+        timestamp: { t_ms: 42 },
+        wire_reference: "ABC01",
+      },
+    },
+  ];
+  const remoteHistory: ReserveTransaction[] = [
+    {
+      type: ReserveTransactionType.Credit,
+      amount: "TESTKUDOS:100",
+      sender_account_url: "payto://void/",
+      timestamp: { t_ms: 42 },
+      wire_reference: "ABC01",
+    },
+    {
+      type: ReserveTransactionType.Credit,
+      amount: "TESTKUDOS:50",
+      sender_account_url: "payto://void/",
+      timestamp: { t_ms: 42 },
+      wire_reference: "ABC02",
+    },
+  ];
+  const r = reconcileReserveHistory(localHistory, remoteHistory);
+  const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+  t.deepEqual(r.updatedLocalHistory.length, 2);
+  t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
+  t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
+  t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:150");
+});
+
+test("fulfilling credit", (t) => {
+  const localHistory: WalletReserveHistoryItem[] = [
+    {
+      type: WalletReserveHistoryItemType.Credit,
+      expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+    },
+  ];
+  const remoteHistory: ReserveTransaction[] = [
+    {
+      type: ReserveTransactionType.Credit,
+      amount: "TESTKUDOS:100",
+      sender_account_url: "payto://void/",
+      timestamp: { t_ms: 42 },
+      wire_reference: "ABC01",
+    },
+    {
+      type: ReserveTransactionType.Credit,
+      amount: "TESTKUDOS:50",
+      sender_account_url: "payto://void/",
+      timestamp: { t_ms: 42 },
+      wire_reference: "ABC02",
+    },
+  ];
+  const r = reconcileReserveHistory(localHistory, remoteHistory);
+  const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+  t.deepEqual(r.updatedLocalHistory.length, 2);
+  t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
+});
+
+test("unfulfilled credit", (t) => {
+  const localHistory: WalletReserveHistoryItem[] = [
+    {
+      type: WalletReserveHistoryItemType.Credit,
+      expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+    },
+  ];
+  const remoteHistory: ReserveTransaction[] = [
+    {
+      type: ReserveTransactionType.Credit,
+      amount: "TESTKUDOS:100",
+      sender_account_url: "payto://void/",
+      timestamp: { t_ms: 42 },
+      wire_reference: "ABC01",
+    },
+    {
+      type: ReserveTransactionType.Credit,
+      amount: "TESTKUDOS:50",
+      sender_account_url: "payto://void/",
+      timestamp: { t_ms: 42 },
+      wire_reference: "ABC02",
+    },
+  ];
+  const r = reconcileReserveHistory(localHistory, remoteHistory);
+  const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+  t.deepEqual(r.updatedLocalHistory.length, 2);
+  t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
+});
+
+test("awaited credit", (t) => {
+  const localHistory: WalletReserveHistoryItem[] = [
+    {
+      type: WalletReserveHistoryItemType.Credit,
+      expectedAmount: Amounts.parseOrThrow("TESTKUDOS:50"),
+    },
+    {
+      type: WalletReserveHistoryItemType.Credit,
+      expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+    },
+  ];
+  const remoteHistory: ReserveTransaction[] = [
+    {
+      type: ReserveTransactionType.Credit,
+      amount: "TESTKUDOS:100",
+      sender_account_url: "payto://void/",
+      timestamp: { t_ms: 42 },
+      wire_reference: "ABC01",
+    },
+  ];
+  const r = reconcileReserveHistory(localHistory, remoteHistory);
+  const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+  t.deepEqual(r.updatedLocalHistory.length, 2);
+  t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100");
+  t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:50");
+  t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:100");
+});
+
+test("withdrawal new match", (t) => {
+  const localHistory: WalletReserveHistoryItem[] = [
+    {
+      type: WalletReserveHistoryItemType.Credit,
+      expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+      matchedExchangeTransaction: {
+        type: ReserveTransactionType.Credit,
+        amount: "TESTKUDOS:100",
+        sender_account_url: "payto://void/",
+        timestamp: { t_ms: 42 },
+        wire_reference: "ABC01",
+      },
+    },
+    {
+      type: WalletReserveHistoryItemType.Withdraw,
+      expectedAmount: Amounts.parseOrThrow("TESTKUDOS:5"),
+    },
+  ];
+  const remoteHistory: ReserveTransaction[] = [
+    {
+      type: ReserveTransactionType.Credit,
+      amount: "TESTKUDOS:100",
+      sender_account_url: "payto://void/",
+      timestamp: { t_ms: 42 },
+      wire_reference: "ABC01",
+    },
+    {
+      type: ReserveTransactionType.Withdraw,
+      amount: "TESTKUDOS:5",
+      h_coin_envelope: "foobar",
+      h_denom_pub: "foobar",
+      reserve_sig: "foobar",
+      withdraw_fee: "TESTKUDOS:0.1",
+    },
+  ];
+  const r = reconcileReserveHistory(localHistory, remoteHistory);
+  const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+  console.log(r);
+  t.deepEqual(r.updatedLocalHistory.length, 2);
+  t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:95");
+  t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
+  t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:95");
+});
+
+test("claimed but now arrived", (t) => {
+  const localHistory: WalletReserveHistoryItem[] = [
+    {
+      type: WalletReserveHistoryItemType.Credit,
+      expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+      matchedExchangeTransaction: {
+        type: ReserveTransactionType.Credit,
+        amount: "TESTKUDOS:100",
+        sender_account_url: "payto://void/",
+        timestamp: { t_ms: 42 },
+        wire_reference: "ABC01",
+      },
+    },
+    {
+      type: WalletReserveHistoryItemType.Withdraw,
+      expectedAmount: Amounts.parseOrThrow("TESTKUDOS:5"),
+    },
+  ];
+  const remoteHistory: ReserveTransaction[] = [
+    {
+      type: ReserveTransactionType.Credit,
+      amount: "TESTKUDOS:100",
+      sender_account_url: "payto://void/",
+      timestamp: { t_ms: 42 },
+      wire_reference: "ABC01",
+    },
+  ];
+  const r = reconcileReserveHistory(localHistory, remoteHistory);
+  const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+  t.deepEqual(r.updatedLocalHistory.length, 2);
+  t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100");
+  t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
+  t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:95");
+});
diff --git a/src/util/reserveHistoryUtil.ts b/src/util/reserveHistoryUtil.ts
new file mode 100644
index 00000000..95f58449
--- /dev/null
+++ b/src/util/reserveHistoryUtil.ts
@@ -0,0 +1,384 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+  WalletReserveHistoryItem,
+  WalletReserveHistoryItemType,
+} from "../types/dbTypes";
+import {
+  ReserveTransaction,
+  ReserveTransactionType,
+} from "../types/ReserveTransaction";
+import * as Amounts from "../util/amounts";
+import { timestampCmp } from "./time";
+import { deepCopy } from "./helpers";
+import { AmountString } from "../types/talerTypes";
+import { AmountJson } from "../util/amounts";
+
+/**
+ * Helpers for dealing with reserve histories.
+ *
+ * @author Florian Dold <address@hidden>
+ */
+
+export interface ReserveReconciliationResult {
+  /**
+   * The wallet's local history reconciled with the exchange's reserve history.
+   */
+  updatedLocalHistory: WalletReserveHistoryItem[];
+
+  /**
+   * History items that were newly created, subset of the
+   * updatedLocalHistory items.
+   */
+  newAddedItems: WalletReserveHistoryItem[];
+
+  /**
+   * History items that were newly matched, subset of the
+   * updatedLocalHistory items.
+   */
+  newMatchedItems: WalletReserveHistoryItem[];
+}
+
+export interface ReserveHistorySummary {
+  /**
+   * Balance computed by the wallet, should match the balance
+   * computed by the reserve.
+   */
+  computedReserveBalance: Amounts.AmountJson;
+
+  /**
+   * Reserve balance that is still available for withdrawal.
+   */
+  unclaimedReserveAmount: Amounts.AmountJson;
+
+  /**
+   * Amount that we're still expecting to come into the reserve.
+   */
+  awaitedReserveAmount: Amounts.AmountJson;
+
+  /**
+   * Amount withdrawn from the reserve so far.  Only counts
+   * finished withdrawals, not withdrawals in progress.
+   */
+  withdrawnAmount: Amounts.AmountJson;
+}
+
+export function isRemoteHistoryMatch(
+  t1: ReserveTransaction,
+  t2: ReserveTransaction,
+): boolean {
+  switch (t1.type) {
+    case ReserveTransactionType.Closing: {
+      return t1.type === t2.type && t1.wtid == t2.wtid;
+    }
+    case ReserveTransactionType.Credit: {
+      return t1.type === t2.type && t1.wire_reference === t2.wire_reference;
+    }
+    case ReserveTransactionType.Recoup: {
+      return (
+        t1.type === t2.type &&
+        t1.coin_pub === t2.coin_pub &&
+        timestampCmp(t1.timestamp, t2.timestamp) === 0
+      );
+    }
+    case ReserveTransactionType.Withdraw: {
+      return t1.type === t2.type && t1.h_coin_envelope === t2.h_coin_envelope;
+    }
+  }
+}
+
+export function isLocalRemoteHistoryPreferredMatch(
+  t1: WalletReserveHistoryItem,
+  t2: ReserveTransaction,
+): boolean {
+  switch (t1.type) {
+    case WalletReserveHistoryItemType.Credit: {
+      return (
+        t2.type === ReserveTransactionType.Credit &&
+        !!t1.expectedAmount &&
+        Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
+      );
+    }
+    case WalletReserveHistoryItemType.Withdraw:
+      return (
+        t2.type === ReserveTransactionType.Withdraw &&
+        !!t1.expectedAmount &&
+        Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
+      )
+    case WalletReserveHistoryItemType.Recoup: {
+      return (
+        t2.type === ReserveTransactionType.Recoup &&
+        !!t1.expectedAmount &&
+        Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
+      );
+    }
+  }
+  return false;
+}
+
+export function isLocalRemoteHistoryAcceptableMatch(
+  t1: WalletReserveHistoryItem,
+  t2: ReserveTransaction,
+): boolean {
+  switch (t1.type) {
+    case WalletReserveHistoryItemType.Closing:
+      throw Error("invariant violated");
+    case WalletReserveHistoryItemType.Credit:
+      return !t1.expectedAmount && t2.type == ReserveTransactionType.Credit;
+    case WalletReserveHistoryItemType.Recoup:
+      return !t1.expectedAmount && t2.type == ReserveTransactionType.Recoup;
+    case WalletReserveHistoryItemType.Withdraw:
+      return !t1.expectedAmount && t2.type == ReserveTransactionType.Withdraw;
+  }
+}
+
+export function summarizeReserveHistory(
+  localHistory: WalletReserveHistoryItem[],
+  currency: string,
+): ReserveHistorySummary {
+  const posAmounts: AmountJson[] = [];
+  const negAmounts: AmountJson[] = [];
+  const expectedPosAmounts: AmountJson[] = [];
+  const expectedNegAmounts: AmountJson[] = [];
+  const withdrawnAmounts: AmountJson[] = [];
+
+  for (const item of localHistory) {
+    switch (item.type) {
+      case WalletReserveHistoryItemType.Credit:
+        if (item.matchedExchangeTransaction) {
+          posAmounts.push(
+            Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
+          );
+        } else if (item.expectedAmount) {
+          expectedPosAmounts.push(item.expectedAmount);
+        }
+        break;
+      case WalletReserveHistoryItemType.Recoup:
+        if (item.matchedExchangeTransaction) {
+          if (item.matchedExchangeTransaction) {
+            posAmounts.push(
+              Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
+            );
+          } else if (item.expectedAmount) {
+            expectedPosAmounts.push(item.expectedAmount);
+          } else {
+            throw Error("invariant failed");
+          }
+        }
+        break;
+      case WalletReserveHistoryItemType.Closing:
+        if (item.matchedExchangeTransaction) {
+          negAmounts.push(
+            Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
+          );
+        } else {
+          throw Error("invariant failed");
+        }
+        break;
+      case WalletReserveHistoryItemType.Withdraw:
+        if (item.matchedExchangeTransaction) {
+          negAmounts.push(
+            Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
+          );
+          
withdrawnAmounts.push(Amounts.parseOrThrow(item.matchedExchangeTransaction.amount));
+        } else if (item.expectedAmount) {
+          expectedNegAmounts.push(item.expectedAmount);
+        } else {
+          throw Error("invariant failed");
+        }
+        break;
+    }
+  }
+
+  const z = Amounts.getZero(currency);
+
+  const computedBalance = Amounts.sub(
+    Amounts.add(z, ...posAmounts).amount,
+    ...negAmounts,
+  ).amount;
+
+  const unclaimedReserveAmount = Amounts.sub(
+    Amounts.add(z, ...posAmounts).amount,
+    ...negAmounts,
+    ...expectedNegAmounts,
+  ).amount;
+
+  const awaitedReserveAmount = Amounts.sub(
+    Amounts.add(z, ...expectedPosAmounts).amount,
+    ...expectedNegAmounts,
+  ).amount;
+
+  const withdrawnAmount = Amounts.add(z, ...withdrawnAmounts).amount;
+
+  return {
+    computedReserveBalance: computedBalance,
+    unclaimedReserveAmount: unclaimedReserveAmount,
+    awaitedReserveAmount: awaitedReserveAmount,
+    withdrawnAmount,
+  };
+}
+
+export function reconcileReserveHistory(
+  localHistory: WalletReserveHistoryItem[],
+  remoteHistory: ReserveTransaction[],
+): ReserveReconciliationResult {
+  const updatedLocalHistory: WalletReserveHistoryItem[] = deepCopy(
+    localHistory,
+  );
+  const newMatchedItems: WalletReserveHistoryItem[] = [];
+  const newAddedItems: WalletReserveHistoryItem[] = [];
+
+  const remoteMatched = remoteHistory.map(() => false);
+  const localMatched = localHistory.map(() => false);
+
+  // Take care of deposits
+
+  // First, see which pairs are already a definite match.
+  for (let remoteIndex = 0; remoteIndex < remoteHistory.length; remoteIndex++) 
{
+    const rhi = remoteHistory[remoteIndex];
+    for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
+      if (localMatched[localIndex]) {
+        continue;
+      }
+      const lhi = localHistory[localIndex];
+      if (!lhi.matchedExchangeTransaction) {
+        continue;
+      }
+      if (isRemoteHistoryMatch(rhi, lhi.matchedExchangeTransaction)) {
+        localMatched[localIndex] = true;
+        remoteMatched[remoteIndex] = true;
+        break;
+      }
+    }
+  }
+
+  // Check that all previously matched items are still matched
+  for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
+    if (localMatched[localIndex]) {
+      continue;
+    }
+    const lhi = localHistory[localIndex];
+    if (lhi.matchedExchangeTransaction) {
+      // Don't use for further matching
+      localMatched[localIndex] = true;
+      // FIXME: emit some error here!
+      throw Error("previously matched reserve history item now unmatched");
+    }
+  }
+
+  // Next, find out if there are any exact new matches between local and remote
+  // history items
+  for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
+    if (localMatched[localIndex]) {
+      continue;
+    }
+    const lhi = localHistory[localIndex];
+    for (
+      let remoteIndex = 0;
+      remoteIndex < remoteHistory.length;
+      remoteIndex++
+    ) {
+      const rhi = remoteHistory[remoteIndex];
+      if (remoteMatched[remoteIndex]) {
+        continue;
+      }
+      if (isLocalRemoteHistoryPreferredMatch(lhi, rhi)) {
+        localMatched[localIndex] = true;
+        remoteMatched[remoteIndex] = true;
+        updatedLocalHistory[localIndex].matchedExchangeTransaction = rhi as 
any;
+        newMatchedItems.push(lhi);
+        break;
+      }
+    }
+  }
+
+  // Next, find out if there are any acceptable new matches between local and 
remote
+  // history items
+  for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
+    if (localMatched[localIndex]) {
+      continue;
+    }
+    const lhi = localHistory[localIndex];
+    for (
+      let remoteIndex = 0;
+      remoteIndex < remoteHistory.length;
+      remoteIndex++
+    ) {
+      const rhi = remoteHistory[remoteIndex];
+      if (remoteMatched[remoteIndex]) {
+        continue;
+      }
+      if (isLocalRemoteHistoryAcceptableMatch(lhi, rhi)) {
+        localMatched[localIndex] = true;
+        remoteMatched[remoteIndex] = true;
+        updatedLocalHistory[localIndex].matchedExchangeTransaction = rhi as 
any;
+        newMatchedItems.push(lhi);
+        break;
+      }
+    }
+  }
+
+  // Finally we add new history items
+  for (let remoteIndex = 0; remoteIndex < remoteHistory.length; remoteIndex++) 
{
+    if (remoteMatched[remoteIndex]) {
+      continue;
+    }
+    const rhi = remoteHistory[remoteIndex];
+    let newItem: WalletReserveHistoryItem;
+    switch (rhi.type) {
+      case ReserveTransactionType.Closing: {
+        newItem = {
+          type: WalletReserveHistoryItemType.Closing,
+          matchedExchangeTransaction: rhi,
+        };
+        break;
+      }
+      case ReserveTransactionType.Credit: {
+        newItem = {
+          type: WalletReserveHistoryItemType.Credit,
+          matchedExchangeTransaction: rhi,
+        };
+        break;
+      }
+      case ReserveTransactionType.Recoup: {
+        newItem = {
+          type: WalletReserveHistoryItemType.Recoup,
+          matchedExchangeTransaction: rhi,
+        };
+        break;
+      }
+      case ReserveTransactionType.Withdraw: {
+        newItem = {
+          type: WalletReserveHistoryItemType.Withdraw,
+          matchedExchangeTransaction: rhi,
+        };
+        break;
+      }
+    }
+    updatedLocalHistory.push(newItem);
+    newAddedItems.push(newItem);
+  }
+
+  return {
+    updatedLocalHistory,
+    newAddedItems,
+    newMatchedItems,
+  };
+}
diff --git a/src/wallet.ts b/src/wallet.ts
index 2560b0dc..3171d0ce 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -26,8 +26,7 @@ import { CryptoWorkerFactory } from 
"./crypto/workers/cryptoApi";
 import { HttpRequestLibrary } from "./util/http";
 import { Database } from "./util/query";
 
-import { AmountJson } from "./util/amounts";
-import * as Amounts from "./util/amounts";
+import { Amounts, AmountJson } from "./util/amounts";
 
 import {
   getWithdrawDetailsForUri,
@@ -92,7 +91,7 @@ import {
 import { InternalWalletState } from "./operations/state";
 import { createReserve, confirmReserve } from "./operations/reserves";
 import { processRefreshGroup, createRefreshGroup } from "./operations/refresh";
-import { processWithdrawSession } from "./operations/withdraw";
+import { processWithdrawGroup } from "./operations/withdraw";
 import { getHistory } from "./operations/history";
 import { getPendingOperations } from "./operations/pending";
 import { getBalances } from "./operations/balance";
@@ -193,9 +192,9 @@ export class Wallet {
         await processReserve(this.ws, pending.reservePub, forceNow);
         break;
       case PendingOperationType.Withdraw:
-        await processWithdrawSession(
+        await processWithdrawGroup(
           this.ws,
-          pending.withdrawSessionId,
+          pending.withdrawalGroupId,
           forceNow,
         );
         break;
@@ -574,10 +573,14 @@ export class Wallet {
     await this.db.put(Stores.currencies, currencyRecord);
   }
 
-  async getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> {
-    return await this.db
-      .iter(Stores.reserves)
-      .filter((r) => r.exchangeBaseUrl === exchangeBaseUrl);
+  async getReserves(exchangeBaseUrl?: string): Promise<ReserveRecord[]> {
+    if (exchangeBaseUrl) {
+      return await this.db
+        .iter(Stores.reserves)
+        .filter((r) => r.exchangeBaseUrl === exchangeBaseUrl);
+    } else {
+      return await this.db.iter(Stores.reserves).toArray();
+    }
   }
 
   async getCoinsForExchange(exchangeBaseUrl: string): Promise<CoinRecord[]> {
@@ -807,8 +810,8 @@ export class Wallet {
       let withdrawalReservePub: string | undefined;
       if (cs.type == CoinSourceType.Withdraw) {
         const ws = await this.db.get(
-          Stores.withdrawalSession,
-          cs.withdrawSessionId,
+          Stores.withdrawalGroups,
+          cs.withdrawalGroupId,
         );
         if (!ws) {
           console.error("no withdrawal session found for coin");
@@ -822,10 +825,10 @@ export class Wallet {
         coin_pub: c.coinPub,
         denom_pub: c.denomPub,
         denom_pub_hash: c.denomPubHash,
-        denom_value: Amounts.toString(denom.value),
+        denom_value: Amounts.stringify(denom.value),
         exchange_base_url: c.exchangeBaseUrl,
         refresh_parent_coin_pub: refreshParentCoinPub,
-        remaining_value: Amounts.toString(c.currentAmount),
+        remaining_value: Amounts.stringify(c.currentAmount),
         withdrawal_reserve_pub: withdrawalReservePub,
         coin_suspended: c.suspended,
       });
diff --git a/src/webex/pages/popup.tsx b/src/webex/pages/popup.tsx
index 7b20f222..17880db5 100644
--- a/src/webex/pages/popup.tsx
+++ b/src/webex/pages/popup.tsx
@@ -565,10 +565,6 @@ function formatHistoryItem(historyItem: HistoryEvent) {
         <HistoryItem
           timestamp={historyItem.timestamp}
           small={i18n.str`Reserve balance updated`}
-          fees={amountDiff(
-            historyItem.amountExpected,
-            historyItem.amountReserveBalance,
-          )}
         />
       );
     }
diff --git a/src/webex/pages/return-coins.tsx b/src/webex/pages/return-coins.tsx
index fd9238ee..3786697c 100644
--- a/src/webex/pages/return-coins.tsx
+++ b/src/webex/pages/return-coins.tsx
@@ -25,7 +25,7 @@
  */
 
 import { AmountJson } from "../../util/amounts";
-import * as Amounts from "../../util/amounts";
+import { Amounts } from "../../util/amounts";
 
 import { SenderWireInfos, WalletBalance } from "../../types/walletTypes";
 
@@ -70,7 +70,7 @@ class ReturnSelectionItem extends React.Component<
     );
     this.state = {
       currency: props.balance.byExchange[props.exchangeUrl].available.currency,
-      selectedValue: Amounts.toString(
+      selectedValue: Amounts.stringify(
         props.balance.byExchange[props.exchangeUrl].available,
       ),
       selectedWire: "",
diff --git a/tsconfig.json b/tsconfig.json
index 6808b337..f87e16b1 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -86,6 +86,8 @@
     "src/util/payto.ts",
     "src/util/promiseUtils.ts",
     "src/util/query.ts",
+    "src/util/reserveHistoryUtil-test.ts",
+    "src/util/reserveHistoryUtil.ts",
     "src/util/talerconfig.ts",
     "src/util/taleruri-test.ts",
     "src/util/taleruri.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]