gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 03/03: model reserve history in the exchange, improv


From: gnunet
Subject: [taler-wallet-core] 03/03: model reserve history in the exchange, improve reserve handling logic
Date: Thu, 02 Apr 2020 17:04:49 +0200

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

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

commit ef0acf06bfb7820a21c4719dba0d659f600be3c7
Author: Florian Dold <address@hidden>
AuthorDate: Thu Apr 2 20:33:01 2020 +0530

    model reserve history in the exchange, improve reserve handling logic
---
 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/dbTypes.ts                       |  70 +++---
 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/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 +-
 27 files changed, 1062 insertions(+), 426 deletions(-)

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/dbTypes.ts b/src/types/dbTypes.ts
index 9c2b3ca3..b87ada11 100644
--- a/src/types/dbTypes.ts
+++ b/src/types/dbTypes.ts
@@ -151,7 +151,7 @@ export interface WalletReserveHistoryCreditItem {
   /**
    * Amount we expect to see credited.
    */
-  expectedAmount?: string;
+  expectedAmount?: AmountJson;
 
   /**
    * Item from the reserve transaction history that this
@@ -161,7 +161,15 @@ export interface WalletReserveHistoryCreditItem {
 }
 
 export interface WalletReserveHistoryWithdrawItem {
-  expectedAmount?: string;
+  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;
 
@@ -188,7 +196,7 @@ export interface WalletReserveHistoryRecoupItem {
   /**
    * Amount we expect to see recouped.
    */
-  expectedAmount?: string;
+  expectedAmount?: AmountJson;
 
   /**
    * Item from the reserve transaction history that this
@@ -222,6 +230,11 @@ export interface ReserveRecord {
    */
   exchangeBaseUrl: string;
 
+  /**
+   * Currency of the reserve.
+   */
+  currency: string;
+
   /**
    * Time when the reserve was created.
    */
@@ -237,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.
@@ -305,7 +297,7 @@ export interface ReserveRecord {
    */
   lastError: OperationError | undefined;
 
-  reserveTransactions: ReserveTransaction[];
+  reserveTransactions: WalletReserveHistoryItem[];
 }
 
 /**
@@ -627,6 +619,7 @@ export interface PlanchetRecord {
   blindingKey: string;
   withdrawSig: string;
   coinEv: string;
+  coinEvHash: string;
   coinValue: AmountJson;
   isFromTip: boolean;
 }
@@ -675,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.
@@ -1362,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
@@ -1636,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" });
     }
   }
 
@@ -1697,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/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: "",

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



reply via email to

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