gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (7e947ca2 -> 21b5e6f2)


From: gnunet
Subject: [taler-wallet-core] branch master updated (7e947ca2 -> 21b5e6f2)
Date: Mon, 11 May 2020 14:52:07 +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 7e947ca2 Add functions for getting and comparing balances to 
integration tests
     new 5d6192b0 make planchet management during withdrawal O(n) instead of 
O(n^2)
     new d9433a21 logging
     new 21b5e6f2 bump db major version

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            |   3 +-
 src/crypto/workers/cryptoImplementation.ts |  88 +++++++++------
 src/db.ts                                  |   2 +-
 src/headless/integrationtest.ts            |   7 +-
 src/operations/balance.ts                  |  25 +++--
 src/operations/history.ts                  |  24 +---
 src/operations/pending.ts                  |  16 ++-
 src/operations/refresh.ts                  |  10 +-
 src/operations/reserves.ts                 | 118 ++++++++++----------
 src/operations/tip.ts                      |  57 +++++++---
 src/operations/withdraw.ts                 | 172 +++++++++++++++++++----------
 src/types/dbTypes.ts                       |  80 +++++++++++---
 src/types/walletTypes.ts                   |   4 +-
 src/util/amounts.ts                        |  29 +++++
 src/util/query.ts                          |   3 -
 src/wallet.ts                              |  31 ++----
 src/webex/renderHtml.tsx                   |  40 +++----
 17 files changed, 420 insertions(+), 289 deletions(-)

diff --git a/src/crypto/workers/cryptoApi.ts b/src/crypto/workers/cryptoApi.ts
index a6f9d162..14964e4d 100644
--- a/src/crypto/workers/cryptoApi.ts
+++ b/src/crypto/workers/cryptoApi.ts
@@ -30,6 +30,7 @@ import {
   RefreshSessionRecord,
   TipPlanchet,
   WireFee,
+  DenominationSelectionInfo,
 } from "../../types/dbTypes";
 
 import { CryptoWorker } from "./cryptoWorker";
