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/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 +++++------ 10 files changed, 289 insertions(+), 373 deletions(-) (limited to 'src/operations') 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), -- cgit v1.2.3