summaryrefslogtreecommitdiff
path: root/packages/taler-util/src/errors.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-util/src/errors.ts')
-rw-r--r--packages/taler-util/src/errors.ts248
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;
+}