ethers.js/src.ts/utils/errors.ts
2022-12-02 21:23:13 -05:00

598 lines
17 KiB
TypeScript

/**
* About Errors.
*
* @_section: api/utils/errors:Errors [about-errors]
*/
import { version } from "../_version.js";
import { defineProperties } from "./properties.js";
import type {
TransactionRequest, TransactionReceipt, TransactionResponse
} from "../providers/index.js";
import type { FetchRequest, FetchResponse } from "./fetch.js";
export type ErrorInfo<T> = Omit<T, "code" | "name" | "message">;
function stringify(value: any): any {
if (value == null) { return "null"; }
if (Array.isArray(value)) {
return "[ " + (value.map(stringify)).join(", ") + " ]";
}
if (value instanceof Uint8Array) {
const HEX = "0123456789abcdef";
let result = "0x";
for (let i = 0; i < value.length; i++) {
result += HEX[value[i] >> 4];
result += HEX[value[i] & 0xf];
}
return result;
}
if (typeof(value) === "object" && typeof(value.toJSON) === "function") {
return stringify(value.toJSON());
}
switch (typeof(value)) {
case "boolean": case "symbol":
return value.toString();
case "bigint":
return BigInt(value).toString();
case "number":
return (value).toString();
case "string":
return JSON.stringify(value);
case "object": {
const keys = Object.keys(value);
keys.sort();
return "{ " + keys.map((k) => `${ stringify(k) }: ${ stringify(value[k]) }`).join(", ") + " }";
}
}
return `[ COULD NOT SERIALIZE ]`;
}
/**
* All errors emitted by ethers have an **ErrorCode** to help
* identify and coalesce errors to simplfy programatic analysis.
*
* _property: ``"UNKNOWN_ERROR"``
* This is a general puspose fallback when no other error makes sense
* or the error wasn't expected
*
* _property: ``"NOT_IMPLEMENTED"``
*/
export type ErrorCode =
// Generic Errors
"UNKNOWN_ERROR" | "NOT_IMPLEMENTED" | "UNSUPPORTED_OPERATION" |
"NETWORK_ERROR" | "SERVER_ERROR" | "TIMEOUT" | "BAD_DATA" |
"CANCELLED" |
// Operational Errors
"BUFFER_OVERRUN" | "NUMERIC_FAULT" |
// Argument Errors
"INVALID_ARGUMENT" | "MISSING_ARGUMENT" | "UNEXPECTED_ARGUMENT" |
"VALUE_MISMATCH" |
// Blockchain Errors
"CALL_EXCEPTION" | "INSUFFICIENT_FUNDS" | "NONCE_EXPIRED" |
"REPLACEMENT_UNDERPRICED" | "TRANSACTION_REPLACED" |
"UNCONFIGURED_NAME" | "OFFCHAIN_FAULT" |
// User Interaction
"ACTION_REJECTED"
;
export interface EthersError<T extends ErrorCode = ErrorCode> extends Error {
code: ErrorCode;
// recover?: (...args: Array<any>) => any;
info?: Record<string, any>;
error?: Error;
}
// Generic Errors
/**
* This Error is a catch-all for when there is no way for Ethers to
* know what the underlying problem is.
*/
export interface UnknownError extends EthersError<"UNKNOWN_ERROR"> {
[ key: string ]: any;
}
/**
* This Error is mostly used as a stub for functionality that is
* intended for the future, but is currently not implemented.
*/
export interface NotImplementedError extends EthersError<"NOT_IMPLEMENTED"> {
/**
* The attempted operation.
*/
operation: string;
}
/**
* This Error indicates that the attempted operation is not supported.
*
* This could range from a specifc JSON-RPC end-point not supporting
* a feature to a specific configuration of an object prohibiting the
* operation.
*
* For example, a [[Wallet]] with no connected [[Provider]] is unable
* to send a transaction.
*/
export interface UnsupportedOperationError extends EthersError<"UNSUPPORTED_OPERATION"> {
/**
* The attempted operation.
*/
operation: string;
}
export interface NetworkError extends EthersError<"NETWORK_ERROR"> {
event: string;
}
/**
* This Error indicates there was a problem fetching a resource from
* a server.
*/
export interface ServerError extends EthersError<"SERVER_ERROR"> {
/**
* The requested resource.
*/
request: FetchRequest | string;
/**
* The response received from the server, if available.
*/
response?: FetchResponse;
}
/**
* This Error indicates that the timeout duration has expired and
* that the operation has been implicitly cancelled.
*
* The side-effect of the operation may still occur, as this
* generally means a request has been sent and there has simply
* been no response to indicate whether it was processed or not.
*/
export interface TimeoutError extends EthersError<"TIMEOUT"> {
/**
* The attempted operation.
*/
operation: string;
/**
* The reason.
*/
reason: string;
/**
* The resource request, if available.
*/
request?: FetchRequest;
}
/**
* This Error indicates that a provided set of data cannot
* be correctly interpretted.
*/
export interface BadDataError extends EthersError<"BAD_DATA"> {
/**
* The data.
*/
value: any;
}
/**
* This Error indicates that the operation was cancelled by a
* programmatic call, for example to ``cancel()``.
*/
export interface CancelledError extends EthersError<"CANCELLED"> {
}
// Operational Errors
/**
* This Error indicates an attempt was made to read outside the bounds
* of protected data.
*
* Most operations in Ethers are protected by bounds checks, to mitigate
* exploits when parsing data.
*/
export interface BufferOverrunError extends EthersError<"BUFFER_OVERRUN"> {
/**
* The buffer that was overrun.
*/
buffer: Uint8Array;
/**
* The length of the buffer.
*/
length: number;
/**
* The offset that was requested.
*/
offset: number;
}
/**
* This Error indicates an operation which would result in incorrect
* arithmetic output has occurred.
*
* For example, trying to divide by zero or using a ``uint8`` to store
* a negative value.
*/
export interface NumericFaultError extends EthersError<"NUMERIC_FAULT"> {
/**
* The attempted operation.
*/
operation: string;
/**
* The fault reported.
*/
fault: string;
/**
* The value the operation was attempted against.
*/
value: any;
}
// Argument Errors
/**
* This Error indicates an incorrect type or value was passed to
* a function or method.
*/
export interface InvalidArgumentError extends EthersError<"INVALID_ARGUMENT"> {
/**
* The name of the argument.
*/
argument: string;
/**
* The value that was provided.
*/
value: any;
info?: Record<string, any>
}
export interface MissingArgumentError extends EthersError<"MISSING_ARGUMENT"> {
count: number;
expectedCount: number;
}
export interface UnexpectedArgumentError extends EthersError<"UNEXPECTED_ARGUMENT"> {
count: number;
expectedCount: number;
}
// Blockchain Errors
export type CallExceptionAction = "call" | "estimateGas" | "getTransactionResult" | "unknown";
export type CallExceptionTransaction = {
to: null | string;
from?: string;
data: string;
};
export interface CallExceptionError extends EthersError<"CALL_EXCEPTION"> {
// What was being performed when the call exception occurred
action: CallExceptionAction;
// The revert data
data: null | string;
// If possible, a human-readable representation of data
reason: null | string;
// The transaction that triggered the exception
transaction: CallExceptionTransaction,
// If avaiable, the contract invocation details
invocation: null | {
method: string;
signature: string;
args: Array<any>;
}
// The built-in or custom revert error if available
revert: null | {
signature: string;
name: string;
args: Array<any>;
}
}
//export interface ContractCallExceptionError extends CallExceptionError {
// The transaction call
// transaction: any;//ErrorTransaction;
//}
export interface InsufficientFundsError extends EthersError<"INSUFFICIENT_FUNDS"> {
transaction: TransactionRequest;
}
export interface NonceExpiredError extends EthersError<"NONCE_EXPIRED"> {
transaction: TransactionRequest;
}
export interface OffchainFaultError extends EthersError<"OFFCHAIN_FAULT"> {
transaction?: TransactionRequest;
reason: string;
}
export interface ReplacementUnderpricedError extends EthersError<"REPLACEMENT_UNDERPRICED"> {
transaction: TransactionRequest;
}
export interface TransactionReplacedError extends EthersError<"TRANSACTION_REPLACED"> {
cancelled: boolean;
reason: "repriced" | "cancelled" | "replaced";
hash: string;
replacement: TransactionResponse;
receipt: TransactionReceipt;
}
/**
* This Error indicates an ENS name was used, but the name has not
* been configured.
*
* This could indicate an ENS name is unowned or that the current
* address being pointed to is the [[Zero]].
*/
export interface UnconfiguredNameError extends EthersError<"UNCONFIGURED_NAME"> {
/**
* The ENS name that was requested
*/
value: string;
}
/**
* This Error indicates a request was rejected by the user.
*
* In most clients (such as MetaMask), when an operation requires user
* authorization (such as ``signer.sendTransaction``), the client
* presents a dialog box to the user. If the user denies the request
* this error is thrown.
*/
export interface ActionRejectedError extends EthersError<"ACTION_REJECTED"> {
/**
* The requested action.
*/
action: "requestAccess" | "sendTransaction" | "signMessage" | "signTransaction" | "signTypedData" | "unknown",
/**
* The reason the action was rejected.
*
* If there is already a pending request, some clients may indicate
* there is already a ``"pending"`` action. This prevents an app
* from spamming the user.
*/
reason: "expired" | "rejected" | "pending"
}
// Coding; converts an ErrorCode its Typed Error
/**
* A conditional type that transforms the [[ErrorCode]] T into
* its EthersError type.
*
* @flatworm-skip-docs
*/
export type CodedEthersError<T> =
T extends "UNKNOWN_ERROR" ? UnknownError:
T extends "NOT_IMPLEMENTED" ? NotImplementedError:
T extends "UNSUPPORTED_OPERATION" ? UnsupportedOperationError:
T extends "NETWORK_ERROR" ? NetworkError:
T extends "SERVER_ERROR" ? ServerError:
T extends "TIMEOUT" ? TimeoutError:
T extends "BAD_DATA" ? BadDataError:
T extends "CANCELLED" ? CancelledError:
T extends "BUFFER_OVERRUN" ? BufferOverrunError:
T extends "NUMERIC_FAULT" ? NumericFaultError:
T extends "INVALID_ARGUMENT" ? InvalidArgumentError:
T extends "MISSING_ARGUMENT" ? MissingArgumentError:
T extends "UNEXPECTED_ARGUMENT" ? UnexpectedArgumentError:
T extends "CALL_EXCEPTION" ? CallExceptionError:
T extends "INSUFFICIENT_FUNDS" ? InsufficientFundsError:
T extends "NONCE_EXPIRED" ? NonceExpiredError:
T extends "OFFCHAIN_FAULT" ? OffchainFaultError:
T extends "REPLACEMENT_UNDERPRICED" ? ReplacementUnderpricedError:
T extends "TRANSACTION_REPLACED" ? TransactionReplacedError:
T extends "UNCONFIGURED_NAME" ? UnconfiguredNameError:
T extends "ACTION_REJECTED" ? ActionRejectedError:
never;
/**
* Returns true if the %%error%% matches an error thrown by ethers
* that matches the error %%code%%.
*
* In TypeScript envornoments, this can be used to check that %%error%%
* matches an EthersError type, which means the expected properties will
* be set.
*
* @See [ErrorCodes](api:ErrorCode)
* @example
* try {
* / / code....
* } catch (e) {
* if (isError(e, "CALL_EXCEPTION")) {
* console.log(e.data);
* }
* }
*/
export function isError<K extends ErrorCode, T extends CodedEthersError<K>>(error: any, code: K): error is T {
return (error && (<EthersError>error).code === code);
}
/**
* Returns true if %%error%% is a [CALL_EXCEPTION](api:CallExceptionError).
*/
export function isCallException(error: any): error is CallExceptionError {
return isError(error, "CALL_EXCEPTION");
}
/**
* Returns a new Error configured to the format ethers emits errors, with
* the %%message%%, [[api:ErrorCode]] %%code%% and additioanl properties
* for the corresponding EthersError.
*
* Each error in ethers includes the version of ethers, a
* machine-readable [[ErrorCode]], and depneding on %%code%%, additional
* required properties. The error message will also include the %%meeage%%,
* ethers version, %%code%% and all aditional properties, serialized.
*/
export function makeError<K extends ErrorCode, T extends CodedEthersError<K>>(message: string, code: K, info?: ErrorInfo<T>): T {
{
const details: Array<string> = [];
if (info) {
if ("message" in info || "code" in info || "name" in info) {
throw new Error(`value will overwrite populated values: ${ stringify(info) }`);
}
for (const key in info) {
const value = <any>(info[<keyof ErrorInfo<T>>key]);
// try {
details.push(key + "=" + stringify(value));
// } catch (error: any) {
// console.log("MMM", error.message);
// details.push(key + "=[could not serialize object]");
// }
}
}
details.push(`code=${ code }`);
details.push(`version=${ version }`);
if (details.length) {
message += " (" + details.join(", ") + ")";
}
}
let error;
switch (code) {
case "INVALID_ARGUMENT":
error = new TypeError(message);
break;
case "NUMERIC_FAULT":
case "BUFFER_OVERRUN":
error = new RangeError(message);
break;
default:
error = new Error(message);
}
defineProperties<EthersError>(<EthersError>error, { code });
if (info) { defineProperties<any>(error, info); }
return <T>error;
}
/**
* Throws an EthersError with %%message%%, %%code%% and additional error
* %%info%% when %%check%% is falsish..
*
* @see [[api:makeError]]
*/
export function assert<K extends ErrorCode, T extends CodedEthersError<K>>(check: unknown, message: string, code: K, info?: ErrorInfo<T>): asserts check {
if (!check) { throw makeError(message, code, info); }
}
/**
* A simple helper to simply ensuring provided arguments match expected
* constraints, throwing if not.
*
* In TypeScript environments, the %%check%% has been asserted true, so
* any further code does not need additional compile-time checks.
*/
export function assertArgument(check: unknown, message: string, name: string, value: unknown): asserts check {
assert(check, message, "INVALID_ARGUMENT", { argument: name, value: value });
}
export function assertArgumentCount(count: number, expectedCount: number, message?: string): void {
if (message == null) { message = ""; }
if (message) { message = ": " + message; }
assert(count >= expectedCount, "missing arguemnt" + message, "MISSING_ARGUMENT", {
count: count,
expectedCount: expectedCount
});
assert(count <= expectedCount, "too many arguemnts" + message, "UNEXPECTED_ARGUMENT", {
count: count,
expectedCount: expectedCount
});
}
const _normalizeForms = ["NFD", "NFC", "NFKD", "NFKC"].reduce((accum, form) => {
try {
// General test for normalize
/* c8 ignore start */
if ("test".normalize(form) !== "test") { throw new Error("bad"); };
/* c8 ignore stop */
if (form === "NFD") {
const check = String.fromCharCode(0xe9).normalize("NFD");
const expected = String.fromCharCode(0x65, 0x0301)
/* c8 ignore start */
if (check !== expected) { throw new Error("broken") }
/* c8 ignore stop */
}
accum.push(form);
} catch(error) { }
return accum;
}, <Array<string>>[]);
/**
* Throws if the normalization %%form%% is not supported.
*/
export function assertNormalize(form: string): void {
assert(_normalizeForms.indexOf(form) >= 0, "platform missing String.prototype.normalize", "UNSUPPORTED_OPERATION", {
operation: "String.prototype.normalize", info: { form }
});
}
/**
* Many classes use file-scoped values to guard the constructor,
* making it effectively private. This facilitates that pattern
* by ensuring the %%givenGaurd%% matches the file-scoped %%guard%%,
* throwing if not, indicating the %%className%% if provided.
*/
export function assertPrivate(givenGuard: any, guard: any, className?: string): void {
if (className == null) { className = ""; }
if (givenGuard !== guard) {
let method = className, operation = "new";
if (className) {
method += ".";
operation += " " + className;
}
assert(false, `private constructor; use ${ method }from* methods`, "UNSUPPORTED_OPERATION", {
operation
});
}
}