wallet-core/packages/taler-util/src/errors.ts
2023-09-25 14:50:44 -03:00

265 lines
7.4 KiB
TypeScript

/*
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_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<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]: {
numErrors: number;
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;
};
[TalerErrorCode.WALLET_REFRESH_GROUP_INCOMPLETE]: {
numErrors: number;
/**
* Errors, can be truncated.
*/
errors: TalerErrorDetail[];
};
[TalerErrorCode.WALLET_EXCHANGE_BASE_URL_MISMATCH]: {
urlWallet: string;
urlExchange: string;
};
}
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;
}