improved error reporting / towards a working recoup
This commit is contained in:
parent
2ec6799c8c
commit
b5b8f96cc9
@ -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),
|
||||
};
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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
|
||||
@ -16,13 +14,26 @@ import { OperationError } from "../types/walletTypes";
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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,14 +45,73 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -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";
|
||||
|
||||
/**
|
||||
|
@ -427,6 +427,8 @@ async function gatherRecoupPending(
|
||||
type: PendingOperationType.Recoup,
|
||||
givesLifeness: true,
|
||||
recoupGroupId: rg.recoupGroupId,
|
||||
retryInfo: rg.retryInfo,
|
||||
lastError: rg.lastError,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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: {
|
||||
|
@ -202,6 +202,35 @@ export async function createReserve(
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
|
@ -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]) {
|
||||
|
@ -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,24 +139,14 @@ export interface ReserveClosingTransaction {
|
||||
timestamp: Timestamp;
|
||||
}
|
||||
|
||||
export interface ReservePaybackTransaction {
|
||||
type: ReserveTransactionType.Payback;
|
||||
export interface ReserveRecoupTransaction {
|
||||
type: ReserveTransactionType.Recoup;
|
||||
|
||||
/**
|
||||
* Amount paid back.
|
||||
*/
|
||||
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
|
||||
@ -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,
|
||||
|
@ -1457,6 +1457,11 @@ export namespace Stores {
|
||||
"denomPubIndex",
|
||||
"denomPub",
|
||||
);
|
||||
denomPubHashIndex = new Index<string, CoinRecord>(
|
||||
this,
|
||||
"denomPubHashIndex",
|
||||
"denomPubHash",
|
||||
);
|
||||
}
|
||||
|
||||
class ProposalsStore extends Store<ProposalRecord> {
|
||||
|
@ -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;
|
||||
|
@ -204,6 +204,8 @@ export interface PendingRefundApplyOperation {
|
||||
export interface PendingRecoupOperation {
|
||||
type: PendingOperationType.Recoup;
|
||||
recoupGroupId: string;
|
||||
retryInfo: RetryInfo;
|
||||
lastError: OperationError | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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"),
|
||||
);
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user