/** * 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 = Omit; 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 extends Error { code: ErrorCode; // recover?: (...args: Array) => any; info?: Record; 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 } 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; } // The built-in or custom revert error if available revert: null | { signature: string; name: string; args: Array; } } //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 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>(error: any, code: K): error is T { return (error && (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>(message: string, code: K, info?: ErrorInfo): T { { const details: Array = []; 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 = (info[>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(error, { code }); if (info) { defineProperties(error, info); } return error; } /** * Throws an EthersError with %%message%%, %%code%% and additional error * %%info%% when %%check%% is falsish.. * * @see [[api:makeError]] */ export function assert>(check: unknown, message: string, code: K, info?: ErrorInfo): 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; }, >[]); /** * 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 }); } }