@@ -435,7 +436,7 @@ export class CryptoApi {
     exchangeBaseUrl: string,
     kappa: number,
     meltCoin: CoinRecord,
-    newCoinDenoms: DenominationRecord[],
+    newCoinDenoms: DenominationSelectionInfo,
     meltFee: AmountJson,
   ): Promise<RefreshSessionRecord> {
     return this.doRpc<RefreshSessionRecord>(
diff --git a/src/crypto/workers/cryptoImplementation.ts 
b/src/crypto/workers/cryptoImplementation.ts
index de3b88bb..dc0452dc 100644
--- a/src/crypto/workers/cryptoImplementation.ts
+++ b/src/crypto/workers/cryptoImplementation.ts
@@ -34,6 +34,7 @@ import {
   TipPlanchet,
   WireFee,
   CoinSourceType,
+  DenominationSelectionInfo,
 } from "../../types/dbTypes";
 
 import { CoinDepositPermission, RecoupRequest } from "../../types/talerTypes";
@@ -359,14 +360,15 @@ export class CryptoImplementation {
     exchangeBaseUrl: string,
     kappa: number,
     meltCoin: CoinRecord,
-    newCoinDenoms: DenominationRecord[],
+    newCoinDenoms: DenominationSelectionInfo,
     meltFee: AmountJson,
   ): RefreshSessionRecord {
-    let valueWithFee = Amounts.getZero(newCoinDenoms[0].value.currency);
+    const currency = newCoinDenoms.selectedDenoms[0].denom.value.currency;
+    let valueWithFee = Amounts.getZero(currency);
 
-    for (const ncd of newCoinDenoms) {
-      valueWithFee = Amounts.add(valueWithFee, ncd.value, ncd.feeWithdraw)
-        .amount;
+    for (const ncd of newCoinDenoms.selectedDenoms) {
+      const t = Amounts.add(ncd.denom.value, ncd.denom.feeWithdraw).amount;
+      valueWithFee = Amounts.add(valueWithFee, Amounts.mult(t, 
ncd.count).amount).amount;
     }
 
     // melt fee
@@ -386,9 +388,11 @@ export class CryptoImplementation {
       transferPubs.push(encodeCrock(transferKeyPair.ecdhePub));
     }
 
-    for (const denom of newCoinDenoms) {
-      const r = decodeCrock(denom.denomPub);
-      sessionHc.update(r);
+    for (const denomSel of newCoinDenoms.selectedDenoms) {
+      for (let i = 0; i < denomSel.count; i++) {
+        const r = decodeCrock(denomSel.denom.denomPub);
+        sessionHc.update(r);
+      }
     }
 
     sessionHc.update(decodeCrock(meltCoin.coinPub));
@@ -396,27 +400,29 @@ export class CryptoImplementation {
 
     for (let i = 0; i < kappa; i++) {
       const planchets: RefreshPlanchetRecord[] = [];
-      for (let j = 0; j < newCoinDenoms.length; j++) {
-        const transferPriv = decodeCrock(transferPrivs[i]);
-        const oldCoinPub = decodeCrock(meltCoin.coinPub);
-        const transferSecret = keyExchangeEcdheEddsa(transferPriv, oldCoinPub);
-
-        const fresh = setupRefreshPlanchet(transferSecret, j);
-
-        const coinPriv = fresh.coinPriv;
-        const coinPub = fresh.coinPub;
-        const blindingFactor = fresh.bks;
-        const pubHash = hash(coinPub);
-        const denomPub = decodeCrock(newCoinDenoms[j].denomPub);
-        const ev = rsaBlind(pubHash, blindingFactor, denomPub);
-        const planchet: RefreshPlanchetRecord = {
-          blindingKey: encodeCrock(blindingFactor),
-          coinEv: encodeCrock(ev),
-          privateKey: encodeCrock(coinPriv),
-          publicKey: encodeCrock(coinPub),
-        };
-        planchets.push(planchet);
-        sessionHc.update(ev);
+      for (let j = 0; j < newCoinDenoms.selectedDenoms.length; j++) {
+        const denomSel = newCoinDenoms.selectedDenoms[j];
+        for (let k = 0; k < denomSel.count; k++) {
+          const coinNumber = planchets.length;
+          const transferPriv = decodeCrock(transferPrivs[i]);
+          const oldCoinPub = decodeCrock(meltCoin.coinPub);
+          const transferSecret = keyExchangeEcdheEddsa(transferPriv, 
oldCoinPub);
+          const fresh = setupRefreshPlanchet(transferSecret, coinNumber);
+          const coinPriv = fresh.coinPriv;
+          const coinPub = fresh.coinPub;
+          const blindingFactor = fresh.bks;
+          const pubHash = hash(coinPub);
+          const denomPub = decodeCrock(denomSel.denom.denomPub);
+          const ev = rsaBlind(pubHash, blindingFactor, denomPub);
+          const planchet: RefreshPlanchetRecord = {
+            blindingKey: encodeCrock(blindingFactor),
+            coinEv: encodeCrock(ev),
+            privateKey: encodeCrock(coinPriv),
+            publicKey: encodeCrock(coinPub),
+          };
+          planchets.push(planchet);
+          sessionHc.update(ev);
+        }
       }
       planchetsForGammas.push(planchets);
     }
@@ -432,9 +438,23 @@ export class CryptoImplementation {
 
     const confirmSig = eddsaSign(confirmData, decodeCrock(meltCoin.coinPriv));
 
-    let valueOutput = Amounts.getZero(newCoinDenoms[0].value.currency);
-    for (const denom of newCoinDenoms) {
-      valueOutput = Amounts.add(valueOutput, denom.value).amount;
+    let valueOutput = Amounts.getZero(currency);
+    for (const denomSel of newCoinDenoms.selectedDenoms) {
+      const denom = denomSel.denom;
+      for (let i = 0; i < denomSel.count; i++) {
+        valueOutput = Amounts.add(valueOutput, denom.value).amount;
+      }
+    }
+
+    const newDenoms: string[] = [];
+    const newDenomHashes: string[] = [];
+
+    for (const denomSel of newCoinDenoms.selectedDenoms) {
+      const denom = denomSel.denom;
+      for (let i = 0; i < denomSel.count; i++) {
+        newDenoms.push(denom.denomPub);
+        newDenomHashes.push(denom.denomPubHash);
+      }
     }
 
     const refreshSession: RefreshSessionRecord = {
@@ -442,8 +462,8 @@ export class CryptoImplementation {
       exchangeBaseUrl,
       hash: encodeCrock(sessionHash),
       meltCoinPub: meltCoin.coinPub,
-      newDenomHashes: newCoinDenoms.map((d) => d.denomPubHash),
-      newDenoms: newCoinDenoms.map((d) => d.denomPub),
+      newDenomHashes,
+      newDenoms,
       norevealIndex: undefined,
       planchetsForGammas: planchetsForGammas,
       transferPrivs,
diff --git a/src/db.ts b/src/db.ts
index ea20d2a0..efc3b78a 100644
--- a/src/db.ts
+++ b/src/db.ts
@@ -7,7 +7,7 @@ import { openDatabase, Database, Store, Index } from 
"./util/query";
  * with each major change.  When incrementing the major version,
  * the wallet should import data from the previous version.
  */
-const TALER_DB_NAME = "taler-walletdb-v1";
+const TALER_DB_NAME = "taler-walletdb-v2";
 
 /**
  * Current database minor version, should be incremented
diff --git a/src/headless/integrationtest.ts b/src/headless/integrationtest.ts
index 24ceb9ce..5beb7b79 100644
--- a/src/headless/integrationtest.ts
+++ b/src/headless/integrationtest.ts
@@ -77,9 +77,8 @@ async function makePayment(
 
   paymentStatus = await merchant.checkPayment(orderResp.orderId);
 
-  console.log("payment status after wallet payment:", paymentStatus);
-
   if (!paymentStatus.paid) {
+    console.log("payment status:", paymentStatus);
     throw Error("payment did not succeed");
   }
 
@@ -112,10 +111,6 @@ export async function runIntegrationTest(args: 
IntegrationTestArgs): Promise<voi
   );
   logger.info("done withdrawing test balance");
 
-  const balance = await myWallet.getBalances();
-
-  console.log(JSON.stringify(balance, null, 2));
-
   const myMerchant = new MerchantBackendConnection(
     args.merchantBaseUrl,
     args.merchantApiKey,
diff --git a/src/operations/balance.ts b/src/operations/balance.ts
index c369af19..b5c1ec79 100644
--- a/src/operations/balance.ts
+++ b/src/operations/balance.ts
@@ -106,18 +106,19 @@ export async function getBalancesInsideTransaction(
     }
   });
 
-  await tx.iter(Stores.withdrawalGroups).forEach((wds) => {
-    let w = wds.totalCoinValue;
-    for (let i = 0; i < wds.planchets.length; i++) {
-      if (wds.withdrawn[i]) {
-        const p = wds.planchets[i];
-        if (p) {
-          w = Amounts.sub(w, p.coinValue).amount;
-        }
-      }
-    }
-    addTo(balanceStore, "pendingIncoming", w, wds.exchangeBaseUrl);
-  });
+  // FIXME: re-implement
+  // await tx.iter(Stores.withdrawalGroups).forEach((wds) => {
+  //   let w = wds.totalCoinValue;
+  //   for (let i = 0; i < wds.planchets.length; i++) {
+  //     if (wds.withdrawn[i]) {
+  //       const p = wds.planchets[i];
+  //       if (p) {
+  //         w = Amounts.sub(w, p.coinValue).amount;
+  //       }
+  //     }
+  //   }
+  //   addTo(balanceStore, "pendingIncoming", w, wds.exchangeBaseUrl);
+  // });
 
   await tx.iter(Stores.purchases).forEach((t) => {
     if (t.timestampFirstSuccessfulPay) {
diff --git a/src/operations/history.ts b/src/operations/history.ts
index f32dbbe2..669a6cf8 100644
--- a/src/operations/history.ts
+++ b/src/operations/history.ts
@@ -22,7 +22,6 @@ import {
   Stores,
   ProposalStatus,
   ProposalRecord,
-  PlanchetRecord,
 } from "../types/dbTypes";
 import { Amounts } from "../util/amounts";
 import { AmountJson } from "../util/amounts";
@@ -34,7 +33,6 @@ import {
   ReserveType,
   ReserveCreationDetail,
   VerbosePayCoinDetails,
-  VerboseWithdrawDetails,
   VerboseRefreshDetails,
 } from "../types/history";
 import { assertUnreachable } from "../util/assertUnreachable";
@@ -177,6 +175,7 @@ export async function getHistory(
       Stores.tips,
       Stores.withdrawalGroups,
       Stores.payEvents,
+      Stores.planchets,
       Stores.refundEvents,
       Stores.reserveUpdatedEvents,
       Stores.recoupGroups,
@@ -209,23 +208,6 @@ export async function getHistory(
 
       tx.iter(Stores.withdrawalGroups).forEach((wsr) => {
         if (wsr.timestampFinish) {
-          const cs: PlanchetRecord[] = [];
-          wsr.planchets.forEach((x) => {
-            if (x) {
-              cs.push(x);
-            }
-          });
-
-          let verboseDetails: VerboseWithdrawDetails | undefined = undefined;
-          if (historyQuery?.extraDebug) {
-            verboseDetails = {
-              coins: cs.map((x) => ({
-                value: Amounts.stringify(x.coinValue),
-                denomPub: x.denomPub,
-              })),
-            };
-          }
-
           history.push({
             type: HistoryEventType.Withdrawn,
             withdrawalGroupId: wsr.withdrawalGroupId,
@@ -233,12 +215,12 @@ export async function getHistory(
               HistoryEventType.Withdrawn,
               wsr.withdrawalGroupId,
             ),
-            amountWithdrawnEffective: Amounts.stringify(wsr.totalCoinValue),
+            amountWithdrawnEffective: 
Amounts.stringify(wsr.denomsSel.totalCoinValue),
             amountWithdrawnRaw: Amounts.stringify(wsr.rawWithdrawalAmount),
             exchangeBaseUrl: wsr.exchangeBaseUrl,
             timestamp: wsr.timestampFinish,
             withdrawalSource: wsr.source,
-            verboseDetails,
+            verboseDetails: undefined,
           });
         }
       });
diff --git a/src/operations/pending.ts b/src/operations/pending.ts
index a797763b..14072633 100644
--- a/src/operations/pending.ts
+++ b/src/operations/pending.ts
@@ -246,7 +246,7 @@ async function gatherWithdrawalPending(
   resp: PendingOperationsResponse,
   onlyDue = false,
 ): Promise<void> {
-  await tx.iter(Stores.withdrawalGroups).forEach((wsr) => {
+  await tx.iter(Stores.withdrawalGroups).forEachAsync(async (wsr) => {
     if (wsr.timestampFinish) {
       return;
     }
@@ -258,11 +258,14 @@ async function gatherWithdrawalPending(
     if (onlyDue && wsr.retryInfo.nextRetry.t_ms > now.t_ms) {
       return;
     }
-    const numCoinsWithdrawn = wsr.withdrawn.reduce(
-      (a, x) => a + (x ? 1 : 0),
-      0,
-    );
-    const numCoinsTotal = wsr.withdrawn.length;
+    let numCoinsWithdrawn = 0;
+    let numCoinsTotal = 0;
+    await tx.iterIndexed(Stores.planchets.byGroup, 
wsr.withdrawalGroupId).forEach((x) => {
+      numCoinsTotal++;
+      if (x.withdrawalDone) {
+        numCoinsWithdrawn++;
+      }
+    });
     resp.pendingOperations.push({
       type: PendingOperationType.Withdraw,
       givesLifeness: true,
@@ -443,6 +446,7 @@ export async function getPendingOperations(
       Stores.tips,
       Stores.purchases,
       Stores.recoupGroups,
+      Stores.planchets,
     ],
     async (tx) => {
       const walletBalance = await getBalancesInsideTransaction(ws, tx);
diff --git a/src/operations/refresh.ts b/src/operations/refresh.ts
index 92476933..5563d94d 100644
--- a/src/operations/refresh.ts
+++ b/src/operations/refresh.ts
@@ -67,7 +67,9 @@ export function getTotalRefreshCost(
   const withdrawDenoms = getWithdrawDenomList(withdrawAmount, denoms);
   const resultingAmount = Amounts.add(
     Amounts.getZero(withdrawAmount.currency),
-    ...withdrawDenoms.map((d) => d.value),
+    ...withdrawDenoms.selectedDenoms.map(
+      (d) => Amounts.mult(d.denom.value, d.count).amount,
+    ),
   ).amount;
   const totalCost = Amounts.sub(amountLeft, resultingAmount).amount;
   logger.trace(
@@ -130,7 +132,7 @@ async function refreshCreateSession(
 
   const newCoinDenoms = getWithdrawDenomList(availableAmount, availableDenoms);
 
-  if (newCoinDenoms.length === 0) {
+  if (newCoinDenoms.selectedDenoms.length === 0) {
     logger.trace(
       `not refreshing, available amount ${amountToPretty(
         availableAmount,
@@ -353,7 +355,6 @@ async function refreshReveal(
     `refreshes/${refreshSession.hash}/reveal`,
     refreshSession.exchangeBaseUrl,
   );
-  logger.trace("reveal request:", req);
 
   let resp;
   try {
@@ -364,9 +365,6 @@ async function refreshReveal(
     return;
   }
 
-  logger.trace("session:", refreshSession);
-  logger.trace("reveal response:", resp);
-
   if (resp.status !== 200) {
     console.error("error: /refresh/reveal returned status " + resp.status);
     return;
diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts
index 153ad6b8..f6671d48 100644
--- a/src/operations/reserves.ts
+++ b/src/operations/reserves.ts
@@ -33,7 +33,6 @@ import {
   updateRetryInfoTimeout,
   ReserveUpdatedEventRecord,
   WalletReserveHistoryItemType,
-  DenominationRecord,
   PlanchetRecord,
   WithdrawalSourceType,
 } from "../types/dbTypes";
@@ -593,33 +592,6 @@ 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.
  *
@@ -654,7 +626,7 @@ async function depleteReserve(
     withdrawAmount,
   );
   logger.trace(`got denom list`);
-  if (denomsForWithdraw.length === 0) {
+  if (!denomsForWithdraw) {
     // Only complain about inability to withdraw if we
     // didn't withdraw before.
     if (Amounts.isZero(summary.withdrawnAmount)) {
@@ -675,15 +647,42 @@ async function depleteReserve(
 
   const withdrawalGroupId = encodeCrock(randomBytes(32));
 
-  const totalCoinValue = Amounts.sum(denomsForWithdraw.map((x) => x.value))
-    .amount;
-
   const planchets: PlanchetRecord[] = [];
-  for (const d of denomsForWithdraw) {
-    const p = await makePlanchet(ws, reserve, d);
-    planchets.push(p);
+  let coinIdx = 0;
+  for (let i = 0; i < denomsForWithdraw.selectedDenoms.length; i++) {
+    const d = denomsForWithdraw.selectedDenoms[i];
+    const denom = d.denom;
+    for (let j = 0; j < d.count; j++) {
+      const r = await ws.cryptoApi.createPlanchet({
+        denomPub: denom.denomPub,
+        feeWithdraw: denom.feeWithdraw,
+        reservePriv: reserve.reservePriv,
+        reservePub: reserve.reservePub,
+        value: denom.value,
+      });
+      const planchet: PlanchetRecord = {
+        blindingKey: r.blindingKey,
+        coinEv: r.coinEv,
+        coinEvHash: r.coinEvHash,
+        coinIdx,
+        coinPriv: r.coinPriv,
+        coinPub: r.coinPub,
+        coinValue: r.coinValue,
+        denomPub: r.denomPub,
+        denomPubHash: r.denomPubHash,
+        isFromTip: false,
+        reservePub: r.reservePub,
+        withdrawalDone: false,
+        withdrawSig: r.withdrawSig,
+        withdrawalGroupId: withdrawalGroupId,
+      };
+      planchets.push(planchet);
+      coinIdx++;
+    }
   }
 
+  logger.trace("created plachets");
+
   const withdrawalRecord: WithdrawalGroupRecord = {
     withdrawalGroupId: withdrawalGroupId,
     exchangeBaseUrl: reserve.exchangeBaseUrl,
@@ -693,23 +692,24 @@ async function depleteReserve(
     },
     rawWithdrawalAmount: withdrawAmount,
     timestampStart: getTimestampNow(),
-    denoms: denomsForWithdraw.map((x) => x.denomPub),
-    withdrawn: denomsForWithdraw.map((x) => false),
-    planchets,
-    totalCoinValue,
     retryInfo: initRetryInfo(),
     lastErrorPerCoin: {},
     lastError: undefined,
+    denomsSel: {
+      totalCoinValue: denomsForWithdraw.totalCoinValue,
+      totalWithdrawCost: denomsForWithdraw.totalWithdrawCost,
+      selectedDenoms: denomsForWithdraw.selectedDenoms.map((x) => {
+        return {
+          countAllocated: x.count,
+          countPlanchetCreated: x.count,
+          denomPubHash: x.denom.denomPubHash,
+        };
+      }),
+    },
   };
 
-  const totalCoinWithdrawFee = Amounts.sum(
-    denomsForWithdraw.map((x) => x.feeWithdraw),
-  ).amount;
-  const totalWithdrawAmount = Amounts.add(totalCoinValue, totalCoinWithdrawFee)
-    .amount;
-
   const success = await ws.db.runWithWriteTransaction(
-    [Stores.withdrawalGroups, Stores.reserves],
+    [Stores.withdrawalGroups, Stores.reserves, Stores.planchets],
     async (tx) => {
       const newReserve = await tx.get(Stores.reserves, reservePub);
       if (!newReserve) {
@@ -723,7 +723,10 @@ async function depleteReserve(
         newReserve.currency,
       );
       if (
-        Amounts.cmp(newSummary.unclaimedReserveAmount, totalWithdrawAmount) < 0
+        Amounts.cmp(
+          newSummary.unclaimedReserveAmount,
+          denomsForWithdraw.totalWithdrawCost,
+        ) < 0
       ) {
         // Something must have happened concurrently!
         logger.error(
@@ -731,20 +734,23 @@ async function depleteReserve(
         );
         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,
-        });
+      for (let i = 0; i < denomsForWithdraw.selectedDenoms.length; i++) {
+        const sd = denomsForWithdraw.selectedDenoms[i];
+        for (let j = 0; j < sd.count; j++) {
+          const amt = Amounts.add(sd.denom.value, sd.denom.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);
+      for (const p of planchets) {
+        await tx.put(Stores.planchets, p);
+      }
       return true;
     },
   );
diff --git a/src/operations/tip.ts b/src/operations/tip.ts
index 6f492ea3..27956e26 100644
--- a/src/operations/tip.ts
+++ b/src/operations/tip.ts
@@ -30,6 +30,7 @@ import {
   initRetryInfo,
   updateRetryInfoTimeout,
   WithdrawalSourceType,
+  TipPlanchet,
 } from "../types/dbTypes";
 import {
   getExchangeWithdrawalInfo,
@@ -72,6 +73,7 @@ export async function getTipStatus(
   ]);
 
   if (!tipRecord) {
+    await updateExchangeFromUrl(ws, tipPickupStatus.exchange_url);
     const withdrawDetails = await getExchangeWithdrawalInfo(
       ws,
       tipPickupStatus.exchange_url,
@@ -79,6 +81,11 @@ export async function getTipStatus(
     );
 
     const tipId = encodeCrock(getRandomBytes(32));
+    const selectedDenoms = await getVerifiedWithdrawDenomList(
+      ws,
+      tipPickupStatus.exchange_url,
+      amount,
+    );
 
     tipRecord = {
       tipId,
@@ -100,6 +107,17 @@ export async function getTipStatus(
       ).amount,
       retryInfo: initRetryInfo(),
       lastError: undefined,
+      denomsSel: {
+        totalCoinValue: selectedDenoms.totalCoinValue,
+        totalWithdrawCost: selectedDenoms.totalWithdrawCost,
+        selectedDenoms: selectedDenoms.selectedDenoms.map((x) => {
+          return {
+            countAllocated: x.count,
+            countPlanchetCreated: x.count,
+            denomPubHash: x.denom.denomPubHash,
+          };
+        }),
+      },
     };
     await ws.db.put(Stores.tips, tipRecord);
   }
@@ -185,18 +203,21 @@ async function processTipImpl(
     return;
   }
 
-  if (!tipRecord.planchets) {
-    await updateExchangeFromUrl(ws, tipRecord.exchangeUrl);
-    const denomsForWithdraw = await getVerifiedWithdrawDenomList(
-      ws,
-      tipRecord.exchangeUrl,
-      tipRecord.amount,
-    );
+  const denomsForWithdraw = tipRecord.denomsSel;
 
-    const planchets = await Promise.all(
-      denomsForWithdraw.map((d) => ws.cryptoApi.createTipPlanchet(d)),
-    );
+  if (!tipRecord.planchets) {
+    const planchets: TipPlanchet[] = [];
 
+    for (const sd of denomsForWithdraw.selectedDenoms) {
+      const denom = await 
ws.db.getIndexed(Stores.denominations.denomPubHashIndex, sd.denomPubHash);
+      if (!denom) {
+        throw Error("denom does not exist anymore");
+      }
+      for (let i = 0; i < sd.countAllocated; i++) {
+        const r = await ws.cryptoApi.createTipPlanchet(denom);
+        planchets.push(r);
+      }
+    }
     await ws.db.mutate(Stores.tips, tipId, (r) => {
       if (!r.planchets) {
         r.planchets = planchets;
@@ -244,6 +265,7 @@ async function processTipImpl(
     throw Error("number of tip responses does not match requested planchets");
   }
 
+  const withdrawalGroupId = encodeCrock(getRandomBytes(32));
   const planchets: PlanchetRecord[] = [];
 
   for (let i = 0; i < tipRecord.planchets.length; i++) {
@@ -261,16 +283,15 @@ async function processTipImpl(
       withdrawSig: response.reserve_sigs[i].reserve_sig,
       isFromTip: true,
       coinEvHash,
+      coinIdx: i,
+      withdrawalDone: false,
+      withdrawalGroupId: withdrawalGroupId,
     };
     planchets.push(planchet);
   }
 
-  const withdrawalGroupId = encodeCrock(getRandomBytes(32));
-
   const withdrawalGroup: WithdrawalGroupRecord = {
-    denoms: planchets.map((x) => x.denomPub),
     exchangeBaseUrl: tipRecord.exchangeUrl,
-    planchets: planchets,
     source: {
       type: WithdrawalSourceType.Tip,
       tipId: tipRecord.tipId,
@@ -278,12 +299,11 @@ async function processTipImpl(
     timestampStart: getTimestampNow(),
     withdrawalGroupId: withdrawalGroupId,
     rawWithdrawalAmount: tipRecord.amount,
-    withdrawn: planchets.map((x) => false),
-    totalCoinValue: Amounts.sum(planchets.map((p) => p.coinValue)).amount,
     lastErrorPerCoin: {},
     retryInfo: initRetryInfo(),
     timestampFinish: undefined,
     lastError: undefined,
+    denomsSel: tipRecord.denomsSel,
   };
 
   await ws.db.runWithWriteTransaction(
@@ -301,12 +321,13 @@ async function processTipImpl(
 
       await tx.put(Stores.tips, tr);
       await tx.put(Stores.withdrawalGroups, withdrawalGroup);
+      for (const p of planchets) {
+        await tx.put(Stores.planchets, p);
+      }
     },
   );
 
   await processWithdrawGroup(ws, withdrawalGroupId);
-
-  return;
 }
 
 export async function acceptTip(
diff --git a/src/operations/withdraw.ts b/src/operations/withdraw.ts
index 1f5bfd0b..7c079dc4 100644
--- a/src/operations/withdraw.ts
+++ b/src/operations/withdraw.ts
@@ -14,7 +14,7 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { AmountJson } from "../util/amounts";
+import { AmountJson, Amounts } from "../util/amounts";
 import {
   DenominationRecord,
   Stores,
@@ -24,8 +24,8 @@ import {
   initRetryInfo,
   updateRetryInfoTimeout,
   CoinSourceType,
+  DenominationSelectionInfo,
 } from "../types/dbTypes";
-import * as Amounts from "../util/amounts";
 import {
   BankWithdrawDetails,
   ExchangeWithdrawDetails,
@@ -74,33 +74,52 @@ function isWithdrawableDenom(d: DenominationRecord): 
boolean {
 export function getWithdrawDenomList(
   amountAvailable: AmountJson,
   denoms: DenominationRecord[],
-): DenominationRecord[] {
+): DenominationSelectionInfo {
   let remaining = Amounts.copy(amountAvailable);
-  const ds: DenominationRecord[] = [];
+
+  const selectedDenoms: {
+    count: number;
+    denom: DenominationRecord;
+  }[] = [];
+
+  let totalCoinValue = Amounts.getZero(amountAvailable.currency);
+  let totalWithdrawCost = Amounts.getZero(amountAvailable.currency);
 
   denoms = denoms.filter(isWithdrawableDenom);
   denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value));
 
-  // This is an arbitrary number of coins
-  // we can withdraw in one go.  It's not clear if this limit
-  // is useful ...
-  for (let i = 0; i < 1000; i++) {
-    let found = false;
-    for (const d of denoms) {
-      const cost = Amounts.add(d.value, d.feeWithdraw).amount;
+  for (const d of denoms) {
+    let count = 0;
+    const cost = Amounts.add(d.value, d.feeWithdraw).amount;
+    for (;;) {
       if (Amounts.cmp(remaining, cost) < 0) {
-        continue;
+        break;
       }
-      found = true;
       remaining = Amounts.sub(remaining, cost).amount;
-      ds.push(d);
-      break;
+      count++;
     }
-    if (!found) {
+    if (count > 0) {
+      totalCoinValue = Amounts.add(
+        totalCoinValue,
+        Amounts.mult(d.value, count).amount,
+      ).amount;
+      totalWithdrawCost = Amounts.add(totalWithdrawCost, cost).amount;
+      selectedDenoms.push({
+        count,
+        denom: d,
+      });
+    }
+
+    if (Amounts.isZero(remaining)) {
       break;
     }
   }
-  return ds;
+
+  return {
+    selectedDenoms,
+    totalCoinValue,
+    totalWithdrawCost,
+  };
 }
 
 /**
@@ -167,14 +186,18 @@ async function processPlanchet(
   if (!withdrawalGroup) {
     return;
   }
-  if (withdrawalGroup.withdrawn[coinIdx]) {
-    return;
-  }
-  const planchet = withdrawalGroup.planchets[coinIdx];
+  const planchet = await ws.db.getIndexed(Stores.planchets.byGroupAndIndex, [
+    withdrawalGroupId,
+    coinIdx,
+  ]);
   if (!planchet) {
     console.log("processPlanchet: planchet not found");
     return;
   }
+  if (planchet.withdrawalDone) {
+    console.log("processPlanchet: planchet already withdrawn");
+    return;
+  }
   const exchange = await ws.db.get(
     Stores.exchanges,
     withdrawalGroup.exchangeBaseUrl,
@@ -243,25 +266,32 @@ async function processPlanchet(
   let withdrawalGroupFinished = false;
 
   const success = await ws.db.runWithWriteTransaction(
-    [Stores.coins, Stores.withdrawalGroups, Stores.reserves],
+    [Stores.coins, Stores.withdrawalGroups, Stores.reserves, Stores.planchets],
     async (tx) => {
       const ws = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
       if (!ws) {
         return false;
       }
-      if (ws.withdrawn[coinIdx]) {
+      const p = await tx.get(Stores.planchets, planchet.coinPub);
+      if (!p) {
+        return false;
+      }
+      if (p.withdrawalDone) {
         // Already withdrawn
         return false;
       }
-      ws.withdrawn[coinIdx] = true;
-      delete ws.lastErrorPerCoin[coinIdx];
-      let numDone = 0;
-      for (let i = 0; i < ws.withdrawn.length; i++) {
-        if (ws.withdrawn[i]) {
-          numDone++;
+      p.withdrawalDone = true;
+      await tx.put(Stores.planchets, p);
+
+      let numNotDone = 0;
+
+      await tx.iterIndexed(Stores.planchets.byGroup, 
withdrawalGroupId).forEach((x) => {
+        if (!x.withdrawalDone) {
+          numNotDone++;
         }
-      }
-      if (numDone === ws.denoms.length) {
+      });
+
+      if (numNotDone == 0) {
         ws.timestampFinish = getTimestampNow();
         ws.lastError = undefined;
         ws.retryInfo = initRetryInfo(false);
@@ -298,7 +328,7 @@ export async function getVerifiedWithdrawDenomList(
   ws: InternalWalletState,
   exchangeBaseUrl: string,
   amount: AmountJson,
-): Promise<DenominationRecord[]> {
+): Promise<DenominationSelectionInfo> {
   const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
   if (!exchange) {
     console.log("exchange not found");
@@ -318,25 +348,23 @@ export async function getVerifiedWithdrawDenomList(
 
   let allValid = false;
 
-  let selectedDenoms: DenominationRecord[];
+  let selectedDenoms: DenominationSelectionInfo;
 
   do {
     allValid = true;
     const nextPossibleDenoms = [];
     selectedDenoms = getWithdrawDenomList(amount, possibleDenoms);
     console.log("got withdraw denom list");
-    for (const denom of selectedDenoms || []) {
+    if (!selectedDenoms) {
+      console;
+    }
+    for (const denomSel of selectedDenoms.selectedDenoms) {
+      const denom = denomSel.denom;
       if (denom.status === DenominationStatus.Unverified) {
-        console.log(
-          "checking validity",
-          denom,
-          exchangeDetails.masterPublicKey,
-        );
         const valid = await ws.cryptoApi.isValidDenom(
           denom,
           exchangeDetails.masterPublicKey,
         );
-        console.log("done checking validity");
         if (!valid) {
           denom.status = DenominationStatus.VerifiedBad;
           allValid = false;
@@ -349,7 +377,7 @@ export async function getVerifiedWithdrawDenomList(
         nextPossibleDenoms.push(denom);
       }
     }
-  } while (selectedDenoms.length > 0 && !allValid);
+  } while (selectedDenoms.selectedDenoms.length > 0 && !allValid);
 
   console.log("returning denoms");
 
@@ -402,6 +430,23 @@ async function resetWithdrawalGroupRetry(
   });
 }
 
+async function processInBatches(workGen: Iterator<Promise<void>>, batchSize: 
number): Promise<void> {
+  for (;;) {
+    const batch: Promise<void>[] = [];
+    for (let i = 0; i < batchSize; i++) {
+      const wn = workGen.next();
+      if (wn.done) {
+        break;
+      }
+      batch.push(wn.value);
+    }
+    if (batch.length == 0) {
+      break;
+    }
+    await Promise.all(batch);
+  }
+}
+
 async function processWithdrawGroupImpl(
   ws: InternalWalletState,
   withdrawalGroupId: string,
@@ -420,11 +465,21 @@ async function processWithdrawGroupImpl(
     return;
   }
 
-  const ps = withdrawalGroup.denoms.map((d, i) =>
-    processPlanchet(ws, withdrawalGroupId, i),
-  );
-  await Promise.all(ps);
-  return;
+  const numDenoms = withdrawalGroup.denomsSel.selectedDenoms.length;
+  const genWork = function*(): Iterator<Promise<void>> {
+    let coinIdx = 0;
+    for (let i = 0; i < numDenoms; i++) {
+      const count = withdrawalGroup.denomsSel.selectedDenoms[i].countAllocated;
+      for (let j = 0; j < count; j++) {
+        yield processPlanchet(ws, withdrawalGroupId, coinIdx);
+        coinIdx++;
+      }
+    }
+  }
+
+  // Withdraw coins in batches.
+  // The batch size is relatively large
+  await processInBatches(genWork(), 50);
 }
 
 export async function getExchangeWithdrawalInfo(
@@ -447,14 +502,6 @@ export async function getExchangeWithdrawalInfo(
     baseUrl,
     amount,
   );
-  let acc = Amounts.getZero(amount.currency);
-  for (const d of selectedDenoms) {
-    acc = Amounts.add(acc, d.feeWithdraw).amount;
-  }
-  const actualCoinCost = selectedDenoms
-    .map((d: DenominationRecord) => Amounts.add(d.value, d.feeWithdraw).amount)
-    .reduce((a, b) => Amounts.add(a, b).amount);
-
   const exchangeWireAccounts: string[] = [];
   for (const account of exchangeWireInfo.accounts) {
     exchangeWireAccounts.push(account.payto_uri);
@@ -462,9 +509,11 @@ export async function getExchangeWithdrawalInfo(
 
   const { isTrusted, isAudited } = await getExchangeTrust(ws, exchangeInfo);
 
-  let earliestDepositExpiration = selectedDenoms[0].stampExpireDeposit;
-  for (let i = 1; i < selectedDenoms.length; i++) {
-    const expireDeposit = selectedDenoms[i].stampExpireDeposit;
+  let earliestDepositExpiration =
+    selectedDenoms.selectedDenoms[0].denom.stampExpireDeposit;
+  for (let i = 1; i < selectedDenoms.selectedDenoms.length; i++) {
+    const expireDeposit =
+      selectedDenoms.selectedDenoms[i].denom.stampExpireDeposit;
     if (expireDeposit.t_ms < earliestDepositExpiration.t_ms) {
       earliestDepositExpiration = expireDeposit;
     }
@@ -512,6 +561,11 @@ export async function getExchangeWithdrawalInfo(
     }
   }
 
+  const withdrawFee = Amounts.sub(
+    selectedDenoms.totalWithdrawCost,
+    selectedDenoms.totalCoinValue,
+  ).amount;
+
   const ret: ExchangeWithdrawDetails = {
     earliestDepositExpiration,
     exchangeInfo,
@@ -520,13 +574,13 @@ export async function getExchangeWithdrawalInfo(
     isAudited,
     isTrusted,
     numOfferedDenoms: possibleDenoms.length,
-    overhead: Amounts.sub(amount, actualCoinCost).amount,
+    overhead: Amounts.sub(amount, selectedDenoms.totalWithdrawCost).amount,
     selectedDenoms,
     trustedAuditorPubs,
     versionMatch,
     walletVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
     wireFees: exchangeWireInfo,
-    withdrawFee: acc,
+    withdrawFee,
     termsOfServiceAccepted: tosAccepted,
   };
   return ret;
diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts
index 158d438c..df019fc0 100644
--- a/src/types/dbTypes.ts
+++ b/src/types/dbTypes.ts
@@ -1,17 +1,17 @@
 /*
- This file is part of TALER
- (C) 2018 GNUnet e.V. and INRIA
+ This file is part of GNU Taler
+ (C) 2018-2020 Taler Systems S.A.
 
- TALER is free software; you can redistribute it and/or modify it under the
+ 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.
 
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ 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
- TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
 /**
@@ -608,7 +608,25 @@ export interface PlanchetRecord {
    * Public key of the coin.
    */
   coinPub: string;
+
+  /**
+   * Private key of the coin.
+   */
   coinPriv: string;
+
+  /**
+   * Withdrawal group that this planchet belongs to
+   * (or the empty string).
+   */
+  withdrawalGroupId: string;
+
+  /**
+   * Index within the withdrawal group (or -1).
+   */
+  coinIdx: number;
+
+  withdrawalDone: boolean;
+
   /**
    * Public key of the reserve, this might be a reserve not
    * known to the wallet if the planchet is from a tip.
@@ -889,6 +907,8 @@ export interface TipRecord {
    */
   planchets?: TipPlanchet[];
 
+  denomsSel: DenomSelectionState;
+
   /**
    * Response if the merchant responded,
    * undefined otherwise.
@@ -1356,6 +1376,28 @@ export interface WithdrawalSourceReserve {
 
 export type WithdrawalSource = WithdrawalSourceTip | WithdrawalSourceReserve;
 
+export interface DenominationSelectionInfo {
+  totalCoinValue: AmountJson;
+  totalWithdrawCost: AmountJson;
+  selectedDenoms: {
+    /**
+     * How many times do we withdraw this denomination?
+     */
+    count: number;
+    denom: DenominationRecord;
+  }[];
+}
+
+export interface DenomSelectionState {
+  totalCoinValue: AmountJson;
+  totalWithdrawCost: AmountJson;
+  selectedDenoms: {
+    denomPubHash: string;
+    countAllocated: number;
+    countPlanchetCreated: number;
+  }[];
+}
+
 export interface WithdrawalGroupRecord {
   withdrawalGroupId: string;
 
@@ -1379,22 +1421,13 @@ export interface WithdrawalGroupRecord {
    */
   timestampFinish?: Timestamp;
 
-  totalCoinValue: AmountJson;
-
   /**
    * Amount including fees (i.e. the amount subtracted from the
    * reserve to withdraw all coins in this withdrawal session).
    */
   rawWithdrawalAmount: AmountJson;
 
-  denoms: string[];
-
-  planchets: (undefined | PlanchetRecord)[];
-
-  /**
-   * Coins in this session that are withdrawn are set to true.
-   */
-  withdrawn: boolean[];
+  denomsSel: DenomSelectionState;
 
   /**
    * Retry info, always present even on completed operations so that indexing 
works.
@@ -1625,6 +1658,22 @@ export namespace Stores {
     }
   }
 
+  class PlanchetsStore extends Store<PlanchetRecord> {
+    constructor() {
+      super("planchets", { keyPath: "coinPub" });
+    }
+    byGroupAndIndex = new Index<string, PlanchetRecord>(
+      this,
+      "withdrawalGroupAndCoinIdxIndex",
+      ["withdrawalGroupId", "coinIdx"],
+    );
+    byGroup = new Index<string, PlanchetRecord>(
+      this,
+      "withdrawalGroupIndex",
+      "withdrawalGroupId",
+    );
+  }
+
   class RefundEventsStore extends Store<RefundEventRecord> {
     constructor() {
       super("refundEvents", { keyPath: "refundGroupId" });
@@ -1681,6 +1730,7 @@ export namespace Stores {
   export const tips = new TipsStore();
   export const senderWires = new SenderWiresStore();
   export const withdrawalGroups = new WithdrawalGroupsStore();
+  export const planchets = new PlanchetsStore();
   export const bankWithdrawUris = new BankWithdrawUrisStore();
   export const refundEvents = new RefundEventsStore();
   export const payEvents = new PayEventsStore();
diff --git a/src/types/walletTypes.ts b/src/types/walletTypes.ts
index ed334bc4..da87b1c1 100644
--- a/src/types/walletTypes.ts
+++ b/src/types/walletTypes.ts
@@ -30,9 +30,9 @@
 import { AmountJson, codecForAmountJson } from "../util/amounts";
 import * as LibtoolVersion from "../util/libtoolVersion";
 import {
-  DenominationRecord,
   ExchangeRecord,
   ExchangeWireInfo,
+  DenominationSelectionInfo,
 } from "./dbTypes";
 import { Timestamp } from "../util/time";
 import {
@@ -77,7 +77,7 @@ export interface ExchangeWithdrawDetails {
   /**
    * Selected denominations for withdraw.
    */
-  selectedDenoms: DenominationRecord[];
+  selectedDenoms: DenominationSelectionInfo;
 
   /**
    * Fees for withdraw.
diff --git a/src/util/amounts.ts b/src/util/amounts.ts
index 5953f513..d962b6cb 100644
--- a/src/util/amounts.ts
+++ b/src/util/amounts.ts
@@ -332,6 +332,33 @@ function check(a: any): boolean {
   }
 }
 
+function mult(a: AmountJson, n: number): Result {
+  if (!Number.isInteger(n)) {
+    throw Error("amount can only be multipied by an integer");
+  }
+  if (n < 0) {
+    throw Error("amount can only be multiplied by a positive integer");
+  }
+  if (n == 0) {
+    return { amount: getZero(a.currency), saturated: false };
+  }
+  let acc = {... a};
+  while (n > 1) {
+    let r: Result;
+    if (n % 2 == 0) {
+      n = n / 2;
+      r = add(acc, acc);
+    } else {
+      r = add(acc, a);
+    }
+    if (r.saturated) {
+      return r;
+    }
+    acc = r.amount;
+  }
+  return { amount: acc, saturated: false };
+}
+
 // Export all amount-related functions here for better IDE experience.
 export const Amounts = {
   stringify: stringify,
@@ -341,9 +368,11 @@ export const Amounts = {
   add: add,
   sum: sum,
   sub: sub,
+  mult: mult,
   check: check,
   getZero: getZero,
   isZero: isZero,
   maxAmountValue: maxAmountValue,
   fromFloat: fromFloat,
+  copy: copy,
 };
diff --git a/src/util/query.ts b/src/util/query.ts
index 401d22e7..be319049 100644
--- a/src/util/query.ts
+++ b/src/util/query.ts
@@ -436,9 +436,6 @@ export function openDatabase(
         throw Error("upgrade needed, but new version unknown");
       }
       onUpgradeNeeded(db, e.oldVersion, newVersion);
-      console.log(
-        `DB: upgrade needed: oldVersion=${e.oldVersion}, 
newVersion=${e.newVersion}`,
-      );
     };
   });
 }
diff --git a/src/wallet.ts b/src/wallet.ts
index 41569a44..3558e102 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -252,14 +252,19 @@ export class Wallet {
    * returns without resolving to an exception.
    */
   public async runUntilDone(): Promise<void> {
+    let done = false;
     const p = new Promise((resolve, reject) => {
       // Run this asynchronously
       this.addNotificationListener((n) => {
+        if (done) {
+          return;
+        }
         if (
           n.type === NotificationType.WaitingForRetry &&
           n.numGivingLiveness == 0
         ) {
-          logger.trace("no liveness-giving operations left, returning");
+          done = true;
+          logger.trace("no liveness-giving operations left");
           resolve();
         }
       });
@@ -277,23 +282,9 @@ export class Wallet {
    * returns without resolving to an exception.
    */
   public async runUntilDoneAndStop(): Promise<void> {
-    const p = new Promise((resolve, reject) => {
-      // Run this asynchronously
-      this.addNotificationListener((n) => {
-        if (
-          n.type === NotificationType.WaitingForRetry &&
-          n.numGivingLiveness == 0
-        ) {
-          logger.trace("no liveness-giving operations left, stopping");
-          this.stop();
-        }
-      });
-      this.runRetryLoop().catch((e) => {
-        console.log("exception in wallet retry loop");
-        reject(e);
-      });
-    });
-    await p;
+    await this.runUntilDone();
+    logger.trace("stopping after liveness-giving operations done");
+    this.stop();
   }
 
   /**
@@ -314,9 +305,7 @@ export class Wallet {
 
   private async runRetryLoopImpl(): Promise<void> {
     while (!this.stopped) {
-      console.log("running wallet retry loop iteration");
       const pending = await this.getPendingOperations({ onlyDue: true });
-      console.log("pending ops", JSON.stringify(pending, undefined, 2));
       if (pending.pendingOperations.length === 0) {
         const allPending = await this.getPendingOperations({ onlyDue: false });
         let numPending = 0;
@@ -346,12 +335,10 @@ export class Wallet {
         await Promise.race([timeout, this.latch.wait()]);
         console.log("timeout done");
       } else {
-        logger.trace("running pending operations that are due");
         // FIXME: maybe be a bit smarter about executing these
         // operations in parallel?
         for (const p of pending.pendingOperations) {
           try {
-            console.log("running", p);
             await this.processOnePendingOperation(p);
           } catch (e) {
             console.error(e);
diff --git a/src/webex/renderHtml.tsx b/src/webex/renderHtml.tsx
index a56af37f..39ff470a 100644
--- a/src/webex/renderHtml.tsx
+++ b/src/webex/renderHtml.tsx
@@ -25,7 +25,6 @@
  */
 import { AmountJson } from "../util/amounts";
 import * as Amounts from "../util/amounts";
-import { DenominationRecord } from "../types/dbTypes";
 import { ExchangeWithdrawDetails } from "../types/walletTypes";
 import * as i18n from "./i18n";
 import React from "react";
@@ -208,31 +207,6 @@ function FeeDetailsView(props: {
   }
 
   const denoms = rci.selectedDenoms;
-
-  const countByPub: { [s: string]: number } = {};
-  const uniq: DenominationRecord[] = [];
-
-  denoms.forEach((x: DenominationRecord) => {
-    let c = countByPub[x.denomPub] || 0;
-    if (c === 0) {
-      uniq.push(x);
-    }
-    c += 1;
-    countByPub[x.denomPub] = c;
-  });
-
-  function row(denom: DenominationRecord): JSX.Element {
-    return (
-      <tr>
-        <td>{countByPub[denom.denomPub] + "x"}</td>
-        <td>{renderAmount(denom.value)}</td>
-        <td>{renderAmount(denom.feeWithdraw)}</td>
-        <td>{renderAmount(denom.feeRefresh)}</td>
-        <td>{renderAmount(denom.feeDeposit)}</td>
-      </tr>
-    );
-  }
-
   const withdrawFee = renderAmount(rci.withdrawFee);
   const overhead = renderAmount(rci.overhead);
 
@@ -266,7 +240,19 @@ function FeeDetailsView(props: {
               <th>{i18n.str`Deposit Fee`}</th>
             </tr>
           </thead>
-          <tbody>{uniq.map(row)}</tbody>
+          <tbody>
+            {denoms.selectedDenoms.map((ds) => {
+              return (
+                <tr key={ds.denom.denomPub}>
+                  <td>{ds.count + "x"}</td>
+                  <td>{renderAmount(ds.denom.value)}</td>
+                  <td>{renderAmount(ds.denom.feeWithdraw)}</td>
+                  <td>{renderAmount(ds.denom.feeRefresh)}</td>
+                  <td>{renderAmount(ds.denom.feeDeposit)}</td>
+                </tr>
+              );
+            })}
+          </tbody>
         </table>
       </div>
       <h3>Wire Fees</h3>

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



reply via email to

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