aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2020-03-12 00:44:28 +0530
committerFlorian Dold <florian.dold@gmail.com>2020-03-12 00:44:28 +0530
commit2c52046f0bf358a5e07c53394b3b72d091356cce (patch)
tree8993c992b9c8240ee865671cdfbab380e61af96c /src
parent6e2881fabf74a3c1da8e94dcbe3e68fce6080d9e (diff)
downloadwallet-core-2c52046f0bf358a5e07c53394b3b72d091356cce.tar.gz
wallet-core-2c52046f0bf358a5e07c53394b3b72d091356cce.tar.bz2
wallet-core-2c52046f0bf358a5e07c53394b3b72d091356cce.zip
full recoup, untested/unfinished first attempt
Diffstat (limited to 'src')
-rw-r--r--src/crypto/workers/cryptoApi.ts7
-rw-r--r--src/crypto/workers/cryptoImplementation.ts14
-rw-r--r--src/headless/taler-wallet-cli.ts1
-rw-r--r--src/operations/exchanges.ts43
-rw-r--r--src/operations/history.ts11
-rw-r--r--src/operations/pending.ts28
-rw-r--r--src/operations/recoup.ts372
-rw-r--r--src/operations/refresh.ts8
-rw-r--r--src/operations/reserves.ts1
-rw-r--r--src/operations/state.ts1
-rw-r--r--src/operations/withdraw.ts26
-rw-r--r--src/types/dbTypes.ts123
-rw-r--r--src/types/history.ts14
-rw-r--r--src/types/notifications.ts9
-rw-r--r--src/types/pending.ts6
-rw-r--r--src/types/talerTypes.ts58
-rw-r--r--src/types/walletTypes.ts6
-rw-r--r--src/util/query.ts8
-rw-r--r--src/wallet.ts9
-rw-r--r--src/webex/messages.ts8
-rw-r--r--src/webex/pages/payback.tsx40
-rw-r--r--src/webex/wxApi.ts113
-rw-r--r--src/webex/wxBackend.ts9
23 files changed, 655 insertions, 260 deletions
diff --git a/src/crypto/workers/cryptoApi.ts b/src/crypto/workers/cryptoApi.ts
index 489d56f5c..4adf2882e 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 220046209..3447c56f0 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 9abdb05d1..707849952 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 cf6b06868..ed13a1e5b 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 2fb7854d2..2cf215a5a 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 fce9a3bfb..08ec3fc9e 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 2b646a4d8..842a67b87 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
@@ -15,75 +15,357 @@
*/
/**
+ * Implementation of the recoup operation, which allows to recover the
+ * value of coins held in a revoked denomination.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
* 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 6dd16d61a..092d9f154 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 1f9cc3053..c909555fe 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 3e4936c98..ae32db2b3 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 0c58f5f2f..478aa4ceb 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 c1d049179..56c1f82eb 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.
*/
@@ -197,12 +193,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;
@@ -397,6 +389,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.
*/
exchangeBaseUrl: string;
@@ -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.
@@ -659,12 +679,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.
*/
status: CoinStatus;
@@ -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 30fe8e529..f4a1d0631 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 30ede151c..34e98fe2c 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 b86c7797b..5d732c520 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 10ee83743..e65c82383 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 9887474c3..c6473a9b7 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 <dold@taler.net>
*/
/**
diff --git a/src/util/query.ts b/src/util/query.ts
index 95ef30e1b..d08c901a4 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 23ac8490b..3b619f874 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 132c8c58d..7672fcb4b 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 2601887b0..96d43ff49 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 7464b1f74..5edd1907b 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 faf917f86..248e6dfba 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"));