summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2020-07-22 14:22:03 +0530
committerFlorian Dold <florian.dold@gmail.com>2020-07-22 14:22:03 +0530
commite60563fb540c04d9ba751fea69c1fc0f1de598b5 (patch)
tree45f7c86b66dc150d413f9855efaa6341e4a44624
parentf4a8702b3cf93f9edf96d1d1c8cb88baa309e301 (diff)
downloadwallet-core-e60563fb540c04d9ba751fea69c1fc0f1de598b5.tar.gz
wallet-core-e60563fb540c04d9ba751fea69c1fc0f1de598b5.tar.bz2
wallet-core-e60563fb540c04d9ba751fea69c1fc0f1de598b5.zip
consistent error handling for HTTP request (and some other things)
-rw-r--r--src/android/index.ts17
-rw-r--r--src/headless/NodeHttpLib.ts36
-rw-r--r--src/headless/helpers.ts5
-rw-r--r--src/headless/integrationtest.ts7
-rw-r--r--src/headless/merchant.ts8
-rw-r--r--src/headless/taler-wallet-cli.ts7
-rw-r--r--src/operations/errors.ts61
-rw-r--r--src/operations/exchanges.ts118
-rw-r--r--src/operations/pay.ts57
-rw-r--r--src/operations/recoup.ts36
-rw-r--r--src/operations/refresh.ts76
-rw-r--r--src/operations/refund.ts97
-rw-r--r--src/operations/reserves.ts82
-rw-r--r--src/operations/tip.ts18
-rw-r--r--src/operations/transactions.ts87
-rw-r--r--src/operations/withdraw.ts30
-rw-r--r--src/types/dbTypes.ts29
-rw-r--r--src/types/notifications.ts23
-rw-r--r--src/types/pending.ts16
-rw-r--r--src/types/talerTypes.ts68
-rw-r--r--src/types/transactions.ts2
-rw-r--r--src/types/walletTypes.ts9
-rw-r--r--src/util/amounts-test.ts4
-rw-r--r--src/util/amounts.ts2
-rw-r--r--src/util/http.ts267
-rw-r--r--src/wallet.ts18
-rw-r--r--src/webex/pages/withdraw.tsx4
-rw-r--r--src/webex/wxBackend.ts5
28 files changed, 636 insertions, 553 deletions
diff --git a/src/android/index.ts b/src/android/index.ts
index 63d88d70..d7a5897a 100644
--- a/src/android/index.ts
+++ b/src/android/index.ts
@@ -114,6 +114,8 @@ export class AndroidHttpLib implements HttpRequestLibrary {
const headers = new Headers();
if (msg.status != 0) {
const resp: HttpResponse = {
+ // FIXME: pass through this URL
+ requestUrl: "",
headers,
status: msg.status,
json: async () => JSON.parse(msg.responseText),
@@ -196,7 +198,10 @@ class AndroidWalletMessageHandler {
}
case "getWithdrawalDetailsForAmount": {
const wallet = await this.wp.promise;
- return await wallet.getWithdrawalDetailsForAmount(args.exchangeBaseUrl, args.amount);
+ return await wallet.getWithdrawalDetailsForAmount(
+ args.exchangeBaseUrl,
+ args.amount,
+ );
}
case "withdrawTestkudos": {
const wallet = await this.wp.promise;
@@ -218,7 +223,10 @@ class AndroidWalletMessageHandler {
}
case "setExchangeTosAccepted": {
const wallet = await this.wp.promise;
- await wallet.acceptExchangeTermsOfService(args.exchangeBaseUrl, args.acceptedEtag);
+ await wallet.acceptExchangeTermsOfService(
+ args.exchangeBaseUrl,
+ args.acceptedEtag,
+ );
return {};
}
case "retryPendingNow": {
@@ -237,7 +245,10 @@ class AndroidWalletMessageHandler {
}
case "acceptManualWithdrawal": {
const wallet = await this.wp.promise;
- const res = await wallet.acceptManualWithdrawal(args.exchangeBaseUrl, Amounts.parseOrThrow(args.amount));
+ const res = await wallet.acceptManualWithdrawal(
+ args.exchangeBaseUrl,
+ Amounts.parseOrThrow(args.amount),
+ );
return res;
}
case "startTunnel": {
diff --git a/src/headless/NodeHttpLib.ts b/src/headless/NodeHttpLib.ts
index 118fb9e9..d109c3b7 100644
--- a/src/headless/NodeHttpLib.ts
+++ b/src/headless/NodeHttpLib.ts
@@ -27,6 +27,8 @@ import {
} from "../util/http";
import { RequestThrottler } from "../util/RequestThrottler";
import Axios from "axios";
+import { OperationFailedError, makeErrorDetails } from "../operations/errors";
+import { TalerErrorCode } from "../TalerErrorCode";
/**
* Implementation of the HTTP request library interface for node.
@@ -63,17 +65,44 @@ export class NodeHttpLib implements HttpRequestLibrary {
const respText = resp.data;
if (typeof respText !== "string") {
- throw Error("unexpected response type");
+ throw new OperationFailedError(
+ makeErrorDetails(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ "unexpected response type",
+ {
+ httpStatusCode: resp.status,
+ requestUrl: url,
+ },
+ ),
+ );
}
const makeJson = async (): Promise<any> => {
let responseJson;
try {
responseJson = JSON.parse(respText);
} catch (e) {
- throw Error("Invalid JSON from HTTP response");
+ throw new OperationFailedError(
+ makeErrorDetails(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ "invalid JSON",
+ {
+ httpStatusCode: resp.status,
+ requestUrl: url,
+ },
+ ),
+ );
}
if (responseJson === null || typeof responseJson !== "object") {
- throw Error("Invalid JSON from HTTP response");
+ throw new OperationFailedError(
+ makeErrorDetails(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ "invalid JSON",
+ {
+ httpStatusCode: resp.status,
+ requestUrl: url,
+ },
+ ),
+ );
}
return responseJson;
};
@@ -82,6 +111,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
headers.set(hn, resp.headers[hn]);
}
return {
+ requestUrl: url,
headers,
status: resp.status,
text: async () => resp.data,
diff --git a/src/headless/helpers.ts b/src/headless/helpers.ts
index 67ba62df..e451db55 100644
--- a/src/headless/helpers.ts
+++ b/src/headless/helpers.ts
@@ -143,7 +143,10 @@ export async function withdrawTestBalance(
exchangeBaseUrl = "https://exchange.test.taler.net/",
): Promise<void> {
await myWallet.updateExchangeFromUrl(exchangeBaseUrl, true);
- const reserveResponse = await myWallet.acceptManualWithdrawal(exchangeBaseUrl, Amounts.parseOrThrow(amount));
+ const reserveResponse = await myWallet.acceptManualWithdrawal(
+ exchangeBaseUrl,
+ Amounts.parseOrThrow(amount),
+ );
const reservePub = reserveResponse.reservePub;
diff --git a/src/headless/integrationtest.ts b/src/headless/integrationtest.ts
index 786907a0..db96d57c 100644
--- a/src/headless/integrationtest.ts
+++ b/src/headless/integrationtest.ts
@@ -25,7 +25,6 @@ import { NodeHttpLib } from "./NodeHttpLib";
import { Wallet } from "../wallet";
import { Configuration } from "../util/talerconfig";
import { Amounts, AmountJson } from "../util/amounts";
-import { OperationFailedAndReportedError, OperationFailedError } from "../operations/errors";
const logger = new Logger("integrationtest.ts");
@@ -70,9 +69,9 @@ async function makePayment(
}
const confirmPayResult = await wallet.confirmPay(
- preparePayResult.proposalId,
- undefined,
- );
+ preparePayResult.proposalId,
+ undefined,
+ );
console.log("confirmPayResult", confirmPayResult);
diff --git a/src/headless/merchant.ts b/src/headless/merchant.ts
index 3324924f..34ca5564 100644
--- a/src/headless/merchant.ts
+++ b/src/headless/merchant.ts
@@ -37,7 +37,10 @@ export class MerchantBackendConnection {
reason: string,
refundAmount: string,
): Promise<string> {
- const reqUrl = new URL(`private/orders/${orderId}/refund`, this.merchantBaseUrl);
+ const reqUrl = new URL(
+ `private/orders/${orderId}/refund`,
+ this.merchantBaseUrl,
+ );
const refundReq = {
reason,
refund: refundAmount,
@@ -123,7 +126,8 @@ export class MerchantBackendConnection {
}
async checkPayment(orderId: string): Promise<CheckPaymentResponse> {
- const reqUrl = new URL(`private/orders/${orderId}`, this.merchantBaseUrl).href;
+ const reqUrl = new URL(`private/orders/${orderId}`, this.merchantBaseUrl)
+ .href;
const resp = await axios({
method: "get",
url: reqUrl,
diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
index fc13d77f..b8ae84d7 100644
--- a/src/headless/taler-wallet-cli.ts
+++ b/src/headless/taler-wallet-cli.ts
@@ -30,7 +30,7 @@ import {
setupRefreshPlanchet,
encodeCrock,
} from "../crypto/talerCrypto";
-import { OperationFailedAndReportedError, OperationFailedError } from "../operations/errors";
+import { OperationFailedAndReportedError } from "../operations/errors";
import { Bank } from "./bank";
import { classifyTalerUri, TalerUriType } from "../util/taleruri";
import { Configuration } from "../util/talerconfig";
@@ -527,7 +527,10 @@ advancedCli
.maybeOption("sessionIdOverride", ["--session-id"], clk.STRING)
.action(async (args) => {
await withWallet(args, async (wallet) => {
- wallet.confirmPay(args.payConfirm.proposalId, args.payConfirm.sessionIdOverride);
+ wallet.confirmPay(
+ args.payConfirm.proposalId,
+ args.payConfirm.sessionIdOverride,
+ );
});
});
diff --git a/src/operations/errors.ts b/src/operations/errors.ts
index 01a8283c..198d3f8c 100644
--- a/src/operations/errors.ts
+++ b/src/operations/errors.ts
@@ -23,14 +23,15 @@
/**
* Imports.
*/
-import { OperationError } from "../types/walletTypes";
+import { OperationErrorDetails } from "../types/walletTypes";
+import { TalerErrorCode } from "../TalerErrorCode";
/**
* 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(public operationError: OperationError) {
+ constructor(public operationError: OperationErrorDetails) {
super(operationError.message);
// Set the prototype explicitly.
@@ -43,7 +44,15 @@ export class OperationFailedAndReportedError extends Error {
* responsible for recording the failure in the database.
*/
export class OperationFailedError extends Error {
- constructor(public operationError: OperationError) {
+ static fromCode(
+ ec: TalerErrorCode,
+ message: string,
+ details: Record<string, unknown>,
+ ): OperationFailedError {
+ return new OperationFailedError(makeErrorDetails(ec, message, details));
+ }
+
+ constructor(public operationError: OperationErrorDetails) {
super(operationError.message);
// Set the prototype explicitly.
@@ -51,6 +60,19 @@ export class OperationFailedError extends Error {
}
}
+export function makeErrorDetails(
+ ec: TalerErrorCode,
+ message: string,
+ details: Record<string, unknown>,
+): OperationErrorDetails {
+ return {
+ talerErrorCode: ec,
+ talerErrorHint: `Error: ${TalerErrorCode[ec]}`,
+ details: details,
+ message,
+ };
+}
+
/**
* Run an operation and call the onOpError callback
* when there was an exception or operation error that must be reported.
@@ -58,7 +80,7 @@ export class OperationFailedError extends Error {
*/
export async function guardOperationException<T>(
op: () => Promise<T>,
- onOpError: (e: OperationError) => Promise<void>,
+ onOpError: (e: OperationErrorDetails) => Promise<void>,
): Promise<T> {
try {
return await op();
@@ -71,21 +93,28 @@ export async function guardOperationException<T>(
throw new OperationFailedAndReportedError(e.operationError);
}
if (e instanceof Error) {
- const opErr = {
- type: "exception",
- message: e.message,
- details: {},
- };
+ const opErr = makeErrorDetails(
+ TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+ `unexpected exception (message: ${e.message})`,
+ {},
+ );
await onOpError(opErr);
throw new OperationFailedAndReportedError(opErr);
}
- const opErr = {
- type: "exception",
- message: "unexpected exception thrown",
- details: {
- value: e.toString(),
- },
- };
+ // Something was thrown that is not even an exception!
+ // Try to stringify it.
+ let excString: string;
+ try {
+ excString = e.toString();
+ } catch (e) {
+ // Something went horribly wrong.
+ excString = "can't stringify exception";
+ }
+ const opErr = makeErrorDetails(
+ TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+ `unexpected exception (not an exception, ${excString})`,
+ {},
+ );
await onOpError(opErr);
throw new OperationFailedAndReportedError(opErr);
}
diff --git a/src/operations/exchanges.ts b/src/operations/exchanges.ts
index 6f5ff1d3..ff2cd3da 100644
--- a/src/operations/exchanges.ts
+++ b/src/operations/exchanges.ts
@@ -16,12 +16,11 @@
import { InternalWalletState } from "./state";
import {
- ExchangeKeysJson,
Denomination,
codecForExchangeKeysJson,
codecForExchangeWireJson,
} from "../types/talerTypes";
-import { OperationError } from "../types/walletTypes";
+import { OperationErrorDetails } from "../types/walletTypes";
import {
ExchangeRecord,
ExchangeUpdateStatus,
@@ -38,6 +37,7 @@ import { parsePaytoUri } from "../util/payto";
import {
OperationFailedAndReportedError,
guardOperationException,
+ makeErrorDetails,
} from "./errors";
import {
WALLET_CACHE_BREAKER_CLIENT_VERSION,
@@ -46,6 +46,11 @@ import {
import { getTimestampNow } from "../util/time";
import { compare } from "../util/libtoolVersion";
import { createRecoupGroup, processRecoupGroup } from "./recoup";
+import { TalerErrorCode } from "../TalerErrorCode";
+import {
+ readSuccessResponseJsonOrThrow,
+ readSuccessResponseTextOrThrow,
+} from "../util/http";
async function denominationRecordFromKeys(
ws: InternalWalletState,
@@ -77,7 +82,7 @@ async function denominationRecordFromKeys(
async function setExchangeError(
ws: InternalWalletState,
baseUrl: string,
- err: OperationError,
+ err: OperationErrorDetails,
): Promise<void> {
console.log(`last error for exchange ${baseUrl}:`, err);
const mut = (exchange: ExchangeRecord): ExchangeRecord => {
@@ -102,88 +107,40 @@ async function updateExchangeWithKeys(
if (existingExchangeRecord?.updateStatus != ExchangeUpdateStatus.FetchKeys) {
return;
}
+
const keysUrl = new URL("keys", baseUrl);
keysUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
- let keysResp;
- try {
- const r = await ws.http.get(keysUrl.href);
- if (r.status !== 200) {
- throw Error(`unexpected status for keys: ${r.status}`);
- }
- keysResp = await r.json();
- } catch (e) {
- const m = `Fetching keys failed: ${e.message}`;
- const opErr = {
- type: "network",
- details: {
- requestUrl: e.config?.url,
- },
- message: 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}`;
- const opErr = {
- type: "protocol-violation",
- details: {},
- message: 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.`;
- const opErr = {
- type: "protocol-violation",
- details: {},
- message: m,
- };
- await setExchangeError(ws, baseUrl, opErr);
- throw new OperationFailedAndReportedError(opErr);
- }
+ const resp = await ws.http.get(keysUrl.href);
+ const exchangeKeysJson = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeKeysJson(),
+ );
if (exchangeKeysJson.denoms.length === 0) {
- const m = "exchange doesn't offer any denominations";
- const opErr = {
- type: "protocol-violation",
- details: {},
- message: m,
- };
+ const opErr = makeErrorDetails(
+ TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
+ "exchange doesn't offer any denominations",
+ {
+ exchangeBaseUrl: baseUrl,
+ },
+ );
await setExchangeError(ws, baseUrl, opErr);
throw new OperationFailedAndReportedError(opErr);
}
const protocolVersion = exchangeKeysJson.version;
- if (!protocolVersion) {
- const m = "outdate exchange, no version in /keys response";
- const opErr = {
- type: "protocol-violation",
- details: {},
- message: 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";
- const opErr = {
- type: "protocol-incompatible",
- details: {
+ const opErr = makeErrorDetails(
+ TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
+ "exchange protocol version not compatible with wallet",
+ {
exchangeProtocolVersion: protocolVersion,
walletProtocolVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
},
- message: m,
- };
+ );
await setExchangeError(ws, baseUrl, opErr);
throw new OperationFailedAndReportedError(opErr);
}
@@ -197,6 +154,8 @@ async function updateExchangeWithKeys(
),
);
+ const lastUpdateTimestamp = getTimestampNow();
+
const recoupGroupId: string | undefined = undefined;
await ws.db.runWithWriteTransaction(
@@ -331,11 +290,7 @@ async function updateExchangeWithTermsOfService(
};
const resp = await ws.http.get(reqUrl.href, { headers });
- if (resp.status !== 200) {
- throw Error(`/terms response has unexpected status code (${resp.status})`);
- }
-
- const tosText = await resp.text();
+ const tosText = await readSuccessResponseTextOrThrow(resp);
const tosEtag = resp.headers.get("etag") || undefined;
await ws.db.runWithWriteTransaction([Stores.exchanges], async (tx) => {
@@ -393,14 +348,11 @@ async function updateExchangeWithWireInfo(
reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
const resp = await ws.http.get(reqUrl.href);
- if (resp.status !== 200) {
- throw Error(`/wire response has unexpected status code (${resp.status})`);
- }
- const wiJson = await resp.json();
- if (!wiJson) {
- throw Error("/wire response malformed");
- }
- const wireInfo = codecForExchangeWireJson().decode(wiJson);
+ const wireInfo = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeWireJson(),
+ );
+
for (const a of wireInfo.accounts) {
console.log("validating exchange acct");
const isValid = await ws.cryptoApi.isValidWireAccount(
@@ -461,7 +413,7 @@ export async function updateExchangeFromUrl(
baseUrl: string,
forceNow = false,
): Promise<ExchangeRecord> {
- const onOpErr = (e: OperationError): Promise<void> =>
+ const onOpErr = (e: OperationErrorDetails): Promise<void> =>
setExchangeError(ws, baseUrl, e);
return await guardOperationException(
() => updateExchangeFromUrlImpl(ws, baseUrl, forceNow),
diff --git a/src/operations/pay.ts b/src/operations/pay.ts
index 74bfcc70..29b69783 100644
--- a/src/operations/pay.ts
+++ b/src/operations/pay.ts
@@ -38,7 +38,6 @@ import {
} from "../types/dbTypes";
import { NotificationType } from "../types/notifications";
import {
- PayReq,
codecForProposal,
codecForContractTerms,
CoinDepositPermission,
@@ -46,7 +45,7 @@ import {
} from "../types/talerTypes";
import {
ConfirmPayResult,
- OperationError,
+ OperationErrorDetails,
PreparePayResult,
RefreshReason,
} from "../types/walletTypes";
@@ -59,7 +58,10 @@ import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
import { InternalWalletState } from "./state";
import { getTimestampNow, timestampAddDuration } from "../util/time";
import { strcmp, canonicalJson } from "../util/helpers";
-import { httpPostTalerJson } from "../util/http";
+import {
+ readSuccessResponseJsonOrErrorCode,
+ readSuccessResponseJsonOrThrow,
+} from "../util/http";
/**
* Logger.
@@ -515,7 +517,7 @@ function getNextUrl(contractData: WalletContractData): string {
async function incrementProposalRetry(
ws: InternalWalletState,
proposalId: string,
- err: OperationError | undefined,
+ err: OperationErrorDetails | undefined,
): Promise<void> {
await ws.db.runWithWriteTransaction([Stores.proposals], async (tx) => {
const pr = await tx.get(Stores.proposals, proposalId);
@@ -538,7 +540,7 @@ async function incrementProposalRetry(
async function incrementPurchasePayRetry(
ws: InternalWalletState,
proposalId: string,
- err: OperationError | undefined,
+ err: OperationErrorDetails | undefined,
): Promise<void> {
console.log("incrementing purchase pay retry with error", err);
await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
@@ -554,7 +556,9 @@ async function incrementPurchasePayRetry(
pr.lastPayError = err;
await tx.put(Stores.purchases, pr);
});
- ws.notify({ type: NotificationType.PayOperationError });
+ if (err) {
+ ws.notify({ type: NotificationType.PayOperationError, error: err });
+ }
}
export async function processDownloadProposal(
@@ -562,7 +566,7 @@ export async function processDownloadProposal(
proposalId: string,
forceNow = false,
): Promise<void> {
- const onOpErr = (err: OperationError): Promise<void> =>
+ const onOpErr = (err: OperationErrorDetails): Promise<void> =>
incrementProposalRetry(ws, proposalId, err);
await guardOperationException(
() => processDownloadProposalImpl(ws, proposalId, forceNow),
@@ -604,14 +608,15 @@ async function processDownloadProposalImpl(
).href;
logger.trace("downloading contract from '" + orderClaimUrl + "'");
- const proposalResp = await httpPostTalerJson({
- url: orderClaimUrl,
- body: {
- nonce: proposal.noncePub,
- },
- codec: codecForProposal(),
- http: ws.http,
- });
+ const reqestBody = {
+ nonce: proposal.noncePub,
+ };
+
+ const resp = await ws.http.postJson(orderClaimUrl, reqestBody);
+ const proposalResp = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForProposal(),
+ );
// The proposalResp contains the contract terms as raw JSON,
// as the coded to parse them doesn't necessarily round-trip.
@@ -779,15 +784,17 @@ export async function submitPay(
purchase.contractData.merchantBaseUrl,
).href;
- const merchantResp = await httpPostTalerJson({
- url: payUrl,
- body: {
- coins: purchase.coinDepositPermissions,
- session_id: purchase.lastSessionId,
- },
- codec: codecForMerchantPayResponse(),
- http: ws.http,
- });
+ const reqBody = {
+ coins: purchase.coinDepositPermissions,
+ session_id: purchase.lastSessionId,
+ };
+
+ const resp = await ws.http.postJson(payUrl, reqBody);
+
+ const merchantResp = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForMerchantPayResponse(),
+ );
console.log("got success from pay URL", merchantResp);
@@ -1050,7 +1057,7 @@ export async function processPurchasePay(
proposalId: string,
forceNow = false,
): Promise<void> {
- const onOpErr = (e: OperationError): Promise<void> =>
+ const onOpErr = (e: OperationErrorDetails): Promise<void> =>
incrementPurchasePayRetry(ws, proposalId, e);
await guardOperationException(
() => processPurchasePayImpl(ws, proposalId, forceNow),
diff --git a/src/operations/recoup.ts b/src/operations/recoup.ts
index d1b3c3bd..445d029c 100644
--- a/src/operations/recoup.ts
+++ b/src/operations/recoup.ts
@@ -44,17 +44,17 @@ import { forceQueryReserve } from "./reserves";
import { Amounts } from "../util/amounts";
import { createRefreshGroup, processRefreshGroup } from "./refresh";
-import { RefreshReason, OperationError } from "../types/walletTypes";
+import { RefreshReason, OperationErrorDetails } from "../types/walletTypes";
import { TransactionHandle } from "../util/query";
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
import { getTimestampNow } from "../util/time";
import { guardOperationException } from "./errors";
-import { httpPostTalerJson } from "../util/http";
+import { readSuccessResponseJsonOrThrow } from "../util/http";
async function incrementRecoupRetry(
ws: InternalWalletState,
recoupGroupId: string,
- err: OperationError | undefined,
+ err: OperationErrorDetails | undefined,
): Promise<void> {
await ws.db.runWithWriteTransaction([Stores.recoupGroups], async (tx) => {
const r = await tx.get(Stores.recoupGroups, recoupGroupId);
@@ -69,7 +69,9 @@ async function incrementRecoupRetry(
r.lastError = err;
await tx.put(Stores.recoupGroups, r);
});
- ws.notify({ type: NotificationType.RecoupOperationError });
+ if (err) {
+ ws.notify({ type: NotificationType.RecoupOperationError, error: err });
+ }
}
async function putGroupAsFinished(
@@ -147,12 +149,11 @@ async function recoupWithdrawCoin(
const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin);
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
- const recoupConfirmation = await httpPostTalerJson({
- url: reqUrl.href,
- body: recoupRequest,
- codec: codecForRecoupConfirmation(),
- http: ws.http,
- });
+ const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
+ const recoupConfirmation = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForRecoupConfirmation(),
+ );
if (recoupConfirmation.reserve_pub !== reservePub) {
throw Error(`Coin's reserve doesn't match reserve on recoup`);
@@ -222,13 +223,12 @@ 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 recoupConfirmation = await httpPostTalerJson({
- url: reqUrl.href,
- body: recoupRequest,
- codec: codecForRecoupConfirmation(),
- http: ws.http,
- });
+
+ const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
+ const recoupConfirmation = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForRecoupConfirmation(),
+ );
if (recoupConfirmation.old_coin_pub != cs.oldCoinPub) {
throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`);
@@ -298,7 +298,7 @@ export async function processRecoupGroup(
forceNow = false,
): Promise<void> {
await ws.memoProcessRecoup.memo(recoupGroupId, async () => {
- const onOpErr = (e: OperationError): Promise<void> =>
+ const onOpErr = (e: OperationErrorDetails): Promise<void> =>
incrementRecoupRetry(ws, recoupGroupId, e);
return await guardOperationException(
async () => await processRecoupGroupImpl(ws, recoupGroupId, forceNow),
diff --git a/src/operations/refresh.ts b/src/operations/refresh.ts
index 2d7ffad2..4d477d64 100644
--- a/src/operations/refresh.ts
+++ b/src/operations/refresh.ts
@@ -34,7 +34,7 @@ import { Logger } from "../util/logging";
import { getWithdrawDenomList } from "./withdraw";
import { updateExchangeFromUrl } from "./exchanges";
import {
- OperationError,
+ OperationErrorDetails,
CoinPublicKey,
RefreshReason,
RefreshGroupId,
@@ -43,6 +43,11 @@ import { guardOperationException } from "./errors";
import { NotificationType } from "../types/notifications";
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
import { getTimestampNow } from "../util/time";
+import { readSuccessResponseJsonOrThrow } from "../util/http";
+import {
+ codecForExchangeMeltResponse,
+ codecForExchangeRevealResponse,
+} from "../types/talerTypes";
const logger = new Logger("refresh.ts");
@@ -243,34 +248,12 @@ async function refreshMelt(
};
logger.trace(`melt request for coin:`, meltReq);
const resp = await ws.http.postJson(reqUrl.href, meltReq);
- if (resp.status !== 200) {
- console.log(`got status ${resp.status} for refresh/melt`);
- try {
- const respJson = await resp.json();
- console.log(
- `body of refresh/melt error response:`,
- JSON.stringify(respJson, undefined, 2),
- );
- } catch (e) {
- console.log(`body of refresh/melt error response is not JSON`);
- }
- throw Error(`unexpected status code ${resp.status} for refresh/melt`);
- }
-
- const respJson = await resp.json();
-
- logger.trace("melt response:", respJson);
-
- if (resp.status !== 200) {
- console.error(respJson);
- throw Error("refresh failed");
- }
-
- const norevealIndex = respJson.noreveal_index;
+ const meltResponse = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeMeltResponse(),
+ );
- if (typeof norevealIndex !== "number") {
- throw Error("invalid response");
- }
+ const norevealIndex = meltResponse.noreveal_index;
refreshSession.norevealIndex = norevealIndex;
@@ -355,30 +338,15 @@ async function refreshReveal(
refreshSession.exchangeBaseUrl,
);
- let resp;
- try {
- resp = await ws.http.postJson(reqUrl.href, req);
- } catch (e) {
- console.error("got error during /refresh/reveal request");
- console.error(e);
- return;
- }
-
- if (resp.status !== 200) {
- console.error("error: /refresh/reveal returned status " + resp.status);
- return;
- }
-
- const respJson = await resp.json();
-
- if (!respJson.ev_sigs || !Array.isArray(respJson.ev_sigs)) {
- console.error("/refresh/reveal did not contain ev_sigs");
- return;
- }
+ const resp = await ws.http.postJson(reqUrl.href, req);
+ const reveal = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeRevealResponse(),
+ );
const coins: CoinRecord[] = [];
- for (let i = 0; i < respJson.ev_sigs.length; i++) {
+ for (let i = 0; i < reveal.ev_sigs.length; i++) {
const denom = await ws.db.get(Stores.denominations, [
refreshSession.exchangeBaseUrl,
refreshSession.newDenoms[i],
@@ -389,7 +357,7 @@ async function refreshReveal(
}
const pc = refreshSession.planchetsForGammas[norevealIndex][i];
const denomSig = await ws.cryptoApi.rsaUnblind(
- respJson.ev_sigs[i].ev_sig,
+ reveal.ev_sigs[i].ev_sig,
pc.blindingKey,
denom.denomPub,
);
@@ -457,7 +425,7 @@ async function refreshReveal(
async function incrementRefreshRetry(
ws: InternalWalletState,
refreshGroupId: string,
- err: OperationError | undefined,
+ err: OperationErrorDetails | undefined,
): Promise<void> {
await ws.db.runWithWriteTransaction([Stores.refreshGroups], async (tx) => {
const r = await tx.get(Stores.refreshGroups, refreshGroupId);
@@ -472,7 +440,9 @@ async function incrementRefreshRetry(
r.lastError = err;
await tx.put(Stores.refreshGroups, r);
});
- ws.notify({ type: NotificationType.RefreshOperationError });
+ if (err) {
+ ws.notify({ type: NotificationType.RefreshOperationError, error: err });
+ }
}
export async function processRefreshGroup(
@@ -481,7 +451,7 @@ export async function processRefreshGroup(
forceNow = false,
): Promise<void> {
await ws.memoProcessRefresh.memo(refreshGroupId, async () => {
- const onOpErr = (e: OperationError): Promise<void> =>
+ const onOpErr = (e: OperationErrorDetails): Promise<void> =>
incrementRefreshRetry(ws, refreshGroupId, e);
return await guardOperationException(
async () => await processRefreshGroupImpl(ws, refreshGroupId, forceNow),
diff --git a/src/operations/refund.ts b/src/operations/refund.ts
index 5f6ccf9d..1d6561bd 100644
--- a/src/operations/refund.ts
+++ b/src/operations/refund.ts
@@ -25,7 +25,7 @@
*/
import { InternalWalletState } from "./state";
import {
- OperationError,
+ OperationErrorDetails,
RefreshReason,
CoinPublicKey,
} from "../types/walletTypes";
@@ -52,15 +52,18 @@ import { randomBytes } from "../crypto/primitives/nacl-fast";
import { encodeCrock } from "../crypto/talerCrypto";
import { getTimestampNow } from "../util/time";
import { Logger } from "../util/logging";
+import { readSuccessResponseJsonOrThrow } from "../util/http";
const logger = new Logger("refund.ts");
+/**
+ * Retry querying and applying refunds for an order later.
+ */
async function incrementPurchaseQueryRefundRetry(
ws: InternalWalletState,
proposalId: string,
- err: OperationError | undefined,
+ err: OperationErrorDetails | undefined,
): Promise<void> {
- console.log("incrementing purchase refund query retry with error", err);
await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
const pr = await tx.get(Stores.purchases, proposalId);
if (!pr) {
@@ -74,54 +77,12 @@ async function incrementPurchaseQueryRefundRetry(
pr.lastRefundStatusError = err;
await tx.put(Stores.purchases, pr);
});
- ws.notify({ type: NotificationType.RefundStatusOperationError });
-}
-
-export async function getFullRefundFees(
- ws: InternalWalletState,
- refundPermissions: MerchantRefundDetails[],
-): Promise<AmountJson> {
- if (refundPermissions.length === 0) {
- throw Error("no refunds given");
- }
- const coin0 = await ws.db.get(Stores.coins, refundPermissions[0].coin_pub);
- if (!coin0) {
- throw Error("coin not found");
- }
- let feeAcc = Amounts.getZero(
- Amounts.parseOrThrow(refundPermissions[0].refund_amount).currency,
- );
-
- const denoms = await ws.db
- .iterIndex(Stores.denominations.exchangeBaseUrlIndex, coin0.exchangeBaseUrl)
- .toArray();
-
- for (const rp of refundPermissions) {
- const coin = await ws.db.get(Stores.coins, rp.coin_pub);
- if (!coin) {
- throw Error("coin not found");
- }
- const denom = await ws.db.get(Stores.denominations, [
- coin0.exchangeBaseUrl,
- coin.denomPub,
- ]);
- if (!denom) {
- throw Error(`denom not found (${coin.denomPub})`);
- }
- // FIXME: this assumes that the refund already happened.
- // When it hasn't, the refresh cost is inaccurate. To fix this,
- // we need introduce a flag to tell if a coin was refunded or
- // refreshed normally (and what about incremental refunds?)
- const refundAmount = Amounts.parseOrThrow(rp.refund_amount);
- const refundFee = Amounts.parseOrThrow(rp.refund_fee);
- const refreshCost = getTotalRefreshCost(
- denoms,
- denom,
- Amounts.sub(refundAmount, refundFee).amount,
- );
- feeAcc = Amounts.add(feeAcc, refreshCost, refundFee).amount;
+ if (err) {
+ ws.notify({
+ type: NotificationType.RefundStatusOperationError,
+ error: err,
+ });
}
- return feeAcc;
}
function getRefundKey(d: MerchantRefundDetails): string {
@@ -310,14 +271,14 @@ async function acceptRefundResponse(
p.lastRefundStatusError = undefined;
p.refundStatusRetryInfo = initRetryInfo(false);
p.refundStatusRequested = false;
- console.log("refund query done");
+ logger.trace("refund query done");
} else {
// No error, but we need to try again!
p.timestampLastRefundStatus = now;
p.refundStatusRetryInfo.retryCounter++;
updateRetryInfoTimeout(p.refundStatusRetryInfo);
p.lastRefundStatusError = undefined;
- console.log("refund query not done");
+ logger.trace("refund query not done");
}
p.refundsRefreshCost = { ...p.refundsRefreshCost, ...refundsRefreshCost };
@@ -369,7 +330,7 @@ async function startRefundQuery(
async (tx) => {
const p = await tx.get(Stores.purchases, proposalId);
if (!p) {
- console.log("no purchase found for refund URL");
+ logger.error("no purchase found for refund URL");
return false;
}
p.refundStatusRequested = true;
@@ -401,7 +362,7 @@ export async function applyRefund(
): Promise<{ contractTermsHash: string; proposalId: string }> {
const parseResult = parseRefundUri(talerRefundUri);
- console.log("applying refund", parseResult);
+ logger.trace("applying refund", parseResult);
if (!parseResult) {
throw Error("invalid refund URI");
@@ -432,7 +393,7 @@ export async function processPurchaseQueryRefund(
proposalId: string,
forceNow = false,
): Promise<void> {
- const onOpErr = (e: OperationError): Promise<void> =>
+ const onOpErr = (e: OperationErrorDetails): Promise<void> =>
incrementPurchaseQueryRefundRetry(ws, proposalId, e);
await guardOperationException(
() => processPurchaseQueryRefundImpl(ws, proposalId, forceNow),
@@ -464,27 +425,23 @@ async function processPurchaseQueryRefundImpl(
if (!purchase) {
return;
}
+
if (!purchase.refundStatusRequested) {
return;
}
- const refundUrlObj = new URL("refund", purchase.contractData.merchantBaseUrl);
- refundUrlObj.searchParams.set("order_id", purchase.contractData.orderId);
- const refundUrl = refundUrlObj.href;
- let resp;
- try {
- resp = await ws.http.get(refundUrl);
- } catch (e) {
- console.error("error downloading refund permission", e);
- throw e;
- }
- if (resp.status !== 200) {
- throw Error(`unexpected status code (${resp.status}) for /refund`);
- }
+ const request = await ws.http.get(
+ new URL(
+ `orders/${purchase.contractData.orderId}`,
+ purchase.contractData.merchantBaseUrl,
+ ).href,
+ );
- const refundResponse = codecForMerchantRefundResponse().decode(
- await resp.json(),
+ const refundResponse = await readSuccessResponseJsonOrThrow(
+ request,
+ codecForMerchantRefundResponse(),
);
+
await acceptRefundResponse(
ws,
proposalId,
diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts
index 365d6e22..e6b09316 100644
--- a/src/operations/reserves.ts
+++ b/src/operations/reserves.ts
@@ -17,7 +17,7 @@
import {
CreateReserveRequest,
CreateReserveResponse,
- OperationError,
+ OperationErrorDetails,
AcceptWithdrawalResponse,
} from "../types/walletTypes";
import { canonicalizeBaseUrl } from "../util/helpers";
@@ -56,7 +56,7 @@ import {
import {
guardOperationException,
OperationFailedAndReportedError,
- OperationFailedError,
+ makeErrorDetails,
} from "./errors";
import { NotificationType } from "../types/notifications";
import { codecForReserveStatus } from "../types/ReserveStatus";
@@ -67,6 +67,11 @@ import {
} from "../util/reserveHistoryUtil";
import { TransactionHandle } from "../util/query";
import { addPaytoQueryParams } from "../util/payto";
+import { TalerErrorCode } from "../TalerErrorCode";
+import {
+ readSuccessResponseJsonOrErrorCode,
+ throwUnexpectedRequestError,
+} from "../util/http";
const logger = new Logger("reserves.ts");
@@ -107,7 +112,9 @@ export async function createReserve(
if (req.bankWithdrawStatusUrl) {
if (!req.exchangePaytoUri) {
- throw Error("Exchange payto URI must be specified for a bank-integrated withdrawal");
+ throw Error(
+ "Exchange payto URI must be specified for a bank-integrated withdrawal",
+ );
}
bankInfo = {
statusUrl: req.bankWithdrawStatusUrl,
@@ -285,7 +292,7 @@ export async function processReserve(
forceNow = false,
): Promise<void> {
return ws.memoProcessReserve.memo(reservePub, async () => {
- const onOpError = (err: OperationError): Promise<void> =>
+ const onOpError = (err: OperationErrorDetails): Promise<void> =>
incrementReserveRetry(ws, reservePub, err);
await guardOperationException(
() => processReserveImpl(ws, reservePub, forceNow),
@@ -344,7 +351,7 @@ export async function processReserveBankStatus(
ws: InternalWalletState,
reservePub: string,
): Promise<void> {
- const onOpError = (err: OperationError): Promise<void> =>
+ const onOpError = (err: OperationErrorDetails): Promise<void> =>
incrementReserveRetry(ws, reservePub, err);
await guardOperationException(
() => processReserveBankStatusImpl(ws, reservePub),
@@ -423,7 +430,7 @@ async function processReserveBankStatusImpl(
async function incrementReserveRetry(
ws: InternalWalletState,
reservePub: string,
- err: OperationError | undefined,
+ err: OperationErrorDetails | undefined,
): Promise<void> {
await ws.db.runWithWriteTransaction([Stores.reserves], async (tx) => {
const r = await tx.get(Stores.reserves, reservePub);
@@ -444,7 +451,7 @@ async function incrementReserveRetry(
if (err) {
ws.notify({
type: NotificationType.ReserveOperationError,
- operationError: err,
+ error: err,
});
}
}
@@ -466,35 +473,32 @@ async function updateReserve(
return;
}
- const reqUrl = new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl);
- let resp;
- try {
- resp = await ws.http.get(reqUrl.href);
- 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({
- type: "waiting",
- message: m,
- details: {},
+ const resp = await ws.http.get(
+ new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl).href,
+ );
+
+ const result = await readSuccessResponseJsonOrErrorCode(
+ resp,
+ codecForReserveStatus(),
+ );
+ if (result.isError) {
+ if (
+ resp.status === 404 &&
+ result.talerErrorResponse.code === TalerErrorCode.RESERVE_STATUS_UNKNOWN
+ ) {
+ ws.notify({
+ type: NotificationType.ReserveNotYetFound,
+ reservePub,
});
+ await incrementReserveRetry(ws, reservePub, undefined);
+ return;
+ } else {
+ throwUnexpectedRequestError(resp, result.talerErrorResponse);
}
- if (resp.status !== 200) {
- throw Error(`unexpected status code ${resp.status} for reserve/status`);
- }
- } catch (e) {
- logger.trace("caught exception for reserve/status");
- const m = e.message;
- const opErr = {
- type: "network",
- details: {},
- message: m,
- };
- await incrementReserveRetry(ws, reservePub, opErr);
- throw new OperationFailedAndReportedError(opErr);
}
- const respJson = await resp.json();
- const reserveInfo = codecForReserveStatus().decode(respJson);
+
+ const reserveInfo = result.response;
+
const balance = Amounts.parseOrThrow(reserveInfo.balance);
const currency = balance.currency;
await ws.db.runWithWriteTransaction(
@@ -656,14 +660,12 @@ async function depleteReserve(
// Only complain about inability to withdraw if we
// didn't withdraw before.
if (Amounts.isZero(summary.withdrawnAmount)) {
- const m = `Unable to withdraw from reserve, no denominations are available to withdraw.`;
- const opErr = {
- type: "internal",
- message: m,
- details: {},
- };
+ const opErr = makeErrorDetails(
+ TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
+ `Unable to withdraw from reserve, no denominations are available to withdraw.`,
+ {},
+ );
await incrementReserveRetry(ws, reserve.reservePub, opErr);
- console.log(m);
throw new OperationFailedAndReportedError(opErr);
}
return;
diff --git a/src/operations/tip.ts b/src/operations/tip.ts
index 1ae7700a..d121b1cb 100644
--- a/src/operations/tip.ts
+++ b/src/operations/tip.ts
@@ -16,7 +16,7 @@
import { InternalWalletState } from "./state";
import { parseTipUri } from "../util/taleruri";
-import { TipStatus, OperationError } from "../types/walletTypes";
+import { TipStatus, OperationErrorDetails } from "../types/walletTypes";
import {
TipPlanchetDetail,
codecForTipPickupGetResponse,
@@ -43,6 +43,7 @@ import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
import { guardOperationException } from "./errors";
import { NotificationType } from "../types/notifications";
import { getTimestampNow } from "../util/time";
+import { readSuccessResponseJsonOrThrow } from "../util/http";
export async function getTipStatus(
ws: InternalWalletState,
@@ -57,13 +58,10 @@ export async function getTipStatus(
tipStatusUrl.searchParams.set("tip_id", res.merchantTipId);
console.log("checking tip status from", tipStatusUrl.href);
const merchantResp = await ws.http.get(tipStatusUrl.href);
- if (merchantResp.status !== 200) {
- throw Error(`unexpected status ${merchantResp.status} for tip-pickup`);
- }
- const respJson = await merchantResp.json();
- console.log("resp:", respJson);
- const tipPickupStatus = codecForTipPickupGetResponse().decode(respJson);
-
+ const tipPickupStatus = await readSuccessResponseJsonOrThrow(
+ merchantResp,
+ codecForTipPickupGetResponse(),
+ );
console.log("status", tipPickupStatus);
const amount = Amounts.parseOrThrow(tipPickupStatus.amount);
@@ -133,7 +131,7 @@ export async function getTipStatus(
async function incrementTipRetry(
ws: InternalWalletState,
refreshSessionId: string,
- err: OperationError | undefined,
+ err: OperationErrorDetails | undefined,
): Promise<void> {
await ws.db.runWithWriteTransaction([Stores.tips], async (tx) => {
const t = await tx.get(Stores.tips, refreshSessionId);
@@ -156,7 +154,7 @@ export async function processTip(
tipId: string,
forceNow = false,
): Promise<void> {
- const onOpErr = (e: OperationError): Promise<void> =>
+ const onOpErr = (e: OperationErrorDetails): Promise<void> =>
incrementTipRetry(ws, tipId, e);
await guardOperationException(
() => processTipImpl(ws, tipId, forceNow),
diff --git a/src/operations/transactions.ts b/src/operations/transactions.ts
index 85cd8716..f104f107 100644
--- a/src/operations/transactions.ts
+++ b/src/operations/transactions.ts
@@ -177,50 +177,57 @@ export async function getTransactions(
}
switch (wsr.source.type) {
- case WithdrawalSourceType.Reserve: {
- const r = await tx.get(Stores.reserves, wsr.source.reservePub);
- if (!r) {
- break;
- }
- let amountRaw: AmountJson | undefined = undefined;
- if (wsr.withdrawalGroupId === r.initialWithdrawalGroupId) {
- amountRaw = r.instructedAmount;
- } else {
- amountRaw = wsr.denomsSel.totalWithdrawCost;
- }
- let withdrawalDetails: WithdrawalDetails;
- if (r.bankInfo) {
+ case WithdrawalSourceType.Reserve:
+ {
+ const r = await tx.get(Stores.reserves, wsr.source.reservePub);
+ if (!r) {
+ break;
+ }
+ let amountRaw: AmountJson | undefined = undefined;
+ if (wsr.withdrawalGroupId === r.initialWithdrawalGroupId) {
+ amountRaw = r.instructedAmount;
+ } else {
+ amountRaw = wsr.denomsSel.totalWithdrawCost;
+ }
+ let withdrawalDetails: WithdrawalDetails;
+ if (r.bankInfo) {
withdrawalDetails = {
type: WithdrawalType.TalerBankIntegrationApi,
confirmed: true,
bankConfirmationUrl: r.bankInfo.confirmUrl,
};
- } else {
- const exchange = await tx.get(Stores.exchanges, r.exchangeBaseUrl);
- if (!exchange) {
- // FIXME: report somehow
- break;
+ } else {
+ const exchange = await tx.get(
+ Stores.exchanges,
+ r.exchangeBaseUrl,
+ );
+ if (!exchange) {
+ // FIXME: report somehow
+ break;
+ }
+ withdrawalDetails = {
+ type: WithdrawalType.ManualTransfer,
+ exchangePaytoUris:
+ exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [],
+ };
}
- withdrawalDetails = {
- type: WithdrawalType.ManualTransfer,
- exchangePaytoUris: exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [],
- };
+ transactions.push({
+ type: TransactionType.Withdrawal,
+ amountEffective: Amounts.stringify(
+ wsr.denomsSel.totalCoinValue,
+ ),
+ amountRaw: Amounts.stringify(amountRaw),
+ withdrawalDetails,
+ exchangeBaseUrl: wsr.exchangeBaseUrl,
+ pending: !wsr.timestampFinish,
+ timestamp: wsr.timestampStart,
+ transactionId: makeEventId(
+ TransactionType.Withdrawal,
+ wsr.withdrawalGroupId,
+ ),
+ });
}
- transactions.push({
- type: TransactionType.Withdrawal,
- amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
- amountRaw: Amounts.stringify(amountRaw),
- withdrawalDetails,
- exchangeBaseUrl: wsr.exchangeBaseUrl,
- pending: !wsr.timestampFinish,
- timestamp: wsr.timestampStart,
- transactionId: makeEventId(
- TransactionType.Withdrawal,
- wsr.withdrawalGroupId,
- ),
- });
- }
- break;
+ break;
default:
// Tips are reported via their own event
break;
@@ -254,7 +261,7 @@ export async function getTransactions(
type: WithdrawalType.TalerBankIntegrationApi,
confirmed: false,
bankConfirmationUrl: r.bankInfo.confirmUrl,
- }
+ };
} else {
withdrawalDetails = {
type: WithdrawalType.ManualTransfer,
@@ -264,9 +271,7 @@ export async function getTransactions(
transactions.push({
type: TransactionType.Withdrawal,
amountRaw: Amounts.stringify(r.instructedAmount),
- amountEffective: Amounts.stringify(
- r.initialDenomSel.totalCoinValue,
- ),
+ amountEffective: Amounts.stringify(r.initialDenomSel.totalCoinValue),
exchangeBaseUrl: r.exchangeBaseUrl,
pending: true,
timestamp: r.timestampCreated,
diff --git a/src/operations/withdraw.ts b/src/operations/withdraw.ts
index 98969d21..f7879dfe 100644
--- a/src/operations/withdraw.ts
+++ b/src/operations/withdraw.ts
@@ -33,7 +33,7 @@ import {
BankWithdrawDetails,
ExchangeWithdrawDetails,
WithdrawalDetailsResponse,
- OperationError,
+ OperationErrorDetails,
} from "../types/walletTypes";
import {
codecForWithdrawOperationStatusResponse,
@@ -54,7 +54,7 @@ import {
timestampCmp,
timestampSubtractDuraction,
} from "../util/time";
-import { httpPostTalerJson } from "../util/http";
+import { readSuccessResponseJsonOrThrow } from "../util/http";
const logger = new Logger("withdraw.ts");
@@ -142,14 +142,11 @@ export async function getBankWithdrawalInfo(
throw Error(`can't parse URL ${talerWithdrawUri}`);
}
const resp = await ws.http.get(uriResult.statusUrl);
- if (resp.status !== 200) {
- throw Error(
- `unexpected status (${resp.status}) from bank for ${uriResult.statusUrl}`,
- );
- }
- const respJson = await resp.json();
+ const status = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForWithdrawOperationStatusResponse(),
+ );
- const status = codecForWithdrawOperationStatusResponse().decode(respJson);
return {
amount: Amounts.parseOrThrow(status.amount),
confirmTransferUrl: status.confirm_transfer_url,
@@ -310,12 +307,11 @@ async function processPlanchet(
exchange.baseUrl,
).href;
- const r = await httpPostTalerJson({
- url: reqUrl,
- body: wd,
- codec: codecForWithdrawResponse(),
- http: ws.http,
- });
+ const resp = await ws.http.postJson(reqUrl, wd);
+ const r = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForWithdrawResponse(),
+ );
logger.trace(`got response for /withdraw`);
@@ -505,7 +501,7 @@ export async function selectWithdrawalDenoms(
async function incrementWithdrawalRetry(
ws: InternalWalletState,
withdrawalGroupId: string,
- err: OperationError | undefined,
+ err: OperationErrorDetails | undefined,
): Promise<void> {
await ws.db.runWithWriteTransaction([Stores.withdrawalGroups], async (tx) => {
const wsr = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
@@ -530,7 +526,7 @@ export async function processWithdrawGroup(
withdrawalGroupId: string,
forceNow = false,
): Promise<void> {
- const onOpErr = (e: OperationError): Promise<void> =>
+ const onOpErr = (e: OperationErrorDetails): Promise<void> =>
incrementWithdrawalRetry(ws, withdrawalGroupId, e);
await guardOperationException(
() => processWithdrawGroupImpl(ws, withdrawalGroupId, forceNow),
diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts
index b085f83d..252649b0 100644
--- a/src/types/dbTypes.ts
+++ b/src/types/dbTypes.ts
@@ -28,7 +28,6 @@ import {
Auditor,
CoinDepositPermission,
MerchantRefundDetails,
- PayReq,
TipResponse,
ExchangeSignKeyJson,
MerchantInfo,
@@ -36,7 +35,7 @@ import {
} from "./talerTypes";
import { Index, Store } from "../util/query";
-import { OperationError, RefreshReason } from "./walletTypes";
+import { OperationErrorDetails, RefreshReason } from "./walletTypes";
import {
ReserveTransaction,
ReserveCreditTransaction,
@@ -319,7 +318,7 @@ export interface ReserveRecord {
* Last error that happened in a reserve operation
* (either talking to the bank or the exchange).
*/
- lastError: OperationError | undefined;
+ lastError: OperationErrorDetails | undefined;
}
/**
@@ -633,7 +632,7 @@ export interface ExchangeRecord {
*/
updateDiff: ExchangeUpdateDiff | undefined;
- lastError?: OperationError;
+ lastError?: OperationErrorDetails;
}
/**
@@ -890,14 +889,14 @@ export interface ProposalRecord {
*/
retryInfo: RetryInfo;
- lastError: OperationError | undefined;
+ lastError: OperationErrorDetails | undefined;
}
/**
* Status of a tip we got from a merchant.
*/
export interface TipRecord {
- lastError: OperationError | undefined;
+ lastError: OperationErrorDetails | undefined;
/**
* Has the user accepted the tip? Only after the tip has been accepted coins
@@ -982,9 +981,9 @@ export interface RefreshGroupRecord {
*/
retryInfo: RetryInfo;
- lastError: OperationError | undefined;
+ lastError: OperationErrorDetails | undefined;
- lastErrorPerCoin: { [coinIndex: number]: OperationError };
+ lastErrorPerCoin: { [coinIndex: number]: OperationErrorDetails };
refreshGroupId: string;
@@ -1012,7 +1011,7 @@ export interface RefreshGroupRecord {
* Ongoing refresh
*/
export interface RefreshSessionRecord {
- lastError: OperationError | undefined;
+ lastError: OperationErrorDetails | undefined;
/**
* Public key that's being melted in this session.
@@ -1330,7 +1329,7 @@ export interface PurchaseRecord {
payRetryInfo: RetryInfo;
- lastPayError: OperationError | undefined;
+ lastPayError: OperationErrorDetails | undefined;
/**
* Retry information for querying the refund status with the merchant.
@@ -1340,7 +1339,7 @@ export interface PurchaseRecord {
/**
* Last error (or undefined) for querying the refund status with the merchant.
*/
- lastRefundStatusError: OperationError | undefined;
+ lastRefundStatusError: OperationErrorDetails | undefined;
/**
* Continue querying the refund status until this deadline has expired.
@@ -1448,7 +1447,7 @@ export interface DenomSelectionState {
/**
* Group of withdrawal operations that need to be executed.
* (Either for a normal withdrawal or from a tip.)
- *
+ *
* The withdrawal group record is only created after we know
* the coin selection we want to withdraw.
*/
@@ -1492,9 +1491,9 @@ export interface WithdrawalGroupRecord {
* Last error per coin/planchet, or undefined if no error occured for
* the coin/planchet.
*/
- lastErrorPerCoin: { [coinIndex: number]: OperationError };
+ lastErrorPerCoin: { [coinIndex: number]: OperationErrorDetails };
- lastError: OperationError | undefined;
+ lastError: OperationErrorDetails | undefined;
}
export interface BankWithdrawUriRecord {
@@ -1559,7 +1558,7 @@ export interface RecoupGroupRecord {
/**
* Last error that occured, if any.
*/
- lastError: OperationError | undefined;
+ lastError: OperationErrorDetails | undefined;
}
export const enum ImportPayloadType {
diff --git a/src/types/notifications.ts b/src/types/notifications.ts
index ac30b6fe..5d6d2ee1 100644
--- a/src/types/notifications.ts
+++ b/src/types/notifications.ts
@@ -22,7 +22,7 @@
/**
* Imports.
*/
-import { OperationError } from "./walletTypes";
+import { OperationErrorDetails } from "./walletTypes";
import { WithdrawalSource } from "./dbTypes";
export const enum NotificationType {
@@ -54,6 +54,7 @@ export const enum NotificationType {
TipOperationError = "tip-error",
PayOperationError = "pay-error",
WithdrawOperationError = "withdraw-error",
+ ReserveNotYetFound = "reserve-not-yet-found",
ReserveOperationError = "reserve-error",
InternalError = "internal-error",
PendingOperationProcessed = "pending-operation-processed",
@@ -72,6 +73,11 @@ export interface InternalErrorNotification {
exception: any;
}
+export interface ReserveNotYetFoundNotification {
+ type: NotificationType.ReserveNotYetFound;
+ reservePub: string;
+}
+
export interface CoinWithdrawnNotification {
type: NotificationType.CoinWithdrawn;
}
@@ -148,27 +154,32 @@ export interface RefundFinishedNotification {
export interface ExchangeOperationErrorNotification {
type: NotificationType.ExchangeOperationError;
+ error: OperationErrorDetails;
}
export interface RefreshOperationErrorNotification {
type: NotificationType.RefreshOperationError;
+ error: OperationErrorDetails;
}
export interface RefundStatusOperationErrorNotification {
type: NotificationType.RefundStatusOperationError;
+ error: OperationErrorDetails;
}
export interface RefundApplyOperationErrorNotification {
type: NotificationType.RefundApplyOperationError;
+ error: OperationErrorDetails;
}
export interface PayOperationErrorNotification {
type: NotificationType.PayOperationError;
+ error: OperationErrorDetails;
}
export interface ProposalOperationErrorNotification {
type: NotificationType.ProposalOperationError;
- error: OperationError;
+ error: OperationErrorDetails;
}
export interface TipOperationErrorNotification {
@@ -177,16 +188,17 @@ export interface TipOperationErrorNotification {
export interface WithdrawOperationErrorNotification {
type: NotificationType.WithdrawOperationError;
- error: OperationError,
+ error: OperationErrorDetails;
}
export interface RecoupOperationErrorNotification {
type: NotificationType.RecoupOperationError;
+ error: OperationErrorDetails;
}
export interface ReserveOperationErrorNotification {
type: NotificationType.ReserveOperationError;
- operationError: OperationError;
+ error: OperationErrorDetails;
}
export interface ReserveCreatedNotification {
@@ -238,4 +250,5 @@ export type WalletNotification =
| InternalErrorNotification
| PendingOperationProcessedNotification
| ProposalRefusedNotification
- | ReserveRegisteredWithBankNotification; \ No newline at end of file
+ | ReserveRegisteredWithBankNotification
+ | ReserveNotYetFoundNotification;
diff --git a/src/types/pending.ts b/src/types/pending.ts
index f949b7c1..8a1e8436 100644
--- a/src/types/pending.ts
+++ b/src/types/pending.ts
@@ -21,7 +21,7 @@
/**
* Imports.
*/
-import { OperationError, WalletBalance } from "./walletTypes";
+import { OperationErrorDetails, WalletBalance } from "./walletTypes";
import { WithdrawalSource, RetryInfo, ReserveRecordStatus } from "./dbTypes";
import { Timestamp, Duration } from "../util/time";
import { ReserveType } from "./history";
@@ -68,7 +68,7 @@ export interface PendingExchangeUpdateOperation {
stage: ExchangeUpdateOperationStage;
reason: string;
exchangeBaseUrl: string;
- lastError: OperationError | undefined;
+ lastError: OperationErrorDetails | undefined;
}
/**
@@ -112,7 +112,7 @@ export interface PendingReserveOperation {
*/
export interface PendingRefreshOperation {
type: PendingOperationType.Refresh;
- lastError?: OperationError;
+ lastError?: OperationErrorDetails;
refreshGroupId: string;
finishedPerCoin: boolean[];
retryInfo: RetryInfo;
@@ -127,7 +127,7 @@ export interface PendingProposalDownloadOperation {
proposalTimestamp: Timestamp;
proposalId: string;
orderId: string;
- lastError?: OperationError;
+ lastError?: OperationErrorDetails;
retryInfo: RetryInfo;
}
@@ -172,7 +172,7 @@ export interface PendingPayOperation {
proposalId: string;
isReplay: boolean;
retryInfo: RetryInfo;
- lastError: OperationError | undefined;
+ lastError: OperationErrorDetails | undefined;
}
/**
@@ -183,14 +183,14 @@ export interface PendingRefundQueryOperation {
type: PendingOperationType.RefundQuery;
proposalId: string;
retryInfo: RetryInfo;
- lastError: OperationError | undefined;
+ lastError: OperationErrorDetails | undefined;
}
export interface PendingRecoupOperation {
type: PendingOperationType.Recoup;
recoupGroupId: string;
retryInfo: RetryInfo;
- lastError: OperationError | undefined;
+ lastError: OperationErrorDetails | undefined;
}
/**
@@ -199,7 +199,7 @@ export interface PendingRecoupOperation {
export interface PendingWithdrawOperation {
type: PendingOperationType.Withdraw;
source: WithdrawalSource;
- lastError: OperationError | undefined;
+ lastError: OperationErrorDetails | undefined;
withdrawalGroupId: string;
numCoinsWithdrawn: number;
numCoinsTotal: number;
diff --git a/src/types/talerTypes.ts b/src/types/talerTypes.ts
index 232f5f31..ef14684f 100644
--- a/src/types/talerTypes.ts
+++ b/src/types/talerTypes.ts
@@ -433,7 +433,6 @@ export class ContractTerms {
extra: any;
}
-
/**
* Refund permission in the format that the merchant gives it to us.
*/
@@ -788,6 +787,53 @@ export interface MerchantPayResponse {
sig: string;
}
+export interface ExchangeMeltResponse {
+ /**
+ * Which of the kappa indices does the client not have to reveal.
+ */
+ noreveal_index: number;
+
+ /**
+ * Signature of TALER_RefreshMeltConfirmationPS whereby the exchange
+ * affirms the successful melt and confirming the noreveal_index
+ */
+ exchange_sig: EddsaSignatureString;
+
+ /*
+ * public EdDSA key of the exchange that was used to generate the signature.
+ * Should match one of the exchange's signing keys from /keys. Again given
+ * explicitly as the client might otherwise be confused by clock skew as to
+ * which signing key was used.
+ */
+ exchange_pub: EddsaPublicKeyString;
+
+ /*
+ * Base URL to use for operations on the refresh context
+ * (so the reveal operation). If not given,
+ * the base URL is the same as the one used for this request.
+ * Can be used if the base URL for /refreshes/ differs from that
+ * for /coins/, i.e. for load balancing. Clients SHOULD
+ * respect the refresh_base_url if provided. Any HTTP server
+ * belonging to an exchange MUST generate a 307 or 308 redirection
+ * to the correct base URL should a client uses the wrong base
+ * URL, or if the base URL has changed since the melt.
+ *
+ * When melting the same coin twice (technically allowed
+ * as the response might have been lost on the network),
+ * the exchange may return different values for the refresh_base_url.
+ */
+ refresh_base_url?: string;
+}
+
+export interface ExchangeRevealItem {
+ ev_sig: string;
+}
+
+export interface ExchangeRevealResponse {
+ // List of the exchange's blinded RSA signatures on the new coins.
+ ev_sigs: ExchangeRevealItem[];
+}
+
export type AmountString = string;
export type Base32String = string;
export type EddsaSignatureString = string;
@@ -1028,3 +1074,23 @@ export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> =>
makeCodecForObject<MerchantPayResponse>()
.property("sig", codecForString)
.build("MerchantPayResponse");
+
+export const codecForExchangeMeltResponse = (): Codec<ExchangeMeltResponse> =>
+ makeCodecForObject<ExchangeMeltResponse>()
+ .property("exchange_pub", codecForString)
+ .property("exchange_sig", codecForString)
+ .property("noreveal_index", codecForNumber)
+ .property("refresh_base_url", makeCodecOptional(codecForString))
+ .build("ExchangeMeltResponse");
+
+export const codecForExchangeRevealItem = (): Codec<ExchangeRevealItem> =>
+ makeCodecForObject<ExchangeRevealItem>()
+ .property("ev_sig", codecForString)
+ .build("ExchangeRevealItem");
+
+export const codecForExchangeRevealResponse = (): Codec<
+ ExchangeRevealResponse
+> =>
+ makeCodecForObject<ExchangeRevealResponse>()
+ .property("ev_sigs", makeCodecForList(codecForExchangeRevealItem()))
+ .build("ExchangeRevealResponse");
diff --git a/src/types/transactions.ts b/src/types/transactions.ts
index b87726ba..d6262264 100644
--- a/src/types/transactions.ts
+++ b/src/types/transactions.ts
@@ -119,7 +119,7 @@ interface WithdrawalDetailsForManualTransfer {
/**
* Payto URIs that the exchange supports.
- *
+ *
* Already contains the amount and message.
*/
exchangePaytoUris: string[];
diff --git a/src/types/walletTypes.ts b/src/types/walletTypes.ts
index ee7d071c..95ec47b6 100644
--- a/src/types/walletTypes.ts
+++ b/src/types/walletTypes.ts
@@ -303,7 +303,7 @@ export class ReturnCoinsRequest {
* Wire details for the bank account of the customer that will
* receive the funds.
*/
- senderWire?: object;
+ senderWire?: string;
/**
* Verify that a value matches the schema of this class and convert it into a
@@ -406,10 +406,11 @@ export interface WalletDiagnostics {
dbOutdated: boolean;
}
-export interface OperationError {
- type: string;
+export interface OperationErrorDetails {
+ talerErrorCode: number;
+ talerErrorHint: string;
message: string;
- details: any;
+ details: unknown;
}
export interface PlanchetCreationResult {
diff --git a/src/util/amounts-test.ts b/src/util/amounts-test.ts
index e10ee596..afd8caa5 100644
--- a/src/util/amounts-test.ts
+++ b/src/util/amounts-test.ts
@@ -24,9 +24,7 @@ const jAmt = (
currency: string,
): AmountJson => ({ value, fraction, currency });
-const sAmt = (
- s: string
-): AmountJson => Amounts.parseOrThrow(s);
+const sAmt = (s: string): AmountJson => Amounts.parseOrThrow(s);
test("amount addition (simple)", (t) => {
const a1 = jAmt(1, 0, "EUR");
diff --git a/src/util/amounts.ts b/src/util/amounts.ts
index 94aefb3c..1e7f192f 100644
--- a/src/util/amounts.ts
+++ b/src/util/amounts.ts
@@ -349,7 +349,7 @@ function mult(a: AmountJson, n: number): Result {
n = n / 2;
} else {
n = (n - 1) / 2;
- const r2 = add(acc, x)
+ const r2 = add(acc, x);
if (r2.saturated) {
return r2;
}
diff --git a/src/util/http.ts b/src/util/http.ts
index bc054096..abbc8df0 100644
--- a/src/util/http.ts
+++ b/src/util/http.ts
@@ -14,18 +14,26 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Codec } from "./codec";
-import { OperationFailedError } from "../operations/errors";
-
/**
* Helpers for doing XMLHttpRequest-s that are based on ES6 promises.
* Allows for easy mocking for test cases.
*/
/**
+ * Imports
+ */
+import { Codec } from "./codec";
+import { OperationFailedError, makeErrorDetails } from "../operations/errors";
+import { TalerErrorCode } from "../TalerErrorCode";
+import { Logger } from "./logging";
+
+const logger = new Logger("http.ts");
+
+/**
* An HTTP response that is returned by all request methods of this library.
*/
export interface HttpResponse {
+ requestUrl: string;
status: number;
headers: Headers;
json(): Promise<any>;
@@ -67,10 +75,20 @@ export class Headers {
}
/**
- * The request library is bundled into an interface to m responseJson: object & any;ake mocking easy.
+ * Interface for the HTTP request library used by the wallet.
+ *
+ * The request library is bundled into an interface to make mocking and
+ * request tunneling easy.
*/
export interface HttpRequestLibrary {
+ /**
+ * Make an HTTP GET request.
+ */
get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>;
+
+ /**
+ * Make an HTTP POST request with a JSON body.
+ */
postJson(
url: string,
body: any,
@@ -105,18 +123,29 @@ export class BrowserHttpLib implements HttpRequestLibrary {
}
myRequest.onerror = (e) => {
- console.error("http request error");
- reject(Error("could not make XMLHttpRequest"));
+ logger.error("http request error");
+ reject(
+ OperationFailedError.fromCode(
+ TalerErrorCode.WALLET_NETWORK_ERROR,
+ "Could not make request",
+ {
+ requestUrl: url,
+ },
+ ),
+ );
};
myRequest.addEventListener("readystatechange", (e) => {
if (myRequest.readyState === XMLHttpRequest.DONE) {
if (myRequest.status === 0) {
- reject(
- Error(
- "HTTP Request failed (status code 0, maybe URI scheme is wrong?)",
- ),
+ const exc = OperationFailedError.fromCode(
+ TalerErrorCode.WALLET_NETWORK_ERROR,
+ "HTTP request failed (status 0, maybe URI scheme was wrong?)",
+ {
+ requestUrl: url,
+ },
);
+ reject(exc);
return;
}
const makeJson = async (): Promise<any> => {
@@ -124,10 +153,24 @@ export class BrowserHttpLib implements HttpRequestLibrary {
try {
responseJson = JSON.parse(myRequest.responseText);
} catch (e) {
- throw Error("Invalid JSON from HTTP response");
+ throw OperationFailedError.fromCode(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ "Invalid JSON from HTTP response",
+ {
+ requestUrl: url,
+ httpStatusCode: myRequest.status,
+ },
+ );
}
if (responseJson === null || typeof responseJson !== "object") {
- throw Error("Invalid JSON from HTTP response");
+ throw OperationFailedError.fromCode(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ "Invalid JSON from HTTP response",
+ {
+ requestUrl: url,
+ httpStatusCode: myRequest.status,
+ },
+ );
}
return responseJson;
};
@@ -141,13 +184,14 @@ export class BrowserHttpLib implements HttpRequestLibrary {
const parts = line.split(": ");
const headerName = parts.shift();
if (!headerName) {
- console.error("invalid header");
+ logger.warn("skipping invalid header");
return;
}
const value = parts.join(": ");
headerMap.set(headerName, value);
});
const resp: HttpResponse = {
+ requestUrl: url,
status: myRequest.status,
headers: headerMap,
json: makeJson,
@@ -165,7 +209,7 @@ export class BrowserHttpLib implements HttpRequestLibrary {
postJson(
url: string,
- body: any,
+ body: unknown,
opt?: HttpRequestOptions,
): Promise<HttpResponse> {
return this.req("post", url, JSON.stringify(body), opt);
@@ -176,114 +220,121 @@ export class BrowserHttpLib implements HttpRequestLibrary {
}
}
-export interface PostJsonRequest<RespType> {
- http: HttpRequestLibrary;
- url: string;
- body: any;
- codec: Codec<RespType>;
-}
+type TalerErrorResponse = {
+ code: number;
+} & unknown;
-/**
- * Helper for making Taler-style HTTP POST requests with a JSON payload and response.
- */
-export async function httpPostTalerJson<RespType>(
- req: PostJsonRequest<RespType>,
-): Promise<RespType> {
- const resp = await req.http.postJson(req.url, req.body);
+type ResponseOrError<T> =
+ | { isError: false; response: T }
+ | { isError: true; talerErrorResponse: TalerErrorResponse };
- 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,
- },
- });
+export async function readSuccessResponseJsonOrErrorCode<T>(
+ httpResponse: HttpResponse,
+ codec: Codec<T>,
+): Promise<ResponseOrError<T>> {
+ if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
+ const errJson = await httpResponse.json();
+ const talerErrorCode = errJson.code;
+ if (typeof talerErrorCode !== "number") {
+ throw new OperationFailedError(
+ makeErrorDetails(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ "Error response did not contain error code",
+ {
+ requestUrl: httpResponse.requestUrl,
+ },
+ ),
+ );
}
- throw exc;
+ return {
+ isError: true,
+ talerErrorResponse: errJson,
+ };
}
- let json: any;
+ const respJson = await httpResponse.json();
+ let parsedResponse: T;
try {
- json = await resp.json();
+ parsedResponse = codec.decode(respJson);
} catch (e) {
- const m = "could not parse response JSON";
- throw new OperationFailedError({
- type: "network",
- message: m,
- details: {
- status: resp.status,
+ throw OperationFailedError.fromCode(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ "Response invalid",
+ {
+ requestUrl: httpResponse.requestUrl,
+ httpStatusCode: httpResponse.status,
+ validationError: e.toString(),
},
- });
+ );
}
- return req.codec.decode(json);
+ return {
+ isError: false,
+ response: parsedResponse,
+ };
}
-
-export interface GetJsonRequest<RespType> {
- http: HttpRequestLibrary;
- url: string;
- codec: Codec<RespType>;
+export function throwUnexpectedRequestError(
+ httpResponse: HttpResponse,
+ talerErrorResponse: TalerErrorResponse,
+): never {
+ throw new OperationFailedError(
+ makeErrorDetails(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ "Unexpected error code in response",
+ {
+ requestUrl: httpResponse.requestUrl,
+ httpStatusCode: httpResponse.status,
+ errorResponse: talerErrorResponse,
+ },
+ ),
+ );
}
-/**
- * Helper for making Taler-style HTTP GET requests with a JSON payload.
- */
-export async function httpGetTalerJson<RespType>(
- req: GetJsonRequest<RespType>,
-): Promise<RespType> {
- const resp = await req.http.get(req.url);
+export async function readSuccessResponseJsonOrThrow<T>(
+ httpResponse: HttpResponse,
+ codec: Codec<T>,
+): Promise<T> {
+ const r = await readSuccessResponseJsonOrErrorCode(httpResponse, codec);
+ if (!r.isError) {
+ return r.response;
+ }
+ throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
+}
- 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,
- },
- });
+export async function readSuccessResponseTextOrErrorCode<T>(
+ httpResponse: HttpResponse,
+): Promise<ResponseOrError<string>> {
+ if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
+ const errJson = await httpResponse.json();
+ const talerErrorCode = errJson.code;
+ if (typeof talerErrorCode !== "number") {
+ throw new OperationFailedError(
+ makeErrorDetails(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ "Error response did not contain error code",
+ {
+ requestUrl: httpResponse.requestUrl,
+ },
+ ),
+ );
}
- throw exc;
+ return {
+ isError: true,
+ talerErrorResponse: errJson,
+ };
}
- 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,
- },
- });
+ const respJson = await httpResponse.text();
+ return {
+ isError: false,
+ response: respJson,
+ };
+}
+
+export async function readSuccessResponseTextOrThrow<T>(
+ httpResponse: HttpResponse,
+): Promise<string> {
+ const r = await readSuccessResponseTextOrErrorCode(httpResponse);
+ if (!r.isError) {
+ return r.response;
}
- return req.codec.decode(json);
+ throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
}
diff --git a/src/wallet.ts b/src/wallet.ts
index 9df27989..ff72f3c7 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -52,7 +52,7 @@ import {
ReserveRecordStatus,
CoinSourceType,
} from "./types/dbTypes";
-import { MerchantRefundDetails, CoinDumpJson } from "./types/talerTypes";
+import { CoinDumpJson } from "./types/talerTypes";
import {
BenchmarkResult,
ConfirmPayResult,
@@ -106,11 +106,7 @@ import {
} from "./types/pending";
import { WalletNotification, NotificationType } from "./types/notifications";
import { HistoryQuery, HistoryEvent } from "./types/history";
-import {
- processPurchaseQueryRefund,
- getFullRefundFees,
- applyRefund,
-} from "./operations/refund";
+import { processPurchaseQueryRefund, applyRefund } from "./operations/refund";
import { durationMin, Duration } from "./util/time";
import { processRecoupGroup } from "./operations/recoup";
import { OperationFailedAndReportedError } from "./operations/errors";
@@ -372,12 +368,12 @@ export class Wallet {
type: NotificationType.InternalError,
message: "uncaught exception",
exception: e,
- });
+ });
}
}
this.ws.notify({
type: NotificationType.PendingOperationProcessed,
- });
+ });
}
}
}
@@ -712,12 +708,6 @@ export class Wallet {
return this.db.get(Stores.purchases, contractTermsHash);
}
- async getFullRefundFees(
- refundPermissions: MerchantRefundDetails[],
- ): Promise<AmountJson> {
- return getFullRefundFees(this.ws, refundPermissions);
- }
-
async acceptTip(talerTipUri: string): Promise<void> {
try {
return acceptTip(this.ws, talerTipUri);
diff --git a/src/webex/pages/withdraw.tsx b/src/webex/pages/withdraw.tsx
index c4e4ebbb..0216cdb4 100644
--- a/src/webex/pages/withdraw.tsx
+++ b/src/webex/pages/withdraw.tsx
@@ -35,7 +35,9 @@ import {
} from "../wxApi";
function WithdrawalDialog(props: { talerWithdrawUri: string }): JSX.Element {
- const [details, setDetails] = useState<WithdrawalDetailsResponse | undefined>();
+ const [details, setDetails] = useState<
+ WithdrawalDetailsResponse | undefined
+ >();
const [selectedExchange, setSelectedExchange] = useState<
string | undefined
>();
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index 12675616..d5a27216 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -29,10 +29,7 @@ import {
openTalerDatabase,
WALLET_DB_MINOR_VERSION,
} from "../db";
-import {
- ReturnCoinsRequest,
- WalletDiagnostics,
-} from "../types/walletTypes";
+import { ReturnCoinsRequest, WalletDiagnostics } from "../types/walletTypes";
import { BrowserHttpLib } from "../util/http";
import { OpenedPromise, openPromise } from "../util/promiseUtils";
import { classifyTalerUri, TalerUriType } from "../util/taleruri";