UNPKG

ox

Version:

Ethereum Standard Library

795 lines (761 loc) 20.5 kB
import * as abitype from 'abitype' import type * as Abi from './Abi.js' import * as AbiItem from './AbiItem.js' import * as AbiParameters from './AbiParameters.js' import type * as Errors from './Errors.js' import * as Hex from './Hex.js' import type * as internal from './internal/abiError.js' import type * as AbiItem_internal from './internal/abiItem.js' import type { IsNarrowable, IsNever } from './internal/types.js' /** Root type for an {@link ox#AbiItem.AbiItem} with an `error` type. */ export type AbiError = abitype.AbiError & { hash?: Hex.Hex | undefined overloads?: readonly AbiError[] | undefined } /** @internal */ export function decode< const abiError extends AbiError, as extends 'Object' | 'Array' = 'Array', >( abiError: abiError, data: Hex.Hex, options?: decode.Options<as> | undefined, ): decode.ReturnType<abiError, as> /** * ABI-decodes the provided error input (`inputs`). * * :::tip * * This function is typically used to decode contract function reverts (e.g. a JSON-RPC error response). * * See the [End-to-end Example](#end-to-end). * * ::: * * @example * ```ts twoslash * import { AbiError } from 'ox' * * const error = AbiError.from('error InvalidSignature(uint r, uint s, uint8 yParity)') * * const value = AbiError.decode(error, '0xecde634900000000000000000000000000000000000000000000000000000000000001a400000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001') * // @log: [420n, 69n, 1] * ``` * * @example * You can extract an ABI Error from a JSON ABI with {@link ox#AbiError.(fromAbi:function)}: * * ```ts twoslash * // @noErrors * import { Abi, AbiError } from 'ox' * * const abi = Abi.from([...]) // [!code hl] * const error = AbiError.fromAbi(abi, 'InvalidSignature') // [!code hl] * * const value = AbiError.decode(error, '0xecde634900000000000000000000000000000000000000000000000000000000000001a400000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001') * // @log: [420n, 69n, 1] * ``` * * @example * You can pass the error `data` to the `name` property of {@link ox#AbiError.(fromAbi:function)} to extract and infer the error by its 4-byte selector: * * ```ts twoslash * // @noErrors * import { Abi, AbiError } from 'ox' * * const data = '0xecde634900000000000000000000000000000000000000000000000000000000000001a400000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001' * * const abi = Abi.from([...]) * const error = AbiError.fromAbi(abi, data) // [!code hl] * * const value = AbiError.decode(error, data) * // @log: [420n, 69n, 1] * ``` * * @example * ### ABI-shorthand * * You can also specify an entire ABI object as a parameter to {@link ox#AbiError.(decode:function)}: * * ```ts twoslash * // @noErrors * import { Abi, AbiError } from 'ox' * * const abi = Abi.from([...]) * * const value = AbiError.decode( * abi, // [!code hl] * 'InvalidSignature', // [!code hl] * '0x...' * ) * // @log: [420n, 69n, 1] * ``` * * @example * ### End-to-end * * Below is an end-to-end example of using `AbiError.decode` to decode the revert error of an `approve` contract call on the [Wagmi Mint Example contract](https://etherscan.io/address/0xfba3912ca04dd458c843e2ee08967fc04f3579c2). * * ```ts twoslash * // @noErrors * import 'ox/window' * import { Abi, AbiError, AbiFunction } from 'ox' * * // 1. Extract the Function from the Contract's ABI. * const abi = Abi.from([ * // ... * { * inputs: [ * { name: 'to', type: 'address' }, * { name: 'tokenId', type: 'uint256' }, * ], * name: 'approve', * outputs: [], * stateMutability: 'nonpayable', * type: 'function', * }, * // ... * ]) * const approve = AbiFunction.fromAbi(abi, 'approve') * * // 2. Encode the Function Input. * const data = AbiFunction.encodeData( * approve, * ['0xd8da6bf26964af9d7eed9e03e53415d37aa96045', 69420n] * ) * * try { * // 3. Attempt to perform the the Contract Call. * await window.ethereum!.request({ * method: 'eth_call', * params: [ * { * data, * to: '0xfba3912ca04dd458c843e2ee08967fc04f3579c2', * }, * ], * }) * } catch (e) { // [!code focus] * // 4. Extract and decode the Error. // [!code focus] * const error = AbiError.fromAbi(abi, e.data) // [!code focus] * const value = AbiError.decode(error, e.data) // [!code focus] * console.error(`${error.name}(${value})`) // [!code focus] * // @error: Error(ERC721: approve caller is not owner nor approved for all) * } // [!code focus] * ``` * * :::note * * For simplicity, the above example uses `window.ethereum.request`, but you can use any * type of JSON-RPC interface. * * ::: * * @param abiError - The ABI Error to decode. * @param data - The error data. * @param options - Decoding options. * @returns The decoded error. */ export function decode< const abi extends Abi.Abi | readonly unknown[], name extends Name<abi>, const args extends | AbiItem_internal.ExtractArgs<abi, name> | undefined = undefined, as extends 'Object' | 'Array' = 'Array', // abiError extends AbiError = AbiItem.fromAbi.ReturnType< abi, name, args, AbiError >, allNames = Name<abi>, >( abi: abi | Abi.Abi | readonly unknown[], name: Hex.Hex | (name extends allNames ? name : never), data: Hex.Hex, options?: decode.Options<as> | undefined, ): decode.ReturnType<abiError, as> export function decode< const abiError extends AbiError, as extends 'Object' | 'Array' = 'Array', >( abiError: abiError | AbiError, data: Hex.Hex, options?: decode.Options<as> | undefined, ): decode.ReturnType<abiError, as> // eslint-disable-next-line jsdoc/require-jsdoc export function decode( ...parameters: | [ abi: Abi.Abi | readonly unknown[], name: Hex.Hex | string, data: Hex.Hex, options?: decode.Options | undefined, ] | [abiError: AbiError, data: Hex.Hex, options?: decode.Options | undefined] ): decode.ReturnType { const [abiError, data, options = {}] = (() => { if (Array.isArray(parameters[0])) { const [abi, name, data, options] = parameters as [ Abi.Abi | readonly unknown[], Hex.Hex | string, Hex.Hex, decode.Options | undefined, ] return [fromAbi(abi, name), data, options] } return parameters as [AbiError, Hex.Hex, decode.Options | undefined] })() if (Hex.size(data) < 4) throw new AbiItem.InvalidSelectorSizeError({ data }) if (abiError.inputs.length === 0) return undefined const values = AbiParameters.decode( abiError.inputs, Hex.slice(data, 4), options, ) if (values && Object.keys(values).length === 1) { if (Array.isArray(values)) return values[0] return Object.values(values)[0] } return values } export declare namespace decode { type Options<as extends 'Object' | 'Array' = 'Array'> = { /** * Whether the decoded values should be returned as an `Object` or `Array`. * * @default "Array" */ as?: as | 'Array' | 'Object' | undefined } type ReturnType< abiError extends AbiError = AbiError, as extends 'Object' | 'Array' = 'Array', > = IsNarrowable<abiError, AbiError> extends true ? abiError['inputs'] extends readonly [] ? undefined : abiError['inputs'] extends readonly [ infer type extends abitype.AbiParameter, ] ? abitype.AbiParameterToPrimitiveType<type> : AbiParameters.decode.ReturnType< abiError['inputs'], as > extends infer types ? types extends readonly [] ? undefined : types extends readonly [infer type] ? type : types : never : unknown | readonly unknown[] | undefined type ErrorType = | AbiParameters.decode.ErrorType | Hex.size.ErrorType | typeof AbiItem.InvalidSelectorSizeError | Errors.GlobalErrorType } /** * ABI-encodes the provided error input (`inputs`), prefixed with the 4 byte error selector. * * @example * ```ts twoslash * import { AbiError } from 'ox' * * const error = AbiError.from( * 'error InvalidSignature(uint r, uint s, uint8 yParity)' * ) * * const data = AbiError.encode( // [!code focus] * error, // [!code focus] * [1n, 2n, 0] // [!code focus] * ) // [!code focus] * // @log: '0x095ea7b3000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000000000000010f2c' * ``` * * @example * ### ABI-shorthand * * You can also specify an entire ABI object and an error name as parameters to `AbiError.encode`. * * ```ts twoslash * // @noErrors * import { Abi, AbiError } from 'ox' * * const abi = Abi.from([...]) * * const data = AbiError.encode( * abi, // [!code hl] * 'InvalidSignature', // [!code hl] * [1n, 2n, 0] * ) * ``` * * @param abiError - ABI Error to encode * @param args - Error arguments * @returns ABI-encoded error name and arguments */ export function encode< const abi extends Abi.Abi | readonly unknown[], name extends Name<abi>, const args extends | AbiItem_internal.ExtractArgs<abi, name> | undefined = undefined, // abiError extends AbiError = AbiItem.fromAbi.ReturnType< abi, name, args, AbiError >, allNames = Name<abi>, >( abi: abi | Abi.Abi | readonly unknown[], name: Hex.Hex | (name extends allNames ? name : never), ...args: encode.Args<abiError> ): encode.ReturnType export function encode<const abiError extends AbiError>( abiError: abiError, ...args: encode.Args<abiError> ): encode.ReturnType // eslint-disable-next-line jsdoc/require-jsdoc export function encode( ...parameters: | [ abi: Abi.Abi | readonly unknown[], name: Hex.Hex | string, ...args: readonly unknown[], ] | [abiError: AbiError, ...args: readonly unknown[]] ) { const [abiError, args] = (() => { if (Array.isArray(parameters[0])) { const [abi, name, ...args] = parameters as [ Abi.Abi | readonly unknown[], Hex.Hex | string, ...(readonly unknown[]), ] return [fromAbi(abi, name), args] } const [abiError, ...args] = parameters as [ AbiError, ...(readonly unknown[]), ] return [abiError, args] })() const selector = getSelector(abiError) const data = args.length > 0 ? AbiParameters.encode(abiError.inputs, (args as any)[0]) : undefined return data ? Hex.concat(selector, data) : selector } export declare namespace encode { type Args<abiError extends AbiError = AbiError> = IsNarrowable< abiError, AbiError > extends true ? abitype.AbiParametersToPrimitiveTypes< abiError['inputs'] > extends readonly [] ? [] : [abitype.AbiParametersToPrimitiveTypes<abiError['inputs']>] : readonly unknown[] type ReturnType = Hex.Hex type ErrorType = Errors.GlobalErrorType } /** * Formats an {@link ox#AbiError.AbiError} into a **Human Readable ABI Error**. * * @example * ```ts twoslash * import { AbiError } from 'ox' * * const formatted = AbiError.format({ * type: 'error', * name: 'Example', * inputs: [ * { * name: 'spender', * type: 'address', * }, * { * name: 'amount', * type: 'uint256', * }, * ], * }) * * formatted * // ^? * * * ``` * * @param abiError - The ABI Error to format. * @returns The formatted ABI Error. */ export function format<const abiError extends AbiError>( abiError: abiError | AbiError, ): abitype.FormatAbiItem<abiError> { return abitype.formatAbiItem(abiError) as never } export declare namespace format { type ErrorType = Errors.GlobalErrorType } /** * Parses an arbitrary **JSON ABI Error** or **Human Readable ABI Error** into a typed {@link ox#AbiError.AbiError}. * * @example * ### JSON ABIs * * ```ts twoslash * import { AbiError } from 'ox' * * const badSignatureVError = AbiError.from({ * inputs: [{ name: 'v', type: 'uint8' }], * name: 'BadSignatureV', * type: 'error', * }) * * badSignatureVError * //^? * * * * * * * * * * * * * ``` * * @example * ### Human Readable ABIs * * A Human Readable ABI can be parsed into a typed ABI object: * * ```ts twoslash * import { AbiError } from 'ox' * * const badSignatureVError = AbiError.from( * 'error BadSignatureV(uint8 v)' // [!code hl] * ) * * badSignatureVError * //^? * * * * * * * * * * * * * * ``` * * @example * It is possible to specify `struct`s along with your definitions: * * ```ts twoslash * import { AbiError } from 'ox' * * const badSignatureVError = AbiError.from([ * 'struct Signature { uint8 v; }', // [!code hl] * 'error BadSignatureV(Signature signature)', * ]) * * badSignatureVError * //^? * * * * * * * * * * * * * ``` * * * * @param abiError - The ABI Error to parse. * @returns Typed ABI Error. */ export function from< const abiError extends AbiError | string | readonly string[], >( abiError: (abiError | AbiError | string | readonly string[]) & ( | (abiError extends string ? internal.Signature<abiError> : never) | (abiError extends readonly string[] ? internal.Signatures<abiError> : never) | AbiError ), options: from.Options = {}, ): from.ReturnType<abiError> { return AbiItem.from(abiError as AbiError, options) as never } export declare namespace from { type Options = { /** * Whether or not to prepare the extracted function (optimization for encoding performance). * When `true`, the `hash` property is computed and included in the returned value. * * @default true */ prepare?: boolean | undefined } type ReturnType<abiError extends AbiError | string | readonly string[]> = AbiItem.from.ReturnType<abiError> type ErrorType = AbiItem.from.ErrorType | Errors.GlobalErrorType } /** * Extracts an {@link ox#AbiError.AbiError} from an {@link ox#Abi.Abi} given a name and optional arguments. * * @example * ### Extracting by Name * * ABI Errors can be extracted by their name using the `name` option: * * ```ts twoslash * import { Abi, AbiError } from 'ox' * * const abi = Abi.from([ * 'function foo()', * 'error BadSignatureV(uint8 v)', * 'function bar(string a) returns (uint256 x)', * ]) * * const item = AbiError.fromAbi(abi, 'BadSignatureV') // [!code focus] * // ^? * * * * * * * ``` * * @example * ### Extracting by Selector * * ABI Errors can be extract by their selector when {@link ox#Hex.Hex} is provided to `name`. * * ```ts twoslash * import { Abi, AbiError } from 'ox' * * const abi = Abi.from([ * 'function foo()', * 'error BadSignatureV(uint8 v)', * 'function bar(string a) returns (uint256 x)', * ]) * const item = AbiError.fromAbi(abi, '0x095ea7b3') // [!code focus] * // ^? * * * * * * * * * * ``` * * :::note * * Extracting via a hex selector is useful when extracting an ABI Error from JSON-RPC error data. * * ::: * * @param abi - The ABI to extract from. * @param name - The name (or selector) of the ABI item to extract. * @param options - Extraction options. * @returns The ABI item. */ export function fromAbi< const abi extends Abi.Abi | readonly unknown[], name extends Name<abi>, const args extends | AbiItem_internal.ExtractArgs<abi, name> | undefined = undefined, // allNames = Name<abi>, >( abi: abi | Abi.Abi | readonly unknown[], name: Hex.Hex | (name extends allNames ? name : never), options?: AbiItem.fromAbi.Options< abi, name, args, AbiItem_internal.ExtractArgs<abi, name> >, ): fromAbi.ReturnType<abi, name, args> { if (name === 'Error') return solidityError as never if (name === 'Panic') return solidityPanic as never if (Hex.validate(name, { strict: false })) { const selector = Hex.slice(name, 0, 4) if (selector === solidityErrorSelector) return solidityError as never if (selector === solidityPanicSelector) return solidityPanic as never } const item = AbiItem.fromAbi(abi, name, options as any) if (item.type !== 'error') throw new AbiItem.NotFoundError({ name, type: 'error' }) return item as never } export declare namespace fromAbi { type ReturnType< abi extends Abi.Abi | readonly unknown[] = Abi.Abi, name extends Name<abi> = Name<abi>, args extends | AbiItem_internal.ExtractArgs<abi, name> | undefined = AbiItem_internal.ExtractArgs<abi, name>, > = IsNarrowable<name, Name<abi>> extends true ? | (name extends 'Error' ? typeof solidityError : never) | (name extends 'Panic' ? typeof solidityPanic : never) extends infer result ? IsNever<result> extends true ? AbiItem.fromAbi.ReturnType<abi, name, args, AbiError> : result : never : | AbiItem.fromAbi.ReturnType<abi, name, args, AbiError> | typeof solidityError | typeof solidityPanic type ErrorType = AbiItem.fromAbi.ErrorType | Errors.GlobalErrorType } /** * Computes the [4-byte selector](https://solidity-by-example.org/function-selector/) for an {@link ox#AbiError.AbiError}. * * @example * ```ts twoslash * import { AbiError } from 'ox' * * const selector = AbiError.getSelector('error BadSignatureV(uint8 v)') * // @log: '0x6352211e' * ``` * * @example * ```ts twoslash * import { AbiError } from 'ox' * * const selector = AbiError.getSelector({ * inputs: [{ name: 'v', type: 'uint8' }], * name: 'BadSignatureV', * type: 'error' * }) * // @log: '0x6352211e' * ``` * * @param abiItem - The ABI item to compute the selector for. * @returns The first 4 bytes of the {@link ox#Hash.(keccak256:function)} hash of the error signature. */ export function getSelector(abiItem: string | AbiError): Hex.Hex { return AbiItem.getSelector(abiItem) } export declare namespace getSelector { type ErrorType = AbiItem.getSelector.ErrorType | Errors.GlobalErrorType } // https://docs.soliditylang.org/en/v0.8.16/control-structures.html#panic-via-assert-and-error-via-require export const panicReasons = { 1: 'An `assert` condition failed.', 17: 'Arithmetic operation resulted in underflow or overflow.', 18: 'Division or modulo by zero (e.g. `5 / 0` or `23 % 0`).', 33: 'Attempted to convert to an invalid type.', 34: 'Attempted to access a storage byte array that is incorrectly encoded.', 49: 'Performed `.pop()` on an empty array', 50: 'Array index is out of bounds.', 65: 'Allocated too much memory or created an array which is too large.', 81: 'Attempted to call a zero-initialized variable of internal function type.', } as Record<number, string> export const solidityError = /*#__PURE__*/ from({ inputs: [ { name: 'message', type: 'string', }, ], name: 'Error', type: 'error', }) export const solidityErrorSelector = '0x08c379a0' export const solidityPanic = /*#__PURE__*/ from({ inputs: [ { name: 'reason', type: 'uint8', }, ], name: 'Panic', type: 'error', }) export const solidityPanicSelector = '0x4e487b71' /** * Extracts an {@link ox#AbiError.AbiError} item from an {@link ox#Abi.Abi}, given a name. * * @example * ```ts twoslash * import { Abi, AbiError } from 'ox' * * const abi = Abi.from([ * 'error Foo(string)', * 'error Bar(uint256)', * ]) * * type Foo = AbiError.FromAbi<typeof abi, 'Foo'> * // ^? * * * * * * * * * ``` */ export type FromAbi< abi extends Abi.Abi, name extends ExtractNames<abi>, > = abitype.ExtractAbiError<abi, name> /** * Extracts the names of all {@link ox#AbiError.AbiError} items in an {@link ox#Abi.Abi}. * * @example * ```ts twoslash * import { Abi, AbiError } from 'ox' * * const abi = Abi.from([ * 'error Foo(string)', * 'error Bar(uint256)', * ]) * * type names = AbiError.Name<typeof abi> * // ^? * ``` */ export type Name<abi extends Abi.Abi | readonly unknown[] = Abi.Abi> = abi extends Abi.Abi ? ExtractNames<abi> : string export type ExtractNames<abi extends Abi.Abi> = | abitype.ExtractAbiErrorNames<abi> | 'Panic' | 'Error'