diff options
author | Florian Dold <florian@dold.me> | 2023-02-15 23:32:42 +0100 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2023-02-16 02:50:29 +0100 |
commit | 825d2c4352022e7397854b2bd9ba7d3589873c07 (patch) | |
tree | d23530bf8408367439e6b3820ea0c4269bfeb39a /packages/taler-util/src/errors.ts | |
parent | cb2f4c21d85707abb0221cbf2a859a98836b2d44 (diff) | |
download | wallet-core-825d2c4352022e7397854b2bd9ba7d3589873c07.tar.xz |
make wallet-cli runnable under qtart
Diffstat (limited to 'packages/taler-util/src/errors.ts')
-rw-r--r-- | packages/taler-util/src/errors.ts | 248 |
1 files changed, 248 insertions, 0 deletions
diff --git a/packages/taler-util/src/errors.ts b/packages/taler-util/src/errors.ts new file mode 100644 index 000000000..038bdbc7c --- /dev/null +++ b/packages/taler-util/src/errors.ts @@ -0,0 +1,248 @@ +/* + 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 <http://www.gnu.org/licenses/> + */ + +/** + * Classes and helpers for error handling specific to wallet operations. + * + * @author Florian Dold <dold@taler.net> + */ + +/** + * Imports. + */ +import { + AbsoluteTime, + PayMerchantInsufficientBalanceDetails, + PayPeerInsufficientBalanceDetails, + TalerErrorCode, + TalerErrorDetail, + TransactionType, +} from "@gnu-taler/taler-util"; + +type empty = Record<string, never>; + +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_TIPPING_COIN_SIGNATURE_INVALID]: empty; + [TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED]: { + orderId: string; + claimUrl: 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]: { + exchangeProtocolVersion: string; + walletProtocolVersion: string; + }; + [TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN]: { + operation: string; + }; + [TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED]: { + requestUrl: string; + requestMethod: string; + throttleStats: Record<string, unknown>; + }; + [TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT]: empty; + [TalerErrorCode.WALLET_NETWORK_ERROR]: { + requestUrl: string; + requestMethod: string; + }; + [TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE]: { + requestUrl: string; + requestMethod: string; + httpStatusCode: number; + validationError?: string; + }; + [TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID]: empty; + [TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE]: { + errorsPerCoin: Record<number, TalerErrorDetail>; + }; + [TalerErrorCode.WALLET_CORE_NOT_AVAILABLE]: empty; + [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: PayMerchantInsufficientBalanceDetails; + }; + [TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE]: { + insufficientBalanceDetails: PayPeerInsufficientBalanceDetails; + }; +} + +type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : empty; + +export function makeErrorDetail<C extends TalerErrorCode>( + code: C, + detail: ErrBody<C>, + hint?: string, +): TalerErrorDetail { + if (!hint && !(detail as any).hint) { + hint = getDefaultHint(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] ?? "<unknown>"; + return `Error (${ed.code}/${errName})`; +} + +function getDefaultHint(code: number): string { + const errName = TalerErrorCode[code]; + if (errName) { + return `Error (${errName})`; + } else { + return `Error (<unknown>)`; + } +} + +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); + } +} + +export class TalerError<T = any> extends Error { + errorDetail: TalerErrorDetail & T; + private constructor(d: TalerErrorDetail & T) { + super(d.hint ?? `Error (code ${d.code})`); + this.errorDetail = d; + Object.setPrototypeOf(this, TalerError.prototype); + } + + static fromDetail<C extends TalerErrorCode>( + code: C, + detail: ErrBody<C>, + hint?: string, + ): TalerError { + if (!hint) { + hint = getDefaultHint(code); + } + const when = AbsoluteTime.now(); + return new TalerError<unknown>({ code, when, hint, ...detail }); + } + + static fromUncheckedDetail(d: TalerErrorDetail): TalerError { + return new TalerError<unknown>({ ...d }); + } + + static fromException(e: any): TalerError { + const errDetail = getErrorDetailFromException(e); + return new TalerError(errDetail); + } + + hasErrorCode<C extends keyof DetailsMap>( + code: C, + ): this is TalerError<DetailsMap[C]> { + return this.errorDetail.code === code; + } +} + +/** + * 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 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; +} |