gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 02/02: full recoup, untested/unfinished first attemp


From: gnunet
Subject: [taler-wallet-core] 02/02: full recoup, untested/unfinished first attempt
Date: Wed, 11 Mar 2020 20:14:35 +0100

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

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

commit 2c52046f0bf358a5e07c53394b3b72d091356cce
Author: Florian Dold <address@hidden>
AuthorDate: Thu Mar 12 00:44:28 2020 +0530

    full recoup, untested/unfinished first attempt
---
 src/crypto/workers/cryptoApi.ts            |   7 +-
 src/crypto/workers/cryptoImplementation.ts |  14 +-
 src/headless/taler-wallet-cli.ts           |   1 +
 src/operations/exchanges.ts                |  43 ++++
 src/operations/history.ts                  |  11 +
 src/operations/pending.ts                  |  28 +++
 src/operations/recoup.ts                   | 372 +++++++++++++++++++++++++----
 src/operations/refresh.ts                  |   8 +-
 src/operations/reserves.ts                 |   1 -
 src/operations/state.ts                    |   1 +
 src/operations/withdraw.ts                 |  26 +-
 src/types/dbTypes.ts                       | 123 +++++++---
 src/types/history.ts                       |  14 +-
 src/types/notifications.ts                 |   9 +-
 src/types/pending.ts                       |   6 +
 src/types/talerTypes.ts                    |  58 +++--
 src/types/walletTypes.ts                   |   6 +-
 src/util/query.ts                          |   8 +
 src/wallet.ts                              |   9 +-
 src/webex/messages.ts                      |   8 -
 src/webex/pages/payback.tsx                |  40 +---
 src/webex/wxApi.ts                         | 113 +++++----
 src/webex/wxBackend.ts                     |   9 -
 23 files changed, 655 insertions(+), 260 deletions(-)

diff --git a/src/crypto/workers/cryptoApi.ts b/src/crypto/workers/cryptoApi.ts
index 489d56f5..4adf2882 100644
--- a/src/crypto/workers/cryptoApi.ts
+++ b/src/crypto/workers/cryptoApi.ts
@@ -30,12 +30,11 @@ import {
   RefreshSessionRecord,
   TipPlanchet,
   WireFee,
-  WalletContractData,
 } from "../../types/dbTypes";
 
 import { CryptoWorker } from "./cryptoWorker";
 
