gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (26d961ad -> 2c52046f)


From: gnunet
Subject: [taler-wallet-core] branch master updated (26d961ad -> 2c52046f)
Date: Wed, 11 Mar 2020 20:14:33 +0100

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

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

    from 26d961ad support exchange API version 7:0:0
     new 6e2881fa cleanup
     new 2c52046f full recoup, untested/unfinished first attempt

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 src/crypto/workers/cryptoApi.ts            |   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/payback.ts                  |  89 -------
 src/operations/pending.ts                  |  28 +++
 src/operations/recoup.ts                   | 371 +++++++++++++++++++++++++++++
 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                              |  13 +-
 src/webex/messages.ts                      |  12 -
 src/webex/pages/payback.tsx                |  40 +---
 src/webex/wxApi.ts                         | 120 +++++-----
 src/webex/wxBackend.ts                     |  37 ++-
 tsconfig.json                              |   2 +-
 25 files changed, 714 insertions(+), 334 deletions(-)
 delete mode 100644 src/operations/payback.ts
 create mode 100644 src/operations/recoup.ts

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/payback.ts b/src/operations/payback.ts
deleted file mode 100644
index 18152769..00000000
--- a/src/operations/payback.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import {
-  Database
-} from "../util/query";
-import { InternalWalletState } from "./state";
-import { Stores, TipRecord, CoinStatus } from "../types/dbTypes";
-
-import { Logger } from "../util/logging";
-import { RecoupConfirmation, codecForRecoupConfirmation } from 
"../types/talerTypes";
-import { updateExchangeFromUrl } from "./exchanges";
-import { NotificationType } from "../types/notifications";
-
-const logger = new Logger("payback.ts");
-
-export async function payback(
-  ws: InternalWalletState,
-  coinPub: string,
-): Promise<void> {
-  let coin = await ws.db.get(Stores.coins, coinPub);
-  if (!coin) {
-    throw Error(`Coin ${coinPub} not found, can't request payback`);
-  }
-  const reservePub = coin.reservePub;
-  if (!reservePub) {
-    throw Error(`Can't request payback for a refreshed coin`);
-  }
-  const reserve = await ws.db.get(Stores.reserves, reservePub);
-  if (!reserve) {
-    throw Error(`Reserve of coin ${coinPub} not found`);
-  }
-  switch (coin.status) {
-    case CoinStatus.Dormant:
-      throw Error(`Can't do payback for coin ${coinPub} since it's dormant`);
-  }
-  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;
-  await ws.db.runWithWriteTransaction(
-    [Stores.coins, Stores.reserves],
-    async tx => {
-      await tx.put(Stores.coins, coin!!);
-      await tx.put(Stores.reserves, reserve);
-    },
-  );
-  ws.notify({
-    type: NotificationType.PaybackStarted,
-  });
-
-  const paybackRequest = await ws.cryptoApi.createPaybackRequest(coin);
-  const reqUrl = new URL("payback", coin.exchangeBaseUrl);
-  const resp = await ws.http.postJson(reqUrl.href, paybackRequest);
-  if (resp.status !== 200) {
-    throw Error();
-  }
-  const paybackConfirmation = codecForRecoupConfirmation().decode(await 
resp.json());
-  if (paybackConfirmation.reserve_pub !== coin.reservePub) {
-    throw Error(`Coin's reserve doesn't match reserve on payback`);
-  }
-  coin = await ws.db.get(Stores.coins, coinPub);
-  if (!coin) {
-    throw Error(`Coin ${coinPub} not found, can't confirm payback`);
-  }
-  coin.status = CoinStatus.Dormant;
-  await ws.db.put(Stores.coins, coin);
-  ws.notify({
-    type: NotificationType.PaybackFinished,
-  });
-  await updateExchangeFromUrl(ws, coin.exchangeBaseUrl, true);
-}
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
new file mode 100644
index 00000000..842a67b8
--- /dev/null
+++ b/src/operations/recoup.ts
@@ -0,0 +1,371 @@
+/*
+ This file is part of GNU Taler
+ (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
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * 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 { InternalWalletState } from "./state";
+import {
+  Stores,
+  CoinStatus,
+  CoinSourceType,
+  CoinRecord,
+  WithdrawCoinSource,
+  RefreshCoinSource,
+  ReserveRecordStatus,
+  RecoupGroupRecord,
+  initRetryInfo,
+  updateRetryInfoTimeout,
+} from "../types/dbTypes";
+
+import { codecForRecoupConfirmation } from "../types/talerTypes";
+import { NotificationType } from "../types/notifications";
+import { processReserve } from "./reserves";
+
+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";
+
+async function incrementRecoupRetry(
+  ws: InternalWalletState,
+  recoupGroupId: string,
+  err: OperationError | undefined,
+): Promise<void> {
+  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;
+    }
+  }
+  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) {
+    // 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");
+  }
+  const recoupConfirmation = codecForRecoupConfirmation().decode(
+    await resp.json(),
+  );
+
+  if (recoupConfirmation.reserve_pub !== reservePub) {
+    throw Error(`Coin's reserve doesn't match reserve on recoup`);
+  }
+
+  // FIXME: verify that our expectations about the amount match
+
+  await ws.db.runWithWriteTransaction(
+    [Stores.coins, Stores.reserves, Stores.recoupGroups],
+    async tx => {
+      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.RecoupFinished,
+  });
+
+  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("recoup request failed");
+  }
+  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`);
+  }
+
+  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);
+    });
+  }
+}
+
+async function resetRecoupGroupRetry(
+  ws: InternalWalletState,
+  recoupGroupId: string,
+) {
+  await ws.db.mutate(Stores.recoupGroups, recoupGroupId, x => {
+    if (x.retryInfo.active) {
+      x.retryInfo = initRetryInfo();
+    }
+    return x;
+  });
+}
+
+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 12bc2ccb..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 { payback } from "./operations/payback";
 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,14 +580,6 @@ export class Wallet {
     return await this.db.iter(Stores.coins).toArray();
   }
 
-  async payback(coinPub: string): Promise<void> {
-    return payback(this.ws, coinPub);
-  }
-
-  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 579dd434..7672fcb4 100644
--- a/src/webex/messages.ts
+++ b/src/webex/messages.ts
@@ -106,22 +106,10 @@ 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[];
   };
-  "payback-coin": {
-    request: { coinPub: string };
-    response: void;
-  };
   "check-upgrade": {
     request: {};
     response: UpgradeResponse;
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 bb5222a2..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,22 +169,16 @@ export function refresh(coinPub: string): Promise<void> {
   return callBackend("refresh-coin", { coinPub });
 }
 
-
-/**
- * Request payback for a coin.  Only works for non-refreshed coins.
- */
-export function payback(coinPub: string): Promise<void> {
-  return callBackend("payback-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.
  */
@@ -219,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);
 }
 
@@ -233,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.
  */
@@ -283,7 +261,6 @@ export function acceptTip(talerTipUri: string): 
Promise<void> {
   return callBackend("accept-tip", { talerTipUri });
 }
 
-
 /**
  * Download a refund and accept it.
  */
@@ -298,7 +275,6 @@ export function abortFailedPayment(contractTermsHash: 
string) {
   return callBackend("abort-failed-payment", { contractTermsHash });
 }
 
-
 /**
  * Abort a failed payment and try to get a refund.
  */
@@ -309,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,
+  });
 }
 
 /**
@@ -323,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 053b8296..248e6dfb 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -24,9 +24,18 @@
  * Imports.
  */
 import { BrowserCryptoWorkerFactory } from "../crypto/workers/cryptoApi";
-import { deleteTalerDatabase, openTalerDatabase, WALLET_DB_VERSION } from 
"../db";
-import { ConfirmReserveRequest, CreateReserveRequest, ReturnCoinsRequest, 
WalletDiagnostics, codecForCreateReserveRequest, codecForConfirmReserveRequest 
} from "../types/walletTypes";
-import { AmountJson, codecForAmountJson } from "../util/amounts";
+import {
+  deleteTalerDatabase,
+  openTalerDatabase,
+  WALLET_DB_VERSION,
+} from "../db";
+import {
+  ReturnCoinsRequest,
+  WalletDiagnostics,
+  codecForCreateReserveRequest,
+  codecForConfirmReserveRequest,
+} from "../types/walletTypes";
+import { codecForAmountJson } from "../util/amounts";
 import { BrowserHttpLib } from "../util/http";
 import { OpenedPromise, openPromise } from "../util/promiseUtils";
 import { classifyTalerUri, TalerUriType } from "../util/taleruri";
@@ -67,7 +76,7 @@ async function handleMessage(
     }
     case "dump-db": {
       const db = needsWallet().db;
-      return db.exportDatabase()
+      return db.exportDatabase();
     }
     case "import-db": {
       const db = needsWallet().db;
@@ -139,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"));
@@ -166,12 +166,6 @@ async function handleMessage(
       }
       return needsWallet().refresh(detail.coinPub);
     }
-    case "payback-coin": {
-      if (typeof detail.coinPub !== "string") {
-        return Promise.reject(Error("coinPub missing"));
-      }
-      return needsWallet().payback(detail.coinPub);
-    }
     case "get-sender-wire-infos": {
       return needsWallet().getSenderWireInfos();
     }
@@ -399,10 +393,7 @@ async function reinitWallet() {
   setBadgeText({ text: "" });
   const badge = new ChromeBadge();
   try {
-    currentDatabase = await openTalerDatabase(
-      indexedDB,
-      reinitWallet,
-    );
+    currentDatabase = await openTalerDatabase(indexedDB, reinitWallet);
   } catch (e) {
     console.error("could not open database", e);
     walletInit.reject(e);
diff --git a/tsconfig.json b/tsconfig.json
index ec15f8dd..a6cd7b26 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -51,8 +51,8 @@
     "src/operations/exchanges.ts",
     "src/operations/history.ts",
     "src/operations/pay.ts",
-    "src/operations/payback.ts",
     "src/operations/pending.ts",
+    "src/operations/recoup.ts",
     "src/operations/refresh.ts",
     "src/operations/refund.ts",
     "src/operations/reserves.ts",

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



reply via email to

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