summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2020-03-12 19:25:38 +0530
committerFlorian Dold <florian.dold@gmail.com>2020-03-12 19:25:38 +0530
commitb5b8f96cc94e3a3c0ee7d989819197ab5df393cd (patch)
tree0382770a735c4f43e09bfb9d03345bc93ecc498a
parent2ec6799c8c6836d44944460a41fabefb8eb8186f (diff)
downloadwallet-core-b5b8f96cc94e3a3c0ee7d989819197ab5df393cd.tar.gz
wallet-core-b5b8f96cc94e3a3c0ee7d989819197ab5df393cd.tar.bz2
wallet-core-b5b8f96cc94e3a3c0ee7d989819197ab5df393cd.zip
improved error reporting / towards a working recoup
-rw-r--r--src/crypto/workers/cryptoImplementation.ts2
-rw-r--r--src/headless/taler-wallet-cli.ts3
-rw-r--r--src/operations/errors.ts104
-rw-r--r--src/operations/exchanges.ts56
-rw-r--r--src/operations/history.ts1
-rw-r--r--src/operations/pending.ts2
-rw-r--r--src/operations/recoup.ts34
-rw-r--r--src/operations/refund.ts2
-rw-r--r--src/operations/reserves.ts45
-rw-r--r--src/operations/withdraw.ts36
-rw-r--r--src/types/ReserveTransaction.ts34
-rw-r--r--src/types/dbTypes.ts5
-rw-r--r--src/types/notifications.ts21
-rw-r--r--src/types/pending.ts2
-rw-r--r--src/types/talerTypes.ts16
-rw-r--r--src/wallet.ts7
16 files changed, 253 insertions, 117 deletions
diff --git a/src/crypto/workers/cryptoImplementation.ts b/src/crypto/workers/cryptoImplementation.ts
index 3447c56f0..5659fec21 100644
--- a/src/crypto/workers/cryptoImplementation.ts
+++ b/src/crypto/workers/cryptoImplementation.ts
@@ -214,7 +214,7 @@ export class CryptoImplementation {
coin_blind_key_secret: coin.blindingKey,
coin_pub: coin.coinPub,
coin_sig: encodeCrock(coinSig),
- denom_pub: coin.denomPub,
+ denom_pub_hash: coin.denomPubHash,
denom_sig: coin.denomSig,
refreshed: (coin.coinSource.type === CoinSourceType.Refresh),
};
diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
index 707849952..28618bcc2 100644
--- a/src/headless/taler-wallet-cli.ts
+++ b/src/headless/taler-wallet-cli.ts
@@ -217,9 +217,10 @@ walletCli
.subcommand("runPendingOpt", "run-pending", {
help: "Run pending operations.",
})
+ .flag("forceNow", ["-f", "--force-now"])
.action(async args => {
await withWallet(args, async wallet => {
- await wallet.runPending();
+ await wallet.runPending(args.runPendingOpt.forceNow);
});
});
diff --git a/src/operations/errors.ts b/src/operations/errors.ts
index 7e97fdb3c..751a57111 100644
--- a/src/operations/errors.ts
+++ b/src/operations/errors.ts
@@ -1,8 +1,6 @@
-import { OperationError } from "../types/walletTypes";
-
/*
This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
+ (C) 2019-2020 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
@@ -17,12 +15,25 @@ import { OperationError } from "../types/walletTypes";
*/
/**
+ * Classes and helpers for error handling specific to wallet operations.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports.
+ */
+import { OperationError } from "../types/walletTypes";
+import { HttpResponse } from "../util/http";
+import { Codec } from "../util/codec";
+
+/**
* This exception is there to let the caller know that an error happened,
* but the error has already been reported by writing it to the database.
*/
export class OperationFailedAndReportedError extends Error {
- constructor(message: string) {
- super(message);
+ constructor(public operationError: OperationError) {
+ super(operationError.message);
// Set the prototype explicitly.
Object.setPrototypeOf(this, OperationFailedAndReportedError.prototype);
@@ -34,8 +45,8 @@ export class OperationFailedAndReportedError extends Error {
* responsible for recording the failure in the database.
*/
export class OperationFailedError extends Error {
- constructor(message: string, public err: OperationError) {
- super(message);
+ constructor(public operationError: OperationError) {
+ super(operationError.message);
// Set the prototype explicitly.
Object.setPrototypeOf(this, OperationFailedError.prototype);
@@ -43,6 +54,65 @@ export class OperationFailedError extends Error {
}
/**
+ * Process an HTTP response that we expect to contain Taler-specific JSON.
+ *
+ * Depending on the status code, we throw an exception. This function
+ * will try to extract Taler-specific error information from the HTTP response
+ * if possible.
+ */
+export async function scrutinizeTalerJsonResponse<T>(
+ resp: HttpResponse,
+ codec: Codec<T>,
+): Promise<T> {
+
+ // FIXME: We should distinguish between different types of error status
+ // to react differently (throttle, report permanent failure)
+
+ // FIXME: Make sure that when we receive an error message,
+ // it looks like a Taler error message
+
+ if (resp.status !== 200) {
+ let exc: OperationFailedError | undefined = undefined;
+ try {
+ const errorJson = await resp.json();
+ const m = `received error response (status ${resp.status})`;
+ exc = new OperationFailedError({
+ type: "protocol",
+ message: m,
+ details: {
+ httpStatusCode: resp.status,
+ errorResponse: errorJson,
+ }
+ });
+ } catch (e) {
+ const m = "could not parse response JSON";
+ exc = new OperationFailedError({
+ type: "network",
+ message: m,
+ details: {
+ status: resp.status,
+ }
+ });
+ }
+ throw exc;
+ }
+ let json: any;
+ try {
+ json = await resp.json();
+ } catch (e) {
+ const m = "could not parse response JSON";
+ throw new OperationFailedError({
+ type: "network",
+ message: m,
+ details: {
+ status: resp.status,
+ }
+ });
+ }
+ return codec.decode(json);
+}
+
+/**
* Run an operation and call the onOpError callback
* when there was an exception or operation error that must be reported.
* The cause will be re-thrown to the caller.
@@ -59,26 +129,28 @@ export async function guardOperationException<T>(
throw e;
}
if (e instanceof OperationFailedError) {
- await onOpError(e.err);
- throw new OperationFailedAndReportedError(e.message);
+ await onOpError(e.operationError);
+ throw new OperationFailedAndReportedError(e.operationError);
}
if (e instanceof Error) {
console.log("guard: caught Error");
- await onOpError({
+ const opErr = {
type: "exception",
message: e.message,
details: {},
- });
- throw new OperationFailedAndReportedError(e.message);
+ }
+ await onOpError(opErr);
+ throw new OperationFailedAndReportedError(opErr);
}
console.log("guard: caught something else");
- await onOpError({
+ const opErr = {
type: "exception",
message: "non-error exception thrown",
details: {
value: e.toString(),
},
- });
- throw new OperationFailedAndReportedError(e.message);
+ };
+ await onOpError(opErr);
+ throw new OperationFailedAndReportedError(opErr);
}
-} \ No newline at end of file
+}
diff --git a/src/operations/exchanges.ts b/src/operations/exchanges.ts
index ed13a1e5b..04238e61d 100644
--- a/src/operations/exchanges.ts
+++ b/src/operations/exchanges.ts
@@ -115,72 +115,78 @@ async function updateExchangeWithKeys(
keysResp = await r.json();
} catch (e) {
const m = `Fetching keys failed: ${e.message}`;
- await setExchangeError(ws, baseUrl, {
+ const opErr = {
type: "network",
details: {
requestUrl: e.config?.url,
},
message: m,
- });
- throw new OperationFailedAndReportedError(m);
+ };
+ await setExchangeError(ws, baseUrl, opErr);
+ throw new OperationFailedAndReportedError(opErr);
}
let exchangeKeysJson: ExchangeKeysJson;
try {
exchangeKeysJson = codecForExchangeKeysJson().decode(keysResp);
} catch (e) {
const m = `Parsing /keys response failed: ${e.message}`;
- await setExchangeError(ws, baseUrl, {
+ const opErr = {
type: "protocol-violation",
details: {},
message: m,
- });
- throw new OperationFailedAndReportedError(m);
+ };
+ await setExchangeError(ws, baseUrl, opErr);
+ throw new OperationFailedAndReportedError(opErr);
}
const lastUpdateTimestamp = exchangeKeysJson.list_issue_date;
if (!lastUpdateTimestamp) {
const m = `Parsing /keys response failed: invalid list_issue_date.`;
- await setExchangeError(ws, baseUrl, {
+ const opErr = {
type: "protocol-violation",
details: {},
message: m,
- });
- throw new OperationFailedAndReportedError(m);
+ };
+ await setExchangeError(ws, baseUrl, opErr);
+ throw new OperationFailedAndReportedError(opErr);
}
if (exchangeKeysJson.denoms.length === 0) {
const m = "exchange doesn't offer any denominations";
- await setExchangeError(ws, baseUrl, {
+ const opErr = {
type: "protocol-violation",
details: {},
message: m,
- });
- throw new OperationFailedAndReportedError(m);
+ };
+ await setExchangeError(ws, baseUrl, opErr);
+ throw new OperationFailedAndReportedError(opErr);
}
const protocolVersion = exchangeKeysJson.version;
if (!protocolVersion) {
const m = "outdate exchange, no version in /keys response";
- await setExchangeError(ws, baseUrl, {
+ const opErr = {
type: "protocol-violation",
details: {},
message: m,
- });
- throw new OperationFailedAndReportedError(m);
+ };
+ await setExchangeError(ws, baseUrl, opErr);
+ throw new OperationFailedAndReportedError(opErr);
}
const versionRes = compare(WALLET_EXCHANGE_PROTOCOL_VERSION, protocolVersion);
if (versionRes?.compatible != true) {
const m = "exchange protocol version not compatible with wallet";
- await setExchangeError(ws, baseUrl, {
+ const opErr = {
type: "protocol-incompatible",
details: {
exchangeProtocolVersion: protocolVersion,
walletProtocolVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
},
message: m,
- });
- throw new OperationFailedAndReportedError(m);
+ };
+ await setExchangeError(ws, baseUrl, opErr);
+ throw new OperationFailedAndReportedError(opErr);
}
const currency = Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value)
@@ -195,7 +201,7 @@ async function updateExchangeWithKeys(
let recoupGroupId: string | undefined = undefined;
await ws.db.runWithWriteTransaction(
- [Stores.exchanges, Stores.denominations],
+ [Stores.exchanges, Stores.denominations, Stores.recoupGroups, Stores.coins],
async tx => {
const r = await tx.get(Stores.exchanges, baseUrl);
if (!r) {
@@ -231,10 +237,11 @@ async function updateExchangeWithKeys(
// Handle recoup
const recoupDenomList = exchangeKeysJson.recoup ?? [];
const newlyRevokedCoinPubs: string[] = [];
- for (const recoupDenomPubHash of recoupDenomList) {
+ console.log("recoup list from exchange", recoupDenomList);
+ for (const recoupInfo of recoupDenomList) {
const oldDenom = await tx.getIndexed(
Stores.denominations.denomPubHashIndex,
- recoupDenomPubHash,
+ recoupInfo.h_denom_pub,
);
if (!oldDenom) {
// We never even knew about the revoked denomination, all good.
@@ -243,18 +250,21 @@ async function updateExchangeWithKeys(
if (oldDenom.isRevoked) {
// We already marked the denomination as revoked,
// this implies we revoked all coins
+ console.log("denom already revoked");
continue;
}
+ console.log("revoking denom", recoupInfo.h_denom_pub);
oldDenom.isRevoked = true;
await tx.put(Stores.denominations, oldDenom);
const affectedCoins = await tx
- .iterIndexed(Stores.coins.denomPubIndex)
+ .iterIndexed(Stores.coins.denomPubHashIndex, recoupInfo.h_denom_pub)
.toArray();
for (const ac of affectedCoins) {
newlyRevokedCoinPubs.push(ac.coinPub);
}
}
if (newlyRevokedCoinPubs.length != 0) {
+ console.log("recouping coins", newlyRevokedCoinPubs);
await createRecoupGroup(ws, tx, newlyRevokedCoinPubs);
}
},
@@ -263,7 +273,7 @@ async function updateExchangeWithKeys(
if (recoupGroupId) {
// Asynchronously start recoup. This doesn't need to finish
// for the exchange update to be considered finished.
- processRecoupGroup(ws, recoupGroupId).catch((e) => {
+ processRecoupGroup(ws, recoupGroupId).catch(e => {
console.log("error while recouping coins:", e);
});
}
diff --git a/src/operations/history.ts b/src/operations/history.ts
index 2cf215a5a..c09aa8d30 100644
--- a/src/operations/history.ts
+++ b/src/operations/history.ts
@@ -41,7 +41,6 @@ import {
} from "../types/history";
import { assertUnreachable } from "../util/assertUnreachable";
import { TransactionHandle, Store } from "../util/query";
-import { ReserveTransactionType } from "../types/ReserveTransaction";
import { timestampCmp } from "../util/time";
/**
diff --git a/src/operations/pending.ts b/src/operations/pending.ts
index 08ec3fc9e..a628d6130 100644
--- a/src/operations/pending.ts
+++ b/src/operations/pending.ts
@@ -427,6 +427,8 @@ async function gatherRecoupPending(
type: PendingOperationType.Recoup,
givesLifeness: true,
recoupGroupId: rg.recoupGroupId,
+ retryInfo: rg.retryInfo,
+ lastError: rg.lastError,
});
});
}
diff --git a/src/operations/recoup.ts b/src/operations/recoup.ts
index 842a67b87..3097dd05c 100644
--- a/src/operations/recoup.ts
+++ b/src/operations/recoup.ts
@@ -40,7 +40,7 @@ import {
import { codecForRecoupConfirmation } from "../types/talerTypes";
import { NotificationType } from "../types/notifications";
-import { processReserve } from "./reserves";
+import { forceQueryReserve } from "./reserves";
import * as Amounts from "../util/amounts";
import { createRefreshGroup, processRefreshGroup } from "./refresh";
@@ -48,7 +48,7 @@ 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";
+import { guardOperationException, scrutinizeTalerJsonResponse } from "./errors";
async function incrementRecoupRetry(
ws: InternalWalletState,
@@ -133,17 +133,17 @@ async function recoupWithdrawCoin(
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(),
+ const recoupConfirmation = await scrutinizeTalerJsonResponse(
+ resp,
+ codecForRecoupConfirmation(),
);
if (recoupConfirmation.reserve_pub !== reservePub) {
throw Error(`Coin's reserve doesn't match reserve on recoup`);
}
+ // FIXME: verify signature
+
// FIXME: verify that our expectations about the amount match
await ws.db.runWithWriteTransaction(
@@ -178,8 +178,8 @@ async function recoupWithdrawCoin(
type: NotificationType.RecoupFinished,
});
- processReserve(ws, reserve.reservePub).catch(e => {
- console.log("processing reserve after recoup failed:", e);
+ forceQueryReserve(ws, reserve.reservePub).catch(e => {
+ console.log("re-querying reserve after recoup failed:", e);
});
}
@@ -196,12 +196,11 @@ async function recoupRefreshCoin(
const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin);
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
+ console.log("making recoup request");
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(),
+ const recoupConfirmation = await scrutinizeTalerJsonResponse(
+ resp,
+ codecForRecoupConfirmation(),
);
if (recoupConfirmation.old_coin_pub != cs.oldCoinPub) {
@@ -283,11 +282,14 @@ async function processRecoupGroupImpl(
if (forceNow) {
await resetRecoupGroupRetry(ws, recoupGroupId);
}
+ console.log("in processRecoupGroupImpl");
const recoupGroup = await ws.db.get(Stores.recoupGroups, recoupGroupId);
if (!recoupGroup) {
return;
}
+ console.log(recoupGroup);
if (recoupGroup.timestampFinished) {
+ console.log("recoup group finished");
return;
}
const ps = recoupGroup.coinPubs.map((x, i) =>
@@ -317,11 +319,11 @@ export async function createRecoupGroup(
const coinPub = coinPubs[coinIdx];
const coin = await tx.get(Stores.coins, coinPub);
if (!coin) {
- recoupGroup.recoupFinishedPerCoin[coinIdx] = true;
+ await putGroupAsFinished(tx, recoupGroup, coinIdx);
continue;
}
if (Amounts.isZero(coin.currentAmount)) {
- recoupGroup.recoupFinishedPerCoin[coinIdx] = true;
+ await putGroupAsFinished(tx, recoupGroup, coinIdx);
continue;
}
coin.currentAmount = Amounts.getZero(coin.currentAmount.currency);
diff --git a/src/operations/refund.ts b/src/operations/refund.ts
index 9d1c5308e..c856bb7d2 100644
--- a/src/operations/refund.ts
+++ b/src/operations/refund.ts
@@ -440,7 +440,7 @@ async function processPurchaseApplyRefundImpl(
body = await resp.json();
} catch {}
const m = "refund request (at exchange) failed";
- throw new OperationFailedError(m, {
+ throw new OperationFailedError({
message: m,
type: "network",
details: {
diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts
index c909555fe..efca08a45 100644
--- a/src/operations/reserves.ts
+++ b/src/operations/reserves.ts
@@ -203,6 +203,35 @@ export async function createReserve(
}
/**
+ * Re-query the status of a reserve.
+ */
+export async function forceQueryReserve(
+ ws: InternalWalletState,
+ reservePub: string,
+): Promise<void> {
+ await ws.db.runWithWriteTransaction([Stores.reserves], async (tx) => {
+ const reserve = await tx.get(Stores.reserves, reservePub);
+ if (!reserve) {
+ return;
+ }
+ // Only force status query where it makes sense
+ switch (reserve.reserveStatus) {
+ case ReserveRecordStatus.DORMANT:
+ case ReserveRecordStatus.WITHDRAWING:
+ case ReserveRecordStatus.QUERYING_STATUS:
+ break;
+ default:
+ return;
+ }
+ reserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
+ reserve.retryInfo = initRetryInfo();
+ await tx.put(Stores.reserves, reserve);
+
+ });
+ await processReserve(ws, reservePub);
+}
+
+/**
* First fetch information requred to withdraw from the reserve,
* then deplete the reserve, withdrawing coins until it is empty.
*
@@ -408,7 +437,7 @@ async function updateReserve(
console.log("got reserves/${RESERVE_PUB} response", await resp.json());
if (resp.status === 404) {
const m = "reserve not known to the exchange yet"
- throw new OperationFailedError(m, {
+ throw new OperationFailedError({
type: "waiting",
message: m,
details: {},
@@ -420,12 +449,13 @@ async function updateReserve(
} catch (e) {
logger.trace("caught exception for reserve/status");
const m = e.message;
- await incrementReserveRetry(ws, reservePub, {
+ const opErr = {
type: "network",
details: {},
message: m,
- });
- throw new OperationFailedAndReportedError(m);
+ };
+ await incrementReserveRetry(ws, reservePub, opErr);
+ throw new OperationFailedAndReportedError(opErr);
}
const respJson = await resp.json();
const reserveInfo = codecForReserveStatus().decode(respJson);
@@ -600,13 +630,14 @@ async function depleteReserve(
logger.trace(`got denom list`);
if (denomsForWithdraw.length === 0) {
const m = `Unable to withdraw from reserve, no denominations are available to withdraw.`;
- await incrementReserveRetry(ws, reserve.reservePub, {
+ const opErr = {
type: "internal",
message: m,
details: {},
- });
+ };
+ await incrementReserveRetry(ws, reserve.reservePub, opErr);
console.log(m);
- throw new OperationFailedAndReportedError(m);
+ throw new OperationFailedAndReportedError(opErr);
}
logger.trace("selected denominations");
diff --git a/src/operations/withdraw.ts b/src/operations/withdraw.ts
index 478aa4ceb..09d912bcc 100644
--- a/src/operations/withdraw.ts
+++ b/src/operations/withdraw.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
+ (C) 2019-2029 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
@@ -33,7 +33,10 @@ import {
WithdrawDetails,
OperationError,
} from "../types/walletTypes";
-import { WithdrawOperationStatusResponse, codecForWithdrawOperationStatusResponse } from "../types/talerTypes";
+import {
+ codecForWithdrawOperationStatusResponse,
+ codecForWithdrawResponse,
+} from "../types/talerTypes";
import { InternalWalletState } from "./state";
import { parseWithdrawUri } from "../util/taleruri";
import { Logger } from "../util/logging";
@@ -41,7 +44,7 @@ import { updateExchangeFromUrl, getExchangeTrust } from "./exchanges";
import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions";
import * as LibtoolVersion from "../util/libtoolVersion";
-import { guardOperationException } from "./errors";
+import { guardOperationException, scrutinizeTalerJsonResponse } from "./errors";
import { NotificationType } from "../types/notifications";
import {
getTimestampNow,
@@ -49,7 +52,6 @@ import {
timestampCmp,
timestampSubtractDuraction,
} from "../util/time";
-import { Store } from "../util/query";
const logger = new Logger("withdraw.ts");
@@ -62,7 +64,7 @@ function isWithdrawableDenom(d: DenominationRecord) {
);
const remaining = getDurationRemaining(lastPossibleWithdraw, now);
const stillOkay = remaining.d_ms !== 0;
- return started && stillOkay;
+ return started && stillOkay && !d.isRevoked;
}
/**
@@ -144,8 +146,9 @@ async function getPossibleDenoms(
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl)
.filter(d => {
return (
- d.status === DenominationStatus.Unverified ||
- d.status === DenominationStatus.VerifiedGood
+ (d.status === DenominationStatus.Unverified ||
+ d.status === DenominationStatus.VerifiedGood) &&
+ !d.isRevoked
);
});
}
@@ -199,13 +202,12 @@ async function processPlanchet(
wd.reserve_pub = planchet.reservePub;
wd.reserve_sig = planchet.withdrawSig;
wd.coin_ev = planchet.coinEv;
- const reqUrl = new URL(`reserves/${planchet.reservePub}/withdraw`, exchange.baseUrl).href;
+ const reqUrl = new URL(
+ `reserves/${planchet.reservePub}/withdraw`,
+ exchange.baseUrl,
+ ).href;
const resp = await ws.http.postJson(reqUrl, wd);
- if (resp.status !== 200) {
- throw Error(`unexpected status ${resp.status} for withdraw`);
- }
-
- const r = await resp.json();
+ const r = await scrutinizeTalerJsonResponse(resp, codecForWithdrawResponse());
const denomSig = await ws.cryptoApi.rsaUnblind(
r.ev_sig,
@@ -236,8 +238,8 @@ async function processPlanchet(
type: CoinSourceType.Withdraw,
coinIndex: coinIdx,
reservePub: planchet.reservePub,
- withdrawSessionId: withdrawalSessionId
- }
+ withdrawSessionId: withdrawalSessionId,
+ },
};
let withdrawSessionFinished = false;
@@ -458,11 +460,11 @@ async function processWithdrawCoin(
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/ReserveTransaction.ts b/src/types/ReserveTransaction.ts
index e889f36a8..ba5ce3ffc 100644
--- a/src/types/ReserveTransaction.ts
+++ b/src/types/ReserveTransaction.ts
@@ -40,7 +40,7 @@ import { Timestamp, codecForTimestamp } from "../util/time";
export const enum ReserveTransactionType {
Withdraw = "WITHDRAW",
Deposit = "DEPOSIT",
- Payback = "PAYBACK",
+ Recoup = "RECOUP",
Closing = "CLOSING",
}
@@ -139,8 +139,8 @@ export interface ReserveClosingTransaction {
timestamp: Timestamp;
}
-export interface ReservePaybackTransaction {
- type: ReserveTransactionType.Payback;
+export interface ReserveRecoupTransaction {
+ type: ReserveTransactionType.Recoup;
/**
* Amount paid back.
@@ -148,16 +148,6 @@ export interface ReservePaybackTransaction {
amount: AmountString;
/**
- * Receiver account details.
- */
- receiver_account_details: any;
-
- /**
- * Wire transfer identifier.
- */
- wire_transfer: any;
-
- /**
* This is a signature over
* a struct TALER_PaybackConfirmationPS with purpose
* TALER_SIGNATURE_EXCHANGE_CONFIRM_PAYBACK.
@@ -187,7 +177,7 @@ export type ReserveTransaction =
| ReserveWithdrawTransaction
| ReserveDepositTransaction
| ReserveClosingTransaction
- | ReservePaybackTransaction;
+ | ReserveRecoupTransaction;
export const codecForReserveWithdrawTransaction = () =>
typecheckedCodec<ReserveWithdrawTransaction>(
@@ -229,18 +219,16 @@ export const codecForReserveClosingTransaction = () =>
.build("ReserveClosingTransaction"),
);
-export const codecForReservePaybackTransaction = () =>
- typecheckedCodec<ReservePaybackTransaction>(
- makeCodecForObject<ReservePaybackTransaction>()
+export const codecForReserveRecoupTransaction = () =>
+ typecheckedCodec<ReserveRecoupTransaction>(
+ makeCodecForObject<ReserveRecoupTransaction>()
.property("amount", codecForString)
.property("coin_pub", codecForString)
.property("exchange_pub", codecForString)
.property("exchange_sig", codecForString)
- .property("receiver_account_details", codecForString)
.property("timestamp", codecForTimestamp)
- .property("type", makeCodecForConstString(ReserveTransactionType.Payback))
- .property("wire_transfer", codecForString)
- .build("ReservePaybackTransaction"),
+ .property("type", makeCodecForConstString(ReserveTransactionType.Recoup))
+ .build("ReserveRecoupTransaction"),
);
export const codecForReserveTransaction = () =>
@@ -256,8 +244,8 @@ export const codecForReserveTransaction = () =>
codecForReserveClosingTransaction(),
)
.alternative(
- ReserveTransactionType.Payback,
- codecForReservePaybackTransaction(),
+ ReserveTransactionType.Recoup,
+ codecForReserveRecoupTransaction(),
)
.alternative(
ReserveTransactionType.Deposit,
diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts
index 56c1f82eb..36b45f5ac 100644
--- a/src/types/dbTypes.ts
+++ b/src/types/dbTypes.ts
@@ -1457,6 +1457,11 @@ export namespace Stores {
"denomPubIndex",
"denomPub",
);
+ denomPubHashIndex = new Index<string, CoinRecord>(
+ this,
+ "denomPubHashIndex",
+ "denomPubHash",
+ );
}
class ProposalsStore extends Store<ProposalRecord> {
diff --git a/src/types/notifications.ts b/src/types/notifications.ts
index 34e98fe2c..39930dcca 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",
- RecoupStarted = "payback-started",
- RecoupFinished = "payback-finished",
+ RecoupStarted = "recoup-started",
+ RecoupFinished = "recoup-finished",
RefreshRevealed = "refresh-revealed",
RefreshMelted = "refresh-melted",
RefreshStarted = "refresh-started",
@@ -44,7 +44,7 @@ export const enum NotificationType {
RefundFinished = "refund-finished",
ExchangeOperationError = "exchange-operation-error",
RefreshOperationError = "refresh-operation-error",
- RecoupOperationError = "refresh-operation-error",
+ RecoupOperationError = "recoup-operation-error",
RefundApplyOperationError = "refund-apply-error",
RefundStatusOperationError = "refund-status-error",
ProposalOperationError = "proposal-error",
@@ -82,11 +82,11 @@ export interface RefundsSubmittedNotification {
proposalId: string;
}
-export interface PaybackStartedNotification {
+export interface RecoupStartedNotification {
type: NotificationType.RecoupStarted;
}
-export interface PaybackFinishedNotification {
+export interface RecoupFinishedNotification {
type: NotificationType.RecoupFinished;
}
@@ -171,6 +171,10 @@ export interface WithdrawOperationErrorNotification {
type: NotificationType.WithdrawOperationError;
}
+export interface RecoupOperationErrorNotification {
+ type: NotificationType.RecoupOperationError;
+}
+
export interface ReserveOperationErrorNotification {
type: NotificationType.ReserveOperationError;
operationError: OperationError;
@@ -197,8 +201,8 @@ export type WalletNotification =
| ProposalAcceptedNotification
| ProposalDownloadedNotification
| RefundsSubmittedNotification
- | PaybackStartedNotification
- | PaybackFinishedNotification
+ | RecoupStartedNotification
+ | RecoupFinishedNotification
| RefreshMeltedNotification
| RefreshRevealedNotification
| RefreshStartedNotification
@@ -214,4 +218,5 @@ export type WalletNotification =
| RefundQueriedNotification
| WithdrawSessionCreatedNotification
| CoinWithdrawnNotification
- | WildcardNotification;
+ | WildcardNotification
+ | RecoupOperationErrorNotification;
diff --git a/src/types/pending.ts b/src/types/pending.ts
index 5d732c520..d9d17a3b9 100644
--- a/src/types/pending.ts
+++ b/src/types/pending.ts
@@ -204,6 +204,8 @@ export interface PendingRefundApplyOperation {
export interface PendingRecoupOperation {
type: PendingOperationType.Recoup;
recoupGroupId: string;
+ retryInfo: RetryInfo;
+ lastError: OperationError | undefined;
}
/**
diff --git a/src/types/talerTypes.ts b/src/types/talerTypes.ts
index e65c82383..2ecb82340 100644
--- a/src/types/talerTypes.ts
+++ b/src/types/talerTypes.ts
@@ -148,10 +148,10 @@ export class Auditor {
*/
export interface RecoupRequest {
/**
- * Denomination public key of the coin we want to get
+ * Hashed enomination public key of the coin we want to get
* paid back.
*/
- denom_pub: string;
+ denom_pub_hash: string;
/**
* Signature over the coin public key by the denomination.
@@ -744,6 +744,10 @@ export class TipPickupGetResponse {
stamp_created: Timestamp;
}
+export class WithdrawResponse {
+ ev_sig: string;
+}
+
export type AmountString = string;
export type Base32String = string;
export type EddsaSignatureString = string;
@@ -976,3 +980,11 @@ export const codecForRecoupConfirmation = () =>
.property("exchange_pub", codecForString)
.build("RecoupConfirmation"),
);
+
+
+export const codecForWithdrawResponse = () =>
+ typecheckedCodec<WithdrawResponse>(
+ makeCodecForObject<WithdrawResponse>()
+ .property("ev_sig", codecForString)
+ .build("WithdrawResponse"),
+ ); \ No newline at end of file
diff --git a/src/wallet.ts b/src/wallet.ts
index 3b619f874..9cba1360e 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -113,6 +113,7 @@ import {
} from "./operations/refund";
import { durationMin, Duration } from "./util/time";
import { processRecoupGroup } from "./operations/recoup";
+import { OperationFailedAndReportedError } from "./operations/errors";
const builtinCurrencies: CurrencyRecord[] = [
{
@@ -235,7 +236,11 @@ export class Wallet {
try {
await this.processOnePendingOperation(p, forceNow);
} catch (e) {
- console.error(e);
+ if (e instanceof OperationFailedAndReportedError) {
+ console.error("Operation failed:", JSON.stringify(e.operationError, undefined, 2));
+ } else {
+ console.error(e);
+ }
}
}
}