-import { ContractTerms, PaybackRequest, CoinDepositPermission } from 
"../../types/talerTypes";
+import { RecoupRequest, CoinDepositPermission } from "../../types/talerTypes";
 
 import {
   BenchmarkResult,
@@ -409,8 +408,8 @@ export class CryptoApi {
     return this.doRpc<boolean>("isValidWireAccount", 4, paytoUri, sig, 
masterPub);
   }
 
-  createPaybackRequest(coin: CoinRecord): Promise<PaybackRequest> {
-    return this.doRpc<PaybackRequest>("createPaybackRequest", 1, coin);
+  createRecoupRequest(coin: CoinRecord): Promise<RecoupRequest> {
+    return this.doRpc<RecoupRequest>("createRecoupRequest", 1, coin);
   }
 
   createRefreshSession(
diff --git a/src/crypto/workers/cryptoImplementation.ts 
b/src/crypto/workers/cryptoImplementation.ts
index 22004620..3447c56f 100644
--- a/src/crypto/workers/cryptoImplementation.ts
+++ b/src/crypto/workers/cryptoImplementation.ts
@@ -31,9 +31,10 @@ import {
   RefreshSessionRecord,
   TipPlanchet,
   WireFee,
+  CoinSourceType,
 } from "../../types/dbTypes";
 
-import { CoinDepositPermission, ContractTerms, PaybackRequest } from 
"../../types/talerTypes";
+import { CoinDepositPermission, RecoupRequest } from "../../types/talerTypes";
 import {
   BenchmarkResult,
   PlanchetCreationResult,
@@ -73,7 +74,7 @@ enum SignaturePurpose {
   WALLET_COIN_MELT = 1202,
   TEST = 4242,
   MERCHANT_PAYMENT_OK = 1104,
-  WALLET_COIN_PAYBACK = 1203,
+  WALLET_COIN_RECOUP = 1203,
   WALLET_COIN_LINK = 1204,
 }
 
@@ -198,10 +199,10 @@ export class CryptoImplementation {
   }
 
   /**
-   * Create and sign a message to request payback for a coin.
+   * Create and sign a message to recoup a coin.
    */
-  createPaybackRequest(coin: CoinRecord): PaybackRequest {
-    const p = buildSigPS(SignaturePurpose.WALLET_COIN_PAYBACK)
+  createRecoupRequest(coin: CoinRecord): RecoupRequest {
+    const p = buildSigPS(SignaturePurpose.WALLET_COIN_RECOUP)
       .put(decodeCrock(coin.coinPub))
       .put(decodeCrock(coin.denomPubHash))
       .put(decodeCrock(coin.blindingKey))
@@ -209,12 +210,13 @@ export class CryptoImplementation {
 
     const coinPriv = decodeCrock(coin.coinPriv);
     const coinSig = eddsaSign(p, coinPriv);
-    const paybackRequest: PaybackRequest = {
+    const paybackRequest: RecoupRequest = {
       coin_blind_key_secret: coin.blindingKey,
       coin_pub: coin.coinPub,
       coin_sig: encodeCrock(coinSig),
       denom_pub: coin.denomPub,
       denom_sig: coin.denomSig,
+      refreshed: (coin.coinSource.type === CoinSourceType.Refresh),
     };
     return paybackRequest;
   }
diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
index 9abdb05d..70784995 100644
--- a/src/headless/taler-wallet-cli.ts
+++ b/src/headless/taler-wallet-cli.ts
@@ -365,6 +365,7 @@ advancedCli
         console.log(`coin ${coin.coinPub}`);
         console.log(` status ${coin.status}`);
         console.log(` exchange ${coin.exchangeBaseUrl}`);
+        console.log(` denomPubHash ${coin.denomPubHash}`);
         console.log(
           ` remaining amount ${Amounts.toString(coin.currentAmount)}`,
         );
diff --git a/src/operations/exchanges.ts b/src/operations/exchanges.ts
index cf6b0686..ed13a1e5 100644
--- a/src/operations/exchanges.ts
+++ b/src/operations/exchanges.ts
@@ -31,6 +31,7 @@ import {
   WireFee,
   ExchangeUpdateReason,
   ExchangeUpdatedEventRecord,
+  CoinStatus,
 } from "../types/dbTypes";
 import { canonicalizeBaseUrl } from "../util/helpers";
 import * as Amounts from "../util/amounts";
@@ -45,6 +46,7 @@ import {
 } from "./versions";
 import { getTimestampNow } from "../util/time";
 import { compare } from "../util/libtoolVersion";
+import { createRecoupGroup, processRecoupGroup } from "./recoup";
 
 async function denominationRecordFromKeys(
   ws: InternalWalletState,
@@ -61,6 +63,7 @@ async function denominationRecordFromKeys(
     feeRefund: Amounts.parseOrThrow(denomIn.fee_refund),
     feeWithdraw: Amounts.parseOrThrow(denomIn.fee_withdraw),
     isOffered: true,
+    isRevoked: false,
     masterSig: denomIn.master_sig,
     stampExpireDeposit: denomIn.stamp_expire_deposit,
     stampExpireLegal: denomIn.stamp_expire_legal,
@@ -189,6 +192,8 @@ async function updateExchangeWithKeys(
     ),
   );
 
+  let recoupGroupId: string | undefined = undefined;
+
   await ws.db.runWithWriteTransaction(
     [Stores.exchanges, Stores.denominations],
     async tx => {
@@ -222,8 +227,46 @@ async function updateExchangeWithKeys(
           await tx.put(Stores.denominations, newDenom);
         }
       }
+
+      // Handle recoup
+      const recoupDenomList = exchangeKeysJson.recoup ?? [];
+      const newlyRevokedCoinPubs: string[] = [];
+      for (const recoupDenomPubHash of recoupDenomList) {
+        const oldDenom = await tx.getIndexed(
+          Stores.denominations.denomPubHashIndex,
+          recoupDenomPubHash,
+        );
+        if (!oldDenom) {
+          // We never even knew about the revoked denomination, all good.
+          continue;
+        }
+        if (oldDenom.isRevoked) {
+          // We already marked the denomination as revoked,
+          // this implies we revoked all coins
+          continue;
+        }
+        oldDenom.isRevoked = true;
+        await tx.put(Stores.denominations, oldDenom);
+        const affectedCoins = await tx
+          .iterIndexed(Stores.coins.denomPubIndex)
+          .toArray();
+        for (const ac of affectedCoins) {
+          newlyRevokedCoinPubs.push(ac.coinPub);
+        }
+      }
+      if (newlyRevokedCoinPubs.length != 0) {
+        await createRecoupGroup(ws, tx, newlyRevokedCoinPubs);
+      }
     },
   );
+
+  if (recoupGroupId) {
+    // Asynchronously start recoup.  This doesn't need to finish
+    // for the exchange update to be considered finished.
+    processRecoupGroup(ws, recoupGroupId).catch((e) => {
+      console.log("error while recouping coins:", e);
+    });
+  }
 }
 
 async function updateExchangeFinalize(
diff --git a/src/operations/history.ts b/src/operations/history.ts
index 2fb7854d..2cf215a5 100644
--- a/src/operations/history.ts
+++ b/src/operations/history.ts
@@ -181,6 +181,7 @@ export async function getHistory(
       Stores.payEvents,
       Stores.refundEvents,
       Stores.reserveUpdatedEvents,
+      Stores.recoupGroups,
     ],
     async tx => {
       tx.iter(Stores.exchanges).forEach(exchange => {
@@ -485,6 +486,16 @@ export async function getHistory(
           amountRefundedInvalid: Amounts.toString(amountRefundedInvalid),
         });
       });
+
+      tx.iter(Stores.recoupGroups).forEach(rg => {
+        if (rg.timestampFinished) {
+          history.push({
+            type: HistoryEventType.FundsRecouped,
+            timestamp: rg.timestampFinished,
+            eventId: makeEventId(HistoryEventType.FundsRecouped, 
rg.recoupGroupId),
+          });
+        }
+      });
     },
   );
 
diff --git a/src/operations/pending.ts b/src/operations/pending.ts
index fce9a3bf..08ec3fc9 100644
--- a/src/operations/pending.ts
+++ b/src/operations/pending.ts
@@ -405,6 +405,32 @@ async function gatherPurchasePending(
   });
 }
 
+async function gatherRecoupPending(
+  tx: TransactionHandle,
+  now: Timestamp,
+  resp: PendingOperationsResponse,
+  onlyDue: boolean = false,
+): Promise<void> {
+  await tx.iter(Stores.recoupGroups).forEach(rg => {
+    if (rg.timestampFinished) {
+      return;
+    }
+    resp.nextRetryDelay = updateRetryDelay(
+      resp.nextRetryDelay,
+      now,
+      rg.retryInfo.nextRetry,
+    );
+    if (onlyDue && rg.retryInfo.nextRetry.t_ms > now.t_ms) {
+      return;
+    }
+    resp.pendingOperations.push({
+      type: PendingOperationType.Recoup,
+      givesLifeness: true,
+      recoupGroupId: rg.recoupGroupId,
+    });
+  });
+}
+
 export async function getPendingOperations(
   ws: InternalWalletState,
   { onlyDue = false } = {},
@@ -420,6 +446,7 @@ export async function getPendingOperations(
       Stores.proposals,
       Stores.tips,
       Stores.purchases,
+      Stores.recoupGroups,
     ],
     async tx => {
       const walletBalance = await getBalancesInsideTransaction(ws, tx);
@@ -436,6 +463,7 @@ export async function getPendingOperations(
       await gatherProposalPending(tx, now, resp, onlyDue);
       await gatherTipPending(tx, now, resp, onlyDue);
       await gatherPurchasePending(tx, now, resp, onlyDue);
+      await gatherRecoupPending(tx, now, resp, onlyDue);
       return resp;
     },
   );
diff --git a/src/operations/recoup.ts b/src/operations/recoup.ts
index 2b646a4d..842a67b8 100644
--- a/src/operations/recoup.ts
+++ b/src/operations/recoup.ts
@@ -1,6 +1,6 @@
 /*
  This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
+ (C) 2019-2010 Taler Systems SA
 
  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
@@ -14,76 +14,358 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
+/**
+ * Implementation of the recoup operation, which allows to recover the
+ * value of coins held in a revoked denomination.
+ *
+ * @author Florian Dold <address@hidden>
+ */
+
 /**
  * Imports.
  */
-import {
-  Database
-} from "../util/query";
 import { InternalWalletState } from "./state";
-import { Stores, TipRecord, CoinStatus } from "../types/dbTypes";
+import {
+  Stores,
+  CoinStatus,
+  CoinSourceType,
+  CoinRecord,
+  WithdrawCoinSource,
+  RefreshCoinSource,
+  ReserveRecordStatus,
+  RecoupGroupRecord,
+  initRetryInfo,
+  updateRetryInfoTimeout,
+} from "../types/dbTypes";
 
-import { Logger } from "../util/logging";
-import { RecoupConfirmation, codecForRecoupConfirmation } from 
"../types/talerTypes";
-import { updateExchangeFromUrl } from "./exchanges";
+import { codecForRecoupConfirmation } from "../types/talerTypes";
 import { NotificationType } from "../types/notifications";
+import { processReserve } from "./reserves";
 
-const logger = new Logger("payback.ts");
+import * as Amounts from "../util/amounts";
+import { createRefreshGroup, processRefreshGroup } from "./refresh";
+import { RefreshReason, OperationError } from "../types/walletTypes";
+import { TransactionHandle } from "../util/query";
+import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
+import { getTimestampNow } from "../util/time";
+import { guardOperationException } from "./errors";
 
-export async function recoup(
+async function incrementRecoupRetry(
   ws: InternalWalletState,
-  coinPub: string,
+  recoupGroupId: string,
+  err: OperationError | undefined,
 ): Promise<void> {
-  let coin = await ws.db.get(Stores.coins, coinPub);
-  if (!coin) {
-    throw Error(`Coin ${coinPub} not found, can't request payback`);
+  await ws.db.runWithWriteTransaction([Stores.recoupGroups], async tx => {
+    const r = await tx.get(Stores.recoupGroups, recoupGroupId);
+    if (!r) {
+      return;
+    }
+    if (!r.retryInfo) {
+      return;
+    }
+    r.retryInfo.retryCounter++;
+    updateRetryInfoTimeout(r.retryInfo);
+    r.lastError = err;
+    await tx.put(Stores.recoupGroups, r);
+  });
+  ws.notify({ type: NotificationType.RecoupOperationError });
+}
+
+async function putGroupAsFinished(
+  tx: TransactionHandle,
+  recoupGroup: RecoupGroupRecord,
+  coinIdx: number,
+): Promise<void> {
+  recoupGroup.recoupFinishedPerCoin[coinIdx] = true;
+  let allFinished = true;
+  for (const b of recoupGroup.recoupFinishedPerCoin) {
+    if (!b) {
+      allFinished = false;
+    }
   }
-  const reservePub = coin.reservePub;
-  if (!reservePub) {
-    throw Error(`Can't request payback for a refreshed coin`);
+  if (allFinished) {
+    recoupGroup.timestampFinished = getTimestampNow();
+    recoupGroup.retryInfo = initRetryInfo(false);
+    recoupGroup.lastError = undefined;
   }
+  await tx.put(Stores.recoupGroups, recoupGroup);
+}
+
+async function recoupTipCoin(
+  ws: InternalWalletState,
+  recoupGroupId: string,
+  coinIdx: number,
+  coin: CoinRecord,
+): Promise<void> {
+  // We can't really recoup a coin we got via tipping.
+  // Thus we just put the coin to sleep.
+  // FIXME: somehow report this to the user
+  await ws.db.runWithWriteTransaction([Stores.recoupGroups], async tx => {
+    const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId);
+    if (!recoupGroup) {
+      return;
+    }
+    if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
+      return;
+    }
+    await putGroupAsFinished(tx, recoupGroup, coinIdx);
+  });
+}
+
+async function recoupWithdrawCoin(
+  ws: InternalWalletState,
+  recoupGroupId: string,
+  coinIdx: number,
+  coin: CoinRecord,
+  cs: WithdrawCoinSource,
+): Promise<void> {
+  const reservePub = cs.reservePub;
   const reserve = await ws.db.get(Stores.reserves, reservePub);
   if (!reserve) {
-    throw Error(`Reserve of coin ${coinPub} not found`);
+    // FIXME:  We should at least emit some pending operation / warning for 
this?
+    return;
+  }
+
+  ws.notify({
+    type: NotificationType.RecoupStarted,
+  });
+
+  const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin);
+  const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, 
coin.exchangeBaseUrl);
+  const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
+  if (resp.status !== 200) {
+    throw Error("recoup request failed");
   }
-  switch (coin.status) {
-    case CoinStatus.Dormant:
-      throw Error(`Can't do payback for coin ${coinPub} since it's dormant`);
+  const recoupConfirmation = codecForRecoupConfirmation().decode(
+    await resp.json(),
+  );
+
+  if (recoupConfirmation.reserve_pub !== reservePub) {
+    throw Error(`Coin's reserve doesn't match reserve on recoup`);
   }
-  coin.status = CoinStatus.Dormant;
-  // Even if we didn't get the payback yet, we suspend withdrawal, since
-  // technically we might update reserve status before we get the response
-  // from the reserve for the payback request.
-  reserve.hasPayback = true;
+
+  // FIXME: verify that our expectations about the amount match
+
   await ws.db.runWithWriteTransaction(
-    [Stores.coins, Stores.reserves],
+    [Stores.coins, Stores.reserves, Stores.recoupGroups],
     async tx => {
-      await tx.put(Stores.coins, coin!!);
-      await tx.put(Stores.reserves, reserve);
+      const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId);
+      if (!recoupGroup) {
+        return;
+      }
+      if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
+        return;
+      }
+      const updatedCoin = await tx.get(Stores.coins, coin.coinPub);
+      if (!updatedCoin) {
+        return;
+      }
+      const updatedReserve = await tx.get(Stores.reserves, reserve.reservePub);
+      if (!updatedReserve) {
+        return;
+      }
+      updatedCoin.status = CoinStatus.Dormant;
+      const currency = updatedCoin.currentAmount.currency;
+      updatedCoin.currentAmount = Amounts.getZero(currency);
+      updatedReserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
+      await tx.put(Stores.coins, updatedCoin);
+      await tx.put(Stores.reserves, updatedReserve);
+      await putGroupAsFinished(tx, recoupGroup, coinIdx);
     },
   );
+
   ws.notify({
-    type: NotificationType.PaybackStarted,
+    type: NotificationType.RecoupFinished,
   });
 
-  const paybackRequest = await ws.cryptoApi.createPaybackRequest(coin);
-  const reqUrl = new URL("payback", coin.exchangeBaseUrl);
-  const resp = await ws.http.postJson(reqUrl.href, paybackRequest);
+  processReserve(ws, reserve.reservePub).catch(e => {
+    console.log("processing reserve after recoup failed:", e);
+  });
+}
+
+async function recoupRefreshCoin(
+  ws: InternalWalletState,
+  recoupGroupId: string,
+  coinIdx: number,
+  coin: CoinRecord,
+  cs: RefreshCoinSource,
+): Promise<void> {
+  ws.notify({
+    type: NotificationType.RecoupStarted,
+  });
+
+  const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin);
+  const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, 
coin.exchangeBaseUrl);
+  const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
   if (resp.status !== 200) {
-    throw Error();
+    throw Error("recoup request failed");
   }
-  const paybackConfirmation = codecForRecoupConfirmation().decode(await 
resp.json());
-  if (paybackConfirmation.reserve_pub !== coin.reservePub) {
-    throw Error(`Coin's reserve doesn't match reserve on payback`);
+  const recoupConfirmation = codecForRecoupConfirmation().decode(
+    await resp.json(),
+  );
+
+  if (recoupConfirmation.old_coin_pub != cs.oldCoinPub) {
+    throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`);
   }
-  coin = await ws.db.get(Stores.coins, coinPub);
-  if (!coin) {
-    throw Error(`Coin ${coinPub} not found, can't confirm payback`);
+
+  const refreshGroupId = await ws.db.runWithWriteTransaction(
+    [Stores.coins, Stores.reserves],
+    async tx => {
+      const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId);
+      if (!recoupGroup) {
+        return;
+      }
+      if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
+        return;
+      }
+      const oldCoin = await tx.get(Stores.coins, cs.oldCoinPub);
+      const updatedCoin = await tx.get(Stores.coins, coin.coinPub);
+      if (!updatedCoin) {
+        return;
+      }
+      if (!oldCoin) {
+        return;
+      }
+      updatedCoin.status = CoinStatus.Dormant;
+      oldCoin.currentAmount = Amounts.add(
+        oldCoin.currentAmount,
+        updatedCoin.currentAmount,
+      ).amount;
+      await tx.put(Stores.coins, updatedCoin);
+      await putGroupAsFinished(tx, recoupGroup, coinIdx);
+      return await createRefreshGroup(
+        tx,
+        [{ coinPub: oldCoin.coinPub }],
+        RefreshReason.Recoup,
+      );
+    },
+  );
+
+  if (refreshGroupId) {
+    processRefreshGroup(ws, refreshGroupId.refreshGroupId).then(e => {
+      console.error("error while refreshing after recoup", e);
+    });
   }
-  coin.status = CoinStatus.Dormant;
-  await ws.db.put(Stores.coins, coin);
-  ws.notify({
-    type: NotificationType.PaybackFinished,
+}
+
+async function resetRecoupGroupRetry(
+  ws: InternalWalletState,
+  recoupGroupId: string,
+) {
+  await ws.db.mutate(Stores.recoupGroups, recoupGroupId, x => {
+    if (x.retryInfo.active) {
+      x.retryInfo = initRetryInfo();
+    }
+    return x;
   });
-  await updateExchangeFromUrl(ws, coin.exchangeBaseUrl, true);
+}
+
+export async function processRecoupGroup(
+  ws: InternalWalletState,
+  recoupGroupId: string,
+  forceNow: boolean = false,
+): Promise<void> {
+  await ws.memoProcessRecoup.memo(recoupGroupId, async () => {
+    const onOpErr = (e: OperationError) =>
+      incrementRecoupRetry(ws, recoupGroupId, e);
+    return await guardOperationException(
+      async () => await processRecoupGroupImpl(ws, recoupGroupId, forceNow),
+      onOpErr,
+    );
+  });
+}
+
+async function processRecoupGroupImpl(
+  ws: InternalWalletState,
+  recoupGroupId: string,
+  forceNow: boolean = false,
+): Promise<void> {
+  if (forceNow) {
+    await resetRecoupGroupRetry(ws, recoupGroupId);
+  }
+  const recoupGroup = await ws.db.get(Stores.recoupGroups, recoupGroupId);
+  if (!recoupGroup) {
+    return;
+  }
+  if (recoupGroup.timestampFinished) {
+    return;
+  }
+  const ps = recoupGroup.coinPubs.map((x, i) =>
+    processRecoup(ws, recoupGroupId, i),
+  );
+  await Promise.all(ps);
+}
+
+export async function createRecoupGroup(
+  ws: InternalWalletState,
+  tx: TransactionHandle,
+  coinPubs: string[],
+): Promise<string> {
+  const recoupGroupId = encodeCrock(getRandomBytes(32));
+
+  const recoupGroup: RecoupGroupRecord = {
+    recoupGroupId,
+    coinPubs: coinPubs,
+    lastError: undefined,
+    timestampFinished: undefined,
+    timestampStarted: getTimestampNow(),
+    retryInfo: initRetryInfo(),
+    recoupFinishedPerCoin: coinPubs.map(() => false),
+  };
+
+  for (let coinIdx = 0; coinIdx < coinPubs.length; coinIdx++) {
+    const coinPub = coinPubs[coinIdx];
+    const coin = await tx.get(Stores.coins, coinPub);
+    if (!coin) {
+      recoupGroup.recoupFinishedPerCoin[coinIdx] = true;
+      continue;
+    }
+    if (Amounts.isZero(coin.currentAmount)) {
+      recoupGroup.recoupFinishedPerCoin[coinIdx] = true;
+      continue;
+    }
+    coin.currentAmount = Amounts.getZero(coin.currentAmount.currency);
+    await tx.put(Stores.coins, coin);
+  }
+
+  await tx.put(Stores.recoupGroups, recoupGroup);
+
+  return recoupGroupId;
+}
+
+async function processRecoup(
+  ws: InternalWalletState,
+  recoupGroupId: string,
+  coinIdx: number,
+): Promise<void> {
+  const recoupGroup = await ws.db.get(Stores.recoupGroups, recoupGroupId);
+  if (!recoupGroup) {
+    return;
+  }
+  if (recoupGroup.timestampFinished) {
+    return;
+  }
+  if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
+    return;
+  }
+
+  const coinPub = recoupGroup.coinPubs[coinIdx];
+
+  let coin = await ws.db.get(Stores.coins, coinPub);
+  if (!coin) {
+    throw Error(`Coin ${coinPub} not found, can't request payback`);
+  }
+
+  const cs = coin.coinSource;
+
+  switch (cs.type) {
+    case CoinSourceType.Tip:
+      return recoupTipCoin(ws, recoupGroupId, coinIdx, coin);
+    case CoinSourceType.Refresh:
+      return recoupRefreshCoin(ws, recoupGroupId, coinIdx, coin, cs);
+    case CoinSourceType.Withdraw:
+      return recoupWithdrawCoin(ws, recoupGroupId, coinIdx, coin, cs);
+    default:
+      throw Error("unknown coin source type");
+  }
 }
diff --git a/src/operations/refresh.ts b/src/operations/refresh.ts
index 6dd16d61..092d9f15 100644
--- a/src/operations/refresh.ts
+++ b/src/operations/refresh.ts
@@ -26,6 +26,7 @@ import {
   initRetryInfo,
   updateRetryInfoTimeout,
   RefreshGroupRecord,
+  CoinSourceType,
 } from "../types/dbTypes";
 import { amountToPretty } from "../util/helpers";
 import { Database, TransactionHandle } from "../util/query";
@@ -407,10 +408,11 @@ async function refreshReveal(
       denomPubHash: denom.denomPubHash,
       denomSig,
       exchangeBaseUrl: refreshSession.exchangeBaseUrl,
-      reservePub: undefined,
       status: CoinStatus.Fresh,
-      coinIndex: -1,
-      withdrawSessionId: "",
+      coinSource: {
+        type: CoinSourceType.Refresh,
+        oldCoinPub: refreshSession.meltCoinPub,
+      }
     };
 
     coins.push(coin);
diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts
index 1f9cc305..c909555f 100644
--- a/src/operations/reserves.ts
+++ b/src/operations/reserves.ts
@@ -103,7 +103,6 @@ export async function createReserve(
     amountWithdrawCompleted: Amounts.getZero(currency),
     amountWithdrawRemaining: Amounts.getZero(currency),
     exchangeBaseUrl: canonExchange,
-    hasPayback: false,
     amountInitiallyRequested: req.amount,
     reservePriv: keypair.priv,
     reservePub: keypair.pub,
diff --git a/src/operations/state.ts b/src/operations/state.ts
index 3e4936c9..ae32db2b 100644
--- a/src/operations/state.ts
+++ b/src/operations/state.ts
@@ -39,6 +39,7 @@ export class InternalWalletState {
   > = new AsyncOpMemoSingle();
   memoGetBalance: AsyncOpMemoSingle<WalletBalance> = new AsyncOpMemoSingle();
   memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
+  memoProcessRecoup: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
   cryptoApi: CryptoApi;
 
   listeners: NotificationListener[] = [];
diff --git a/src/operations/withdraw.ts b/src/operations/withdraw.ts
index 0c58f5f2..478aa4ce 100644
--- a/src/operations/withdraw.ts
+++ b/src/operations/withdraw.ts
@@ -24,6 +24,7 @@ import {
   PlanchetRecord,
   initRetryInfo,
   updateRetryInfoTimeout,
+  CoinSourceType,
 } from "../types/dbTypes";
 import * as Amounts from "../util/amounts";
 import {
@@ -48,6 +49,7 @@ import {
   timestampCmp,
   timestampSubtractDuraction,
 } from "../util/time";
+import { Store } from "../util/query";
 
 const logger = new Logger("withdraw.ts");
 
@@ -229,10 +231,13 @@ async function processPlanchet(
     denomPubHash: planchet.denomPubHash,
     denomSig,
     exchangeBaseUrl: withdrawalSession.exchangeBaseUrl,
-    reservePub: planchet.reservePub,
     status: CoinStatus.Fresh,
-    coinIndex: coinIdx,
-    withdrawSessionId: withdrawalSessionId,
+    coinSource: {
+      type: CoinSourceType.Withdraw,
+      coinIndex: coinIdx,
+      reservePub: planchet.reservePub,
+      withdrawSessionId: withdrawalSessionId
+    }
   };
 
   let withdrawSessionFinished = false;
@@ -449,14 +454,15 @@ async function processWithdrawCoin(
     return;
   }
 
-  const coin = await ws.db.getIndexed(Stores.coins.byWithdrawalWithIdx, [
-    withdrawalSessionId,
-    coinIndex,
-  ]);
+  const planchet = withdrawalSession.planchets[coinIndex];
 
-  if (coin) {
-    console.log("coin already exists");
-    return;
+  if (planchet) {
+    const coin = await ws.db.get(Stores.coins, planchet.coinPub);
+  
+    if (coin) {
+      console.log("coin already exists");
+      return;
+    } 
   }
 
   if (!withdrawalSession.planchets[coinIndex]) {
diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts
index c1d04917..56c1f82e 100644
--- a/src/types/dbTypes.ts
+++ b/src/types/dbTypes.ts
@@ -33,10 +33,7 @@ import {
 } from "./talerTypes";
 
 import { Index, Store } from "../util/query";
-import {
-  OperationError,
-  RefreshReason,
-} from "./walletTypes";
+import { OperationError, RefreshReason } from "./walletTypes";
 import { ReserveTransaction } from "./ReserveTransaction";
 import { Timestamp, Duration, getTimestampNow } from "../util/time";
 
@@ -133,7 +130,6 @@ export function initRetryInfo(
   return info;
 }
 
-
 /**
  * A reserve record as stored in the wallet's database.
  */
@@ -196,12 +192,6 @@ export interface ReserveRecord {
    */
   amountInitiallyRequested: AmountJson;
 
-  /**
-   * We got some payback to this reserve.  We'll cease to automatically
-   * withdraw money from it.
-   */
-  hasPayback: boolean;
-
   /**
    * Wire information (as payto URI) for the bank account that
    * transfered funds for this reserve.
@@ -386,6 +376,8 @@ export interface DenominationRecord {
 
   /**
    * Did we verify the signature on the denomination?
+   *
+   * FIXME:  Rename to "verificationStatus"?
    */
   status: DenominationStatus;
 
@@ -396,6 +388,13 @@ export interface DenominationRecord {
    */
   isOffered: boolean;
 
+  /**
+   * Did the exchange revoke the denomination?
+   * When this field is set to true in the database, the same transaction
+   * should also mark all affected coins as revoked.
+   */
+  isRevoked: boolean;
+
   /**
    * Base URL of the exchange.
    */
@@ -577,7 +576,7 @@ export interface RefreshPlanchetRecord {
 /**
  * Status of a coin.
  */
-export enum CoinStatus {
+export const enum CoinStatus {
   /**
    * Withdrawn and never shown to anybody.
    */
@@ -588,26 +587,47 @@ export enum CoinStatus {
   Dormant = "dormant",
 }
 
-export enum CoinSource {
+export const enum CoinSourceType {
   Withdraw = "withdraw",
   Refresh = "refresh",
   Tip = "tip",
 }
 
+export interface WithdrawCoinSource {
+  type: CoinSourceType.Withdraw;
+  withdrawSessionId: string;
+
+  /**
+   * Index of the coin in the withdrawal session.
+   */
+  coinIndex: number;
+
+  /**
+   * Reserve public key for the reserve we got this coin from.
+   */
+  reservePub: string;
+}
+
+export interface RefreshCoinSource {
+  type: CoinSourceType.Refresh;
+  oldCoinPub: string;
+}
+
+export interface TipCoinSource {
+  type: CoinSourceType.Tip;
+}
+
+export type CoinSource = WithdrawCoinSource | RefreshCoinSource | 
TipCoinSource;
+
 /**
  * CoinRecord as stored in the "coins" data store
  * of the wallet database.
  */
 export interface CoinRecord {
   /**
-   * Withdraw session ID, or "" (empty string) if withdrawn via refresh.
+   * Where did the coin come from?  Used for recouping coins.
    */
-  withdrawSessionId: string;
-
-  /**
-   * Index of the coin in the withdrawal session.
-   */
-  coinIndex: number;
+  coinSource: CoinSource;
 
   /**
    * Public key of the coin.
@@ -658,12 +678,6 @@ export interface CoinRecord {
    */
   blindingKey: string;
 
-  /**
-   * Reserve public key for the reserve we got this coin from,
-   * or zero when we got the coin from refresh.
-   */
-  reservePub: string | undefined;
-
   /**
    * Status of the coin.
    */
@@ -992,10 +1006,10 @@ export interface WireFee {
 
 /**
  * Record to store information about a refund event.
- * 
+ *
  * All information about a refund is stored with the purchase,
  * this event is just for the history.
- * 
+ *
  * The event is only present for completed refunds.
  */
 export interface RefundEventRecord {
@@ -1285,6 +1299,11 @@ export type WithdrawalSource = WithdrawalSourceTip | 
WithdrawalSourceReserve;
 export interface WithdrawalSessionRecord {
   withdrawSessionId: string;
 
+  /**
+   * Withdrawal source.  Fields that don't apply to the respective
+   * withdrawal source type must be null (i.e. can't be absent),
+   * otherwise the IndexedDB indexing won't like us.
+   */
   source: WithdrawalSource;
 
   exchangeBaseUrl: string;
@@ -1343,6 +1362,46 @@ export interface BankWithdrawUriRecord {
   reservePub: string;
 }
 
+/**
+ * Status of recoup operations that were grouped together.
+ * 
+ * The remaining amount of involved coins should be set to zero
+ * in the same transaction that inserts the RecoupGroupRecord.
+ */
+export interface RecoupGroupRecord {
+  /**
+   * Unique identifier for the recoup group record.
+   */
+  recoupGroupId: string;
+
+  timestampStarted: Timestamp;
+
+  timestampFinished: Timestamp | undefined;
+
+  /**
+   * Public keys that identify the coins being recouped
+   * as part of this session.
+   * 
+   * (Structured like this to enable multiEntry indexing in IndexedDB.)
+   */
+  coinPubs: string[];
+
+  /**
+   * Array of flags to indicate whether the recoup finished on each individual 
coin.
+   */
+  recoupFinishedPerCoin: boolean[];
+
+  /**
+   * Retry info.
+   */
+  retryInfo: RetryInfo;
+
+  /**
+   * Last error that occured, if any.
+   */
+  lastError: OperationError | undefined;
+}
+
 export const enum ImportPayloadType {
   CoreSchema = "core-schema",
 }
@@ -1398,11 +1457,6 @@ export namespace Stores {
       "denomPubIndex",
       "denomPub",
     );
-    byWithdrawalWithIdx = new Index<any, CoinRecord>(
-      this,
-      "planchetsByWithdrawalWithIdxIndex",
-      ["withdrawSessionId", "coinIndex"],
-    );
   }
 
   class ProposalsStore extends Store<ProposalRecord> {
@@ -1540,6 +1594,9 @@ export namespace Stores {
   export const refreshGroups = new Store<RefreshGroupRecord>("refreshGroups", {
     keyPath: "refreshGroupId",
   });
+  export const recoupGroups = new Store<RecoupGroupRecord>("recoupGroups", {
+    keyPath: "recoupGroupId",
+  });
   export const reserves = new ReservesStore();
   export const purchases = new PurchasesStore();
   export const tips = new TipsStore();
diff --git a/src/types/history.ts b/src/types/history.ts
index 30fe8e52..f4a1d063 100644
--- a/src/types/history.ts
+++ b/src/types/history.ts
@@ -348,19 +348,7 @@ export interface HistoryFundsDepositedToSelfEvent {
  * converted funds in these denominations to new funds.
  */
 export interface HistoryFundsRecoupedEvent {
-  type: HistoryEventType.FundsDepositedToSelf;
-
-  exchangeBaseUrl: string;
-
-  /**
-   * Amount that the wallet managed to recover.
-   */
-  amountRecouped: string;
-
-  /**
-   * Amount that was lost due to fees.
-   */
-  amountLost: string;
+  type: HistoryEventType.FundsRecouped;
 }
 
 /**
diff --git a/src/types/notifications.ts b/src/types/notifications.ts
index 30ede151..34e98fe2 100644
--- a/src/types/notifications.ts
+++ b/src/types/notifications.ts
@@ -26,8 +26,8 @@ export const enum NotificationType {
   ProposalAccepted = "proposal-accepted",
   ProposalDownloaded = "proposal-downloaded",
   RefundsSubmitted = "refunds-submitted",
-  PaybackStarted = "payback-started",
-  PaybackFinished = "payback-finished",
+  RecoupStarted = "payback-started",
+  RecoupFinished = "payback-finished",
   RefreshRevealed = "refresh-revealed",
   RefreshMelted = "refresh-melted",
   RefreshStarted = "refresh-started",
@@ -44,6 +44,7 @@ export const enum NotificationType {
   RefundFinished = "refund-finished",
   ExchangeOperationError = "exchange-operation-error",
   RefreshOperationError = "refresh-operation-error",
+  RecoupOperationError = "refresh-operation-error",
   RefundApplyOperationError = "refund-apply-error",
   RefundStatusOperationError = "refund-status-error",
   ProposalOperationError = "proposal-error",
@@ -82,11 +83,11 @@ export interface RefundsSubmittedNotification {
 }
 
 export interface PaybackStartedNotification {
-  type: NotificationType.PaybackStarted;
+  type: NotificationType.RecoupStarted;
 }
 
 export interface PaybackFinishedNotification {
-  type: NotificationType.PaybackFinished;
+  type: NotificationType.RecoupFinished;
 }
 
 export interface RefreshMeltedNotification {
diff --git a/src/types/pending.ts b/src/types/pending.ts
index b86c7797..5d732c52 100644
--- a/src/types/pending.ts
+++ b/src/types/pending.ts
@@ -58,6 +58,7 @@ export type PendingOperationInfo = PendingOperationInfoCommon 
&
     | PendingTipChoiceOperation
     | PendingTipPickupOperation
     | PendingWithdrawOperation
+    | PendingRecoupOperation
   );
 
 /**
@@ -200,6 +201,11 @@ export interface PendingRefundApplyOperation {
   numRefundsDone: number;
 }
 
+export interface PendingRecoupOperation {
+  type: PendingOperationType.Recoup;
+  recoupGroupId: string;
+}
+
 /**
  * Status of an ongoing withdrawal operation.
  */
diff --git a/src/types/talerTypes.ts b/src/types/talerTypes.ts
index 10ee8374..e65c8238 100644
--- a/src/types/talerTypes.ts
+++ b/src/types/talerTypes.ts
@@ -38,7 +38,12 @@ import {
   codecForBoolean,
   makeCodecForMap,
 } from "../util/codec";
-import { Timestamp, codecForTimestamp, Duration, codecForDuration } from 
"../util/time";
+import {
+  Timestamp,
+  codecForTimestamp,
+  Duration,
+  codecForDuration,
+} from "../util/time";
 
 /**
  * Denomination as found in the /keys response from the exchange.
@@ -141,7 +146,7 @@ export class Auditor {
 /**
  * Request that we send to the exchange to get a payback.
  */
-export interface PaybackRequest {
+export interface RecoupRequest {
   /**
    * Denomination public key of the coin we want to get
    * paid back.
@@ -168,6 +173,11 @@ export interface PaybackRequest {
    * Signature made by the coin, authorizing the payback.
    */
   coin_sig: string;
+
+  /**
+   * Was the coin refreshed (and thus the recoup should go to the old coin)?
+   */
+  refreshed: boolean;
 }
 
 /**
@@ -175,9 +185,15 @@ export interface PaybackRequest {
  */
 export class RecoupConfirmation {
   /**
-   * public key of the reserve that will receive the payback.
+   * Public key of the reserve that will receive the payback.
    */
-  reserve_pub: string;
+  reserve_pub?: string;
+
+  /**
+   * Public key of the old coin that will receive the recoup,
+   * provided if refreshed was true.
+   */
+  old_coin_pub?: string;
 
   /**
    * How much will the exchange pay back (needed by wallet in
@@ -575,7 +591,7 @@ export class TipResponse {
  * Element of the payback list that the
  * exchange gives us in /keys.
  */
-export class Payback {
+export class Recoup {
   /**
    * The hash of the denomination public key for which the payback is offered.
    */
@@ -607,9 +623,9 @@ export class ExchangeKeysJson {
   list_issue_date: Timestamp;
 
   /**
-   * List of paybacks for compromised denominations.
+   * List of revoked denominations.
    */
-  payback?: Payback[];
+  recoup?: Recoup[];
 
   /**
    * Short-lived signing keys used to sign online
@@ -764,7 +780,10 @@ export const codecForAuditor = () =>
     makeCodecForObject<Auditor>()
       .property("auditor_pub", codecForString)
       .property("auditor_url", codecForString)
-      .property("denomination_keys", 
makeCodecForList(codecForAuditorDenomSig()))
+      .property(
+        "denomination_keys",
+        makeCodecForList(codecForAuditorDenomSig()),
+      )
       .build("Auditor"),
   );
 
@@ -779,7 +798,7 @@ export const codecForExchangeHandle = () =>
 export const codecForAuditorHandle = () =>
   typecheckedCodec<AuditorHandle>(
     makeCodecForObject<AuditorHandle>()
-    .property("name", codecForString)
+      .property("name", codecForString)
       .property("master_pub", codecForString)
       .property("url", codecForString)
       .build("AuditorHandle"),
@@ -851,9 +870,9 @@ export const codecForTipResponse = () =>
       .build("TipResponse"),
   );
 
-export const codecForPayback = () =>
-  typecheckedCodec<Payback>(
-    makeCodecForObject<Payback>()
+export const codecForRecoup = () =>
+  typecheckedCodec<Recoup>(
+    makeCodecForObject<Recoup>()
       .property("h_denom_pub", codecForString)
       .build("Payback"),
   );
@@ -865,13 +884,12 @@ export const codecForExchangeKeysJson = () =>
       .property("master_public_key", codecForString)
       .property("auditors", makeCodecForList(codecForAuditor()))
       .property("list_issue_date", codecForTimestamp)
-      .property("payback", 
makeCodecOptional(makeCodecForList(codecForPayback())))
+      .property("recoup", 
makeCodecOptional(makeCodecForList(codecForRecoup())))
       .property("signkeys", codecForAny)
       .property("version", codecForString)
       .build("KeysJson"),
   );
 
-
 export const codecForWireFeesJson = () =>
   typecheckedCodec<WireFeesJson>(
     makeCodecForObject<WireFeesJson>()
@@ -895,7 +913,10 @@ export const codecForExchangeWireJson = () =>
   typecheckedCodec<ExchangeWireJson>(
     makeCodecForObject<ExchangeWireJson>()
       .property("accounts", makeCodecForList(codecForAccountInfo()))
-      .property("fees", 
makeCodecForMap(makeCodecForList(codecForWireFeesJson())))
+      .property(
+        "fees",
+        makeCodecForMap(makeCodecForList(codecForWireFeesJson())),
+      )
       .build("ExchangeWireJson"),
   );
 
@@ -919,13 +940,12 @@ export const codecForCheckPaymentResponse = () =>
       .build("CheckPaymentResponse"),
   );
 
-
 export const codecForWithdrawOperationStatusResponse = () =>
   typecheckedCodec<WithdrawOperationStatusResponse>(
     makeCodecForObject<WithdrawOperationStatusResponse>()
       .property("selection_done", codecForBoolean)
       .property("transfer_done", codecForBoolean)
-      .property("amount",codecForString)
+      .property("amount", codecForString)
       .property("sender_wire", makeCodecOptional(codecForString))
       .property("suggested_exchange", makeCodecOptional(codecForString))
       .property("confirm_transfer_url", makeCodecOptional(codecForString))
@@ -945,11 +965,11 @@ export const codecForTipPickupGetResponse = () =>
       .build("TipPickupGetResponse"),
   );
 
-
 export const codecForRecoupConfirmation = () =>
   typecheckedCodec<RecoupConfirmation>(
     makeCodecForObject<RecoupConfirmation>()
-      .property("reserve_pub", codecForString)
+      .property("reserve_pub", makeCodecOptional(codecForString))
+      .property("old_coin_pub", makeCodecOptional(codecForString))
       .property("amount", codecForString)
       .property("timestamp", codecForTimestamp)
       .property("exchange_sig", codecForString)
diff --git a/src/types/walletTypes.ts b/src/types/walletTypes.ts
index 9887474c..c6473a9b 100644
--- a/src/types/walletTypes.ts
+++ b/src/types/walletTypes.ts
@@ -1,6 +1,6 @@
 /*
- This file is part of TALER
- (C) 2015-2017 GNUnet e.V. and INRIA
+ This file is part of GNU Taler
+ (C) 2015-2020 Taler Systems SA
 
  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
@@ -20,6 +20,8 @@
  * These types are defined in a separate file make tree shaking easier, since
  * some components use these types (via RPC) but do not depend on the wallet
  * code directly.
+ * 
+ * @author Florian Dold <address@hidden>
  */
 
 /**
diff --git a/src/util/query.ts b/src/util/query.ts
index 95ef30e1..d08c901a 100644
--- a/src/util/query.ts
+++ b/src/util/query.ts
@@ -271,6 +271,14 @@ export class TransactionHandle {
     return new ResultStream<T>(req);
   }
 
+  iterIndexed<S extends IDBValidKey,T>(
+    index: Index<S, T>,
+    key?: any,
+  ): ResultStream<T> {
+    const req = 
this.tx.objectStore(index.storeName).index(index.indexName).openCursor(key);
+    return new ResultStream<T>(req);
+  }
+
   delete<T>(store: Store<T>, key: any): Promise<void> {
     const req = this.tx.objectStore(store.name).delete(key);
     return requestToPromise(req);
diff --git a/src/wallet.ts b/src/wallet.ts
index 23ac8490..3b619f87 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -95,7 +95,6 @@ import { getHistory } from "./operations/history";
 import { getPendingOperations } from "./operations/pending";
 import { getBalances } from "./operations/balance";
 import { acceptTip, getTipStatus, processTip } from "./operations/tip";
-import { recoup } from "./operations/recoup";
 import { TimerGroup } from "./util/timer";
 import { AsyncCondition } from "./util/promiseUtils";
 import { AsyncOpMemoSingle } from "./util/asyncMemo";
@@ -113,6 +112,7 @@ import {
   applyRefund,
 } from "./operations/refund";
 import { durationMin, Duration } from "./util/time";
+import { processRecoupGroup } from "./operations/recoup";
 
 const builtinCurrencies: CurrencyRecord[] = [
   {
@@ -217,6 +217,9 @@ export class Wallet {
       case PendingOperationType.RefundApply:
         await processPurchaseApplyRefund(this.ws, pending.proposalId, 
forceNow);
         break;
+      case PendingOperationType.Recoup:
+        await processRecoupGroup(this.ws, pending.recoupGroupId, forceNow);
+        break;
       default:
         assertUnreachable(pending);
     }
@@ -577,10 +580,6 @@ export class Wallet {
     return await this.db.iter(Stores.coins).toArray();
   }
 
-  async getPaybackReserves(): Promise<ReserveRecord[]> {
-    return await this.db.iter(Stores.reserves).filter(r => r.hasPayback);
-  }
-
   /**
    * Stop ongoing processing.
    */
diff --git a/src/webex/messages.ts b/src/webex/messages.ts
index 132c8c58..7672fcb4 100644
--- a/src/webex/messages.ts
+++ b/src/webex/messages.ts
@@ -106,14 +106,6 @@ export interface MessageMap {
     request: { exchangeBaseUrl: string };
     response: dbTypes.ReserveRecord[];
   };
-  "get-payback-reserves": {
-    request: {};
-    response: dbTypes.ReserveRecord[];
-  };
-  "withdraw-payback-reserve": {
-    request: { reservePub: string };
-    response: dbTypes.ReserveRecord[];
-  };
   "get-denoms": {
     request: { exchangeBaseUrl: string };
     response: dbTypes.DenominationRecord[];
diff --git a/src/webex/pages/payback.tsx b/src/webex/pages/payback.tsx
index 2601887b..96d43ff4 100644
--- a/src/webex/pages/payback.tsx
+++ b/src/webex/pages/payback.tsx
@@ -25,49 +25,11 @@
  */
 import { ReserveRecord } from "../../types/dbTypes";
 import { renderAmount, registerMountPage } from "../renderHtml";
-import { getPaybackReserves, withdrawPaybackReserve } from "../wxApi";
 import * as React from "react";
 import { useState } from "react";
 
 function Payback() {
-  const [reserves, setReserves] = useState<ReserveRecord[] | null>(null);
-
-  useState(() => {
-    const update = async () => {
-      const r = await getPaybackReserves();
-      setReserves(r);
-    };
-
-    const port = chrome.runtime.connect();
-    port.onMessage.addListener((msg: any) => {
-      if (msg.notify) {
-        console.log("got notified");
-        update();
-      }
-    });
-  });
-
-  if (!reserves) {
-    return <span>loading ...</span>;
-  }
-  if (reserves.length === 0) {
-    return <span>No reserves with payback available.</span>;
-  }
-  return (
-    <div>
-      {reserves.map(r => (
-        <div>
-          <h2>Reserve for ${renderAmount(r.amountWithdrawRemaining)}</h2>
-          <ul>
-            <li>Exchange: ${r.exchangeBaseUrl}</li>
-          </ul>
-          <button onClick={() => withdrawPaybackReserve(r.reservePub)}>
-            Withdraw again
-          </button>
-        </div>
-      ))}
-    </div>
-  );
+  return <div>not implemented</div>;
 }
 
 registerMountPage(() => <Payback />);
diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts
index 7464b1f7..5edd1907 100644
--- a/src/webex/wxApi.ts
+++ b/src/webex/wxApi.ts
@@ -18,7 +18,6 @@
  * Interface to the wallet through WebExtension messaging.
  */
 
-
 /**
  * Imports.
  */
@@ -28,7 +27,6 @@ import {
   CurrencyRecord,
   DenominationRecord,
   ExchangeRecord,
-  PlanchetRecord,
   ReserveRecord,
 } from "../types/dbTypes";
 import {
@@ -44,7 +42,6 @@ import {
 
 import { MessageMap, MessageType } from "./messages";
 
-
 /**
  * Response with information about available version upgrades.
  */
@@ -66,7 +63,6 @@ export interface UpgradeResponse {
   oldDbVersion: string;
 }
 
-
 /**
  * Error thrown when the function from the backend (via RPC) threw an error.
  */
@@ -78,19 +74,22 @@ export class WalletApiError extends Error {
   }
 }
 
-
 async function callBackend<T extends MessageType>(
   type: T,
   detail: MessageMap[T]["request"],
 ): Promise<MessageMap[T]["response"]> {
   return new Promise<MessageMap[T]["response"]>((resolve, reject) => {
-    chrome.runtime.sendMessage({ type, detail }, (resp) => {
+    chrome.runtime.sendMessage({ type, detail }, resp => {
       if (chrome.runtime.lastError) {
         console.log("Error calling backend");
-        reject(new Error(`Error contacting backend: 
chrome.runtime.lastError.message`));
+        reject(
+          new Error(
+            `Error contacting backend: chrome.runtime.lastError.message`,
+          ),
+        );
       }
       if (typeof resp === "object" && resp && resp.error) {
-        console.warn("response error:", resp)
+        console.warn("response error:", resp);
         const e = new WalletApiError(resp.error.message, resp.error);
         reject(e);
       } else {
@@ -100,42 +99,38 @@ async function callBackend<T extends MessageType>(
   });
 }
 
-
 /**
  * Query the wallet for the coins that would be used to withdraw
  * from a given reserve.
  */
-export function getReserveCreationInfo(baseUrl: string,
-                                       amount: AmountJson): 
Promise<ExchangeWithdrawDetails> {
+export function getReserveCreationInfo(
+  baseUrl: string,
+  amount: AmountJson,
+): Promise<ExchangeWithdrawDetails> {
   return callBackend("reserve-creation-info", { baseUrl, amount });
 }
 
-
 /**
  * Get all exchanges the wallet knows about.
  */
 export function getExchanges(): Promise<ExchangeRecord[]> {
-  return callBackend("get-exchanges", { });
+  return callBackend("get-exchanges", {});
 }
 
-
 /**
  * Get all currencies the exchange knows about.
  */
 export function getCurrencies(): Promise<CurrencyRecord[]> {
-  return callBackend("get-currencies", { });
+  return callBackend("get-currencies", {});
 }
 
-
-
 /**
  * Get information about a specific exchange.
  */
 export function getExchangeInfo(baseUrl: string): Promise<ExchangeRecord> {
-  return callBackend("exchange-info", {baseUrl});
+  return callBackend("exchange-info", { baseUrl });
 }
 
-
 /**
  * Replace an existing currency record with the one given.  The currency to
  * replace is specified inside the currency record.
@@ -144,7 +139,6 @@ export function updateCurrency(currencyRecord: 
CurrencyRecord): Promise<void> {
   return callBackend("update-currency", { currencyRecord });
 }
 
-
 /**
  * Get all reserves the wallet has at an exchange.
  */
@@ -152,23 +146,6 @@ export function getReserves(exchangeBaseUrl: string): 
Promise<ReserveRecord[]> {
   return callBackend("get-reserves", { exchangeBaseUrl });
 }
 
-
-/**
- * Get all reserves for which a payback is available.
- */
-export function getPaybackReserves(): Promise<ReserveRecord[]> {
-  return callBackend("get-payback-reserves", { });
-}
-
-
-/**
- * Withdraw the payback that is available for a reserve.
- */
-export function withdrawPaybackReserve(reservePub: string): 
Promise<ReserveRecord[]> {
-  return callBackend("withdraw-payback-reserve", { reservePub });
-}
-
-
 /**
  * Get all coins withdrawn from the given exchange.
  */
@@ -176,15 +153,15 @@ export function getCoins(exchangeBaseUrl: string): 
Promise<CoinRecord[]> {
   return callBackend("get-coins", { exchangeBaseUrl });
 }
 
-
 /**
  * Get all denoms offered by the given exchange.
  */
-export function getDenoms(exchangeBaseUrl: string): 
Promise<DenominationRecord[]> {
+export function getDenoms(
+  exchangeBaseUrl: string,
+): Promise<DenominationRecord[]> {
   return callBackend("get-denoms", { exchangeBaseUrl });
 }
 
-
 /**
  * Start refreshing a coin.
  */
@@ -192,15 +169,16 @@ export function refresh(coinPub: string): Promise<void> {
   return callBackend("refresh-coin", { coinPub });
 }
 
-
 /**
  * Pay for a proposal.
  */
-export function confirmPay(proposalId: string, sessionId: string | undefined): 
Promise<ConfirmPayResult> {
+export function confirmPay(
+  proposalId: string,
+  sessionId: string | undefined,
+): Promise<ConfirmPayResult> {
   return callBackend("confirm-pay", { proposalId, sessionId });
 }
 
-
 /**
  * Mark a reserve as confirmed.
  */
@@ -212,13 +190,17 @@ export function confirmReserve(reservePub: string): 
Promise<void> {
  * Check upgrade information
  */
 export function checkUpgrade(): Promise<UpgradeResponse> {
-  return callBackend("check-upgrade", { });
+  return callBackend("check-upgrade", {});
 }
 
 /**
  * Create a reserve.
  */
-export function createReserve(args: { amount: AmountJson, exchange: string, 
senderWire?: string }): Promise<any> {
+export function createReserve(args: {
+  amount: AmountJson;
+  exchange: string;
+  senderWire?: string;
+}): Promise<any> {
   return callBackend("create-reserve", args);
 }
 
@@ -226,42 +208,45 @@ export function createReserve(args: { amount: AmountJson, 
exchange: string, send
  * Reset database
  */
 export function resetDb(): Promise<void> {
-  return callBackend("reset-db", { });
+  return callBackend("reset-db", {});
 }
 
 /**
  * Get balances for all currencies/exchanges.
  */
 export function getBalance(): Promise<WalletBalance> {
-  return callBackend("balances", { });
+  return callBackend("balances", {});
 }
 
-
 /**
  * Get possible sender wire infos for getting money
  * wired from an exchange.
  */
 export function getSenderWireInfos(): Promise<SenderWireInfos> {
-  return callBackend("get-sender-wire-infos", { });
+  return callBackend("get-sender-wire-infos", {});
 }
 
 /**
  * Return coins to a bank account.
  */
-export function returnCoins(args: { amount: AmountJson, exchange: string, 
senderWire: object }): Promise<void> {
+export function returnCoins(args: {
+  amount: AmountJson;
+  exchange: string;
+  senderWire: object;
+}): Promise<void> {
   return callBackend("return-coins", args);
 }
 
-
 /**
  * Look up a purchase in the wallet database from
  * the contract terms hash.
  */
-export function getPurchaseDetails(contractTermsHash: string): 
Promise<PurchaseDetails> {
+export function getPurchaseDetails(
+  contractTermsHash: string,
+): Promise<PurchaseDetails> {
   return callBackend("get-purchase-details", { contractTermsHash });
 }
 
-
 /**
  * Get the status of processing a tip.
  */
@@ -276,7 +261,6 @@ export function acceptTip(talerTipUri: string): 
Promise<void> {
   return callBackend("accept-tip", { talerTipUri });
 }
 
-
 /**
  * Download a refund and accept it.
  */
@@ -291,7 +275,6 @@ export function abortFailedPayment(contractTermsHash: 
string) {
   return callBackend("abort-failed-payment", { contractTermsHash });
 }
 
-
 /**
  * Abort a failed payment and try to get a refund.
  */
@@ -302,8 +285,14 @@ export function benchmarkCrypto(repetitions: number): 
Promise<BenchmarkResult> {
 /**
  * Get details about a withdraw operation.
  */
-export function getWithdrawDetails(talerWithdrawUri: string, 
maybeSelectedExchange: string | undefined) {
-  return callBackend("get-withdraw-details", { talerWithdrawUri, 
maybeSelectedExchange });
+export function getWithdrawDetails(
+  talerWithdrawUri: string,
+  maybeSelectedExchange: string | undefined,
+) {
+  return callBackend("get-withdraw-details", {
+    talerWithdrawUri,
+    maybeSelectedExchange,
+  });
 }
 
 /**
@@ -316,8 +305,14 @@ export function preparePay(talerPayUri: string) {
 /**
  * Get details about a withdraw operation.
  */
-export function acceptWithdrawal(talerWithdrawUri: string, selectedExchange: 
string) {
-  return callBackend("accept-withdrawal", { talerWithdrawUri, selectedExchange 
});
+export function acceptWithdrawal(
+  talerWithdrawUri: string,
+  selectedExchange: string,
+) {
+  return callBackend("accept-withdrawal", {
+    talerWithdrawUri,
+    selectedExchange,
+  });
 }
 
 /**
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index faf917f8..248e6dfb 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -148,15 +148,6 @@ async function handleMessage(
       }
       return needsWallet().getReserves(detail.exchangeBaseUrl);
     }
-    case "get-payback-reserves": {
-      return needsWallet().getPaybackReserves();
-    }
-    case "withdraw-payback-reserve": {
-      if (typeof detail.reservePub !== "string") {
-        return Promise.reject(Error("reservePub missing"));
-      }
-      throw Error("not implemented");
-    }
     case "get-coins": {
       if (typeof detail.exchangeBaseUrl !== "string") {
         return Promise.reject(Error("exchangBaseUrl missing"));

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



reply via email to

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