From e60563fb540c04d9ba751fea69c1fc0f1de598b5 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 22 Jul 2020 14:22:03 +0530 Subject: consistent error handling for HTTP request (and some other things) --- src/android/index.ts | 17 ++- src/headless/NodeHttpLib.ts | 36 +++++- src/headless/helpers.ts | 5 +- src/headless/integrationtest.ts | 7 +- src/headless/merchant.ts | 8 +- src/headless/taler-wallet-cli.ts | 7 +- src/operations/errors.ts | 61 ++++++--- src/operations/exchanges.ts | 118 +++++------------ src/operations/pay.ts | 57 +++++---- src/operations/recoup.ts | 36 +++--- src/operations/refresh.ts | 76 ++++------- src/operations/refund.ts | 97 ++++---------- src/operations/reserves.ts | 82 ++++++------ src/operations/tip.ts | 18 ++- src/operations/transactions.ts | 87 +++++++------ src/operations/withdraw.ts | 30 ++--- src/types/dbTypes.ts | 29 ++--- src/types/notifications.ts | 23 +++- src/types/pending.ts | 16 +-- src/types/talerTypes.ts | 68 +++++++++- src/types/transactions.ts | 2 +- src/types/walletTypes.ts | 9 +- src/util/amounts-test.ts | 4 +- src/util/amounts.ts | 2 +- src/util/http.ts | 267 +++++++++++++++++++++++---------------- src/wallet.ts | 18 +-- src/webex/pages/withdraw.tsx | 4 +- src/webex/wxBackend.ts | 5 +- 28 files changed, 636 insertions(+), 553 deletions(-) diff --git a/src/android/index.ts b/src/android/index.ts index 63d88d70b..d7a5897a1 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 118fb9e96..d109c3b7c 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 => { 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 67ba62df8..e451db55b 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 { 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 786907a09..db96d57c4 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 3324924fd..34ca5564d 100644 --- a/src/headless/merchant.ts +++ b/src/headless/merchant.ts @@ -37,7 +37,10 @@ export class MerchantBackendConnection { reason: string, refundAmount: string, ): Promise { - 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 { - 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 fc13d77f3..b8ae84d72 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 01a8283cb..198d3f8c5 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, + ): 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, +): 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( op: () => Promise, - onOpError: (e: OperationError) => Promise, + onOpError: (e: OperationErrorDetails) => Promise, ): Promise { try { return await op(); @@ -71,21 +93,28 @@ export async function guardOperationException( 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 6f5ff1d30..ff2cd3da6 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 { 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 { - const onOpErr = (e: OperationError): Promise => + const onOpErr = (e: OperationErrorDetails): Promise => setExchangeError(ws, baseUrl, e); return await guardOperationException( () => updateExchangeFromUrlImpl(ws, baseUrl, forceNow), diff --git a/src/operations/pay.ts b/src/operations/pay.ts index 74bfcc70b..29b697833 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 { 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 { 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 { - const onOpErr = (err: OperationError): Promise => + const onOpErr = (err: OperationErrorDetails): Promise => 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 { - const onOpErr = (e: OperationError): Promise => + const onOpErr = (e: OperationErrorDetails): Promise => incrementPurchasePayRetry(ws, proposalId, e); await guardOperationException( () => processPurchasePayImpl(ws, proposalId, forceNow), diff --git a/src/operations/recoup.ts b/src/operations/recoup.ts index d1b3c3bda..445d029cd 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 { 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 { await ws.memoProcessRecoup.memo(recoupGroupId, async () => { - const onOpErr = (e: OperationError): Promise => + const onOpErr = (e: OperationErrorDetails): Promise => 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 2d7ffad22..4d477d644 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 { 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 { await ws.memoProcessRefresh.memo(refreshGroupId, async () => { - const onOpErr = (e: OperationError): Promise => + const onOpErr = (e: OperationErrorDetails): Promise => 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 5f6ccf9d4..1d6561bdc 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 { - 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 { - 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 { - const onOpErr = (e: OperationError): Promise => + const onOpErr = (e: OperationErrorDetails): Promise => 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 365d6e221..e6b09316e 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 { return ws.memoProcessReserve.memo(reservePub, async () => { - const onOpError = (err: OperationError): Promise => + const onOpError = (err: OperationErrorDetails): Promise => incrementReserveRetry(ws, reservePub, err); await guardOperationException( () => processReserveImpl(ws, reservePub, forceNow), @@ -344,7 +351,7 @@ export async function processReserveBankStatus( ws: InternalWalletState, reservePub: string, ): Promise { - const onOpError = (err: OperationError): Promise => + const onOpError = (err: OperationErrorDetails): Promise => 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 { 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 1ae7700a5..d121b1cbb 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 { 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 { - const onOpErr = (e: OperationError): Promise => + const onOpErr = (e: OperationErrorDetails): Promise => incrementTipRetry(ws, tipId, e); await guardOperationException( () => processTipImpl(ws, tipId, forceNow), diff --git a/src/operations/transactions.ts b/src/operations/transactions.ts index 85cd87167..f104f1078 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 98969d213..f7879dfec 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 { 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 { - const onOpErr = (e: OperationError): Promise => + const onOpErr = (e: OperationErrorDetails): Promise => incrementWithdrawalRetry(ws, withdrawalGroupId, e); await guardOperationException( () => processWithdrawGroupImpl(ws, withdrawalGroupId, forceNow), diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts index b085f83db..252649b07 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 ac30b6fe2..5d6d2ee11 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 f949b7c16..8a1e84362 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 232f5f314..ef14684f9 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 => makeCodecForObject() .property("sig", codecForString) .build("MerchantPayResponse"); + +export const codecForExchangeMeltResponse = (): Codec => + makeCodecForObject() + .property("exchange_pub", codecForString) + .property("exchange_sig", codecForString) + .property("noreveal_index", codecForNumber) + .property("refresh_base_url", makeCodecOptional(codecForString)) + .build("ExchangeMeltResponse"); + +export const codecForExchangeRevealItem = (): Codec => + makeCodecForObject() + .property("ev_sig", codecForString) + .build("ExchangeRevealItem"); + +export const codecForExchangeRevealResponse = (): Codec< + ExchangeRevealResponse +> => + makeCodecForObject() + .property("ev_sigs", makeCodecForList(codecForExchangeRevealItem())) + .build("ExchangeRevealResponse"); diff --git a/src/types/transactions.ts b/src/types/transactions.ts index b87726bad..d62622648 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 ee7d071c6..95ec47b67 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 e10ee5962..afd8caa51 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 94aefb3cd..1e7f192f4 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 bc054096a..abbc8df03 100644 --- a/src/util/http.ts +++ b/src/util/http.ts @@ -14,18 +14,26 @@ TALER; see the file COPYING. If not, see */ -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; @@ -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; + + /** + * 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 => { @@ -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 { return this.req("post", url, JSON.stringify(body), opt); @@ -176,114 +220,121 @@ export class BrowserHttpLib implements HttpRequestLibrary { } } -export interface PostJsonRequest { - http: HttpRequestLibrary; - url: string; - body: any; - codec: Codec; -} +type TalerErrorResponse = { + code: number; +} & unknown; -/** - * Helper for making Taler-style HTTP POST requests with a JSON payload and response. - */ -export async function httpPostTalerJson( - req: PostJsonRequest, -): Promise { - const resp = await req.http.postJson(req.url, req.body); +type ResponseOrError = + | { 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( + httpResponse: HttpResponse, + codec: Codec, +): Promise> { + 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 { - http: HttpRequestLibrary; - url: string; - codec: Codec; +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( - req: GetJsonRequest, -): Promise { - const resp = await req.http.get(req.url); +export async function readSuccessResponseJsonOrThrow( + httpResponse: HttpResponse, + codec: Codec, +): Promise { + 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( + httpResponse: HttpResponse, +): Promise> { + 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( + httpResponse: HttpResponse, +): Promise { + 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 9df279897..ff72f3c75 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 { - return getFullRefundFees(this.ws, refundPermissions); - } - async acceptTip(talerTipUri: string): Promise { try { return acceptTip(this.ws, talerTipUri); diff --git a/src/webex/pages/withdraw.tsx b/src/webex/pages/withdraw.tsx index c4e4ebbb9..0216cdb4f 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(); + 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 126756165..d5a272160 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"; -- cgit v1.2.3