/* This file is part of GNU Taler (C) 2019-2020 Taler Systems SA GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see */ /** * Classes and helpers for error handling specific to wallet operations. * * @author Florian Dold */ /** * Imports. */ import { AbsoluteTime, CancellationToken, PaymentInsufficientBalanceDetails, TalerErrorCode, TalerErrorDetail, TransactionType, } from "@gnu-taler/taler-util"; type empty = Record; export interface DetailsMap { [TalerErrorCode.WALLET_PENDING_OPERATION_FAILED]: { innerError: TalerErrorDetail; transactionId?: string; }; [TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT]: { exchangeBaseUrl: string; }; [TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE]: { exchangeProtocolVersion: string; walletProtocolVersion: string; }; [TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK]: empty; [TalerErrorCode.WALLET_REWARD_COIN_SIGNATURE_INVALID]: empty; [TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED]: { orderId: string; claimUrl: string; }; [TalerErrorCode.WALLET_ORDER_ALREADY_PAID]: { orderId: string; fulfillmentUrl: string; }; [TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED]: empty; [TalerErrorCode.WALLET_CONTRACT_TERMS_SIGNATURE_INVALID]: { merchantPub: string; orderId: string; }; [TalerErrorCode.WALLET_CONTRACT_TERMS_BASE_URL_MISMATCH]: { baseUrlForDownload: string; baseUrlFromContractTerms: string; }; [TalerErrorCode.WALLET_INVALID_TALER_PAY_URI]: { talerPayUri: string; }; [TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR]: { requestUrl: string; requestMethod: string; httpStatusCode: number; errorResponse?: any; }; [TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION]: { stack?: string; }; [TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE]: { bankProtocolVersion: string; walletProtocolVersion: string; }; [TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN]: { operation: string; }; [TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED]: { requestUrl: string; requestMethod: string; throttleStats: Record; }; [TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT]: { requestUrl: string; requestMethod: string; timeoutMs: number; }; [TalerErrorCode.GENERIC_TIMEOUT]: { requestUrl: string; requestMethod: string; timeoutMs: number; }; [TalerErrorCode.WALLET_NETWORK_ERROR]: { requestUrl: string; requestMethod: string; }; [TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE]: { requestUrl: string; requestMethod: string; httpStatusCode: number; /** * Original response which is malformed */ response?: string; validationError?: string; /** * Content type of the response, usually only specified if not the * expected content type. */ contentType?: string; }; [TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR]: { operation: string; error: string; detail: TalerErrorDetail | undefined; }; [TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID]: empty; [TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE]: { numErrors: number; errorsPerCoin: Record; }; [TalerErrorCode.WALLET_CORE_NOT_AVAILABLE]: { lastError?: TalerErrorDetail; }; [TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR]: { httpStatusCode: number; }; [TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR]: { requestError: TalerErrorDetail; }; [TalerErrorCode.WALLET_CRYPTO_WORKER_ERROR]: { innerError: TalerErrorDetail; }; [TalerErrorCode.WALLET_CRYPTO_WORKER_BAD_REQUEST]: { detail: string; }; [TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED]: { kycUrl: string; }; [TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE]: { insufficientBalanceDetails: PaymentInsufficientBalanceDetails; }; [TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE]: { insufficientBalanceDetails: PaymentInsufficientBalanceDetails; }; [TalerErrorCode.WALLET_REFRESH_GROUP_INCOMPLETE]: { numErrors: number; /** * Errors, can be truncated. */ errors: TalerErrorDetail[]; }; [TalerErrorCode.WALLET_EXCHANGE_BASE_URL_MISMATCH]: { urlWallet: string; urlExchange: string; }; [TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE]: { exchangeBaseUrl: string; innerError: TalerErrorDetail | undefined; }; [TalerErrorCode.WALLET_DB_UNAVAILABLE]: { innerError: TalerErrorDetail | undefined; }; [TalerErrorCode.WALLET_EXCHANGE_TOS_NOT_ACCEPTED]: { exchangeBaseUrl: string; tosStatus: string; currentEtag: string | undefined; }; [TalerErrorCode.WALLET_EXCHANGE_ENTRY_UPDATE_CONFLICT]: { detail?: string; }; } type ErrBody = Y extends keyof DetailsMap ? DetailsMap[Y] : empty; export function makeErrorDetail( code: C, detail: ErrBody, hint?: string, ): TalerErrorDetail { if (!hint && !(detail as any).hint) { hint = getDefaultTalerErrorHint(code); } const when = AbsoluteTime.now(); return { code, when, hint, ...detail }; } export function makePendingOperationFailedError( innerError: TalerErrorDetail, tag: TransactionType, uid: string, ): TalerError { return TalerError.fromDetail(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED, { innerError, transactionId: `${tag}:${uid}`, }); } export function summarizeTalerErrorDetail(ed: TalerErrorDetail): string { const errName = TalerErrorCode[ed.code] ?? ""; return `Error (${ed.code}/${errName})`; } export function getDefaultTalerErrorHint(code: number): string { const errName = TalerErrorCode[code]; if (errName) { return `Error (${errName})`; } else { return `Error ()`; } } export class TalerProtocolViolationError extends Error { constructor(hint?: string) { let msg: string; if (hint) { msg = `Taler protocol violation error (${hint})`; } else { msg = `Taler protocol violation error`; } super(msg); Object.setPrototypeOf(this, TalerProtocolViolationError.prototype); } } // compute a subset of TalerError, just for http request type HttpErrors = | TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT | TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED | TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE | TalerErrorCode.WALLET_NETWORK_ERROR | TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR; type TalerHttpErrorsDetails = { [code in HttpErrors]: TalerError; }; export type TalerHttpError = TalerHttpErrorsDetails[keyof TalerHttpErrorsDetails]; /** * Construct typed error details. * Fills in the hint with a default based on the error code name. */ export function makeTalerErrorDetail( code: C, errBody: ErrBody, hint?: string, ): TalerErrorDetail { if (!hint) { hint = getDefaultTalerErrorHint(code); } return { code, hint, ...errBody }; } export class TalerError extends Error { errorDetail: TalerErrorDetail & T; cause: Error | undefined; private constructor(d: TalerErrorDetail & T, cause?: Error) { super(d.hint ?? `Error (code ${d.code})`); this.errorDetail = d; this.cause = cause; Object.setPrototypeOf(this, TalerError.prototype); } static fromDetail( code: C, detail: ErrBody, hint?: string, cause?: Error, ): TalerError { if (!hint) { hint = getDefaultTalerErrorHint(code); } const when = AbsoluteTime.now(); return new TalerError({ code, when, hint, ...detail }, cause); } static fromUncheckedDetail(d: TalerErrorDetail, c?: Error): TalerError { return new TalerError({ ...d }, c); } static fromException(e: any): TalerError { const errDetail = getErrorDetailFromException(e); return new TalerError(errDetail, e); } hasErrorCode( code: C, ): this is TalerError { return this.errorDetail.code === code; } toString(): string { return `TalerError: ${JSON.stringify(this.errorDetail)}`; } } export function safeStringifyException(e: any): string { return JSON.stringify(getErrorDetailFromException(e), undefined, 2); } /** * Convert an exception (or anything that was thrown) into * a TalerErrorDetail object. */ export function getErrorDetailFromException(e: any): TalerErrorDetail { if (e instanceof TalerError) { return e.errorDetail; } if (e instanceof CancellationToken.CancellationError) { const err = makeErrorDetail( TalerErrorCode.WALLET_CORE_REQUEST_CANCELLED, {}, ); return err; } if (e instanceof Error) { const err = makeErrorDetail( TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION, { stack: e.stack, }, `unexpected exception (message: ${e.message})`, ); return err; } // 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 err = makeErrorDetail( TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION, {}, `unexpected exception (not an exception, ${excString})`, ); return err; } export function assertUnreachable(x: never): never { throw new Error("Didn't expect to get here"); }