diff --git a/.changeset/abi-from-name-selector-split.md b/.changeset/abi-from-name-selector-split.md new file mode 100644 index 00000000..f8853f8f --- /dev/null +++ b/.changeset/abi-from-name-selector-split.md @@ -0,0 +1,23 @@ +--- +"ox": major +--- + +Split polymorphic `from` and `fromAbi` on `Abi*` modules into shape-specific helpers and defaulted `Abi.from` to precompute signature `hash`es. + +```diff +- AbiFunction.from('function approve(address,uint256)') ++ AbiFunction.fromHumanReadable('function approve(address,uint256)') + +- AbiFunction.from({ type: 'function', name: 'approve', /* ... */ }) ++ AbiFunction.fromJson({ type: 'function', name: 'approve', /* ... */ }) + +- AbiFunction.fromAbi(abi, 'approve') ++ AbiFunction.fromAbiName(abi, 'approve') + +- AbiFunction.fromAbi(abi, '0x095ea7b3') ++ AbiFunction.fromAbiSelector(abi, '0x095ea7b3') + +- Abi.from(abi) // no `hash` precomputed ++ Abi.from(abi) // `hash` precomputed by default ++ Abi.from(abi, { prepare: false }) // opt out of `hash` precomputation +``` diff --git a/src/core/Abi.ts b/src/core/Abi.ts index 3907a9e4..d7982ec6 100644 --- a/src/core/Abi.ts +++ b/src/core/Abi.ts @@ -1,4 +1,5 @@ import * as abitype from 'abitype' +import * as AbiItem from './AbiItem.js' import type * as Errors from './Errors.js' import * as internal from './internal/abi.js' import type * as AbiItem_internal from './internal/abiItem.js' @@ -61,10 +62,14 @@ export function from( (abi extends readonly string[] ? AbiItem_internal.Signatures : unknown), + options?: from.Options | undefined, ): from.ReturnType /** * Parses an arbitrary **JSON ABI** or **Human Readable ABI** into a typed {@link ox#Abi.Abi}. * + * By default, each item is prepared (signature hash precomputed and cached on the item) + * for faster subsequent encode/decode. Opt out with `{ prepare: false }`. + * * @example * ### JSON ABIs * @@ -93,18 +98,6 @@ export function from( * * * - * - * - * - * - * - * - * - * - * - * - * - * * ``` * * @example @@ -122,31 +115,43 @@ export function from( * * * - * - * - * - * - * - * - * - * - * - * - * - * * ``` * * @param abi - The ABI to parse. + * @param options - Parsing options. * @returns The typed ABI. */ -export function from(abi: Abi | readonly string[]): Abi +export function from( + abi: Abi | readonly string[], + options?: from.Options | undefined, +): Abi // eslint-disable-next-line jsdoc/require-jsdoc -export function from(abi: Abi | readonly string[]): from.ReturnType { - if (internal.isSignatures(abi)) return abitype.parseAbi(abi) - return abi +export function from( + abi: Abi | readonly string[], + options?: from.Options | undefined, +): from.ReturnType { + const { prepare = true } = options ?? {} + const parsed = ( + internal.isSignatures(abi) ? abitype.parseAbi(abi) : abi + ) as Abi + if (!prepare) return parsed as never + return parsed.map((item) => ({ + ...item, + hash: AbiItem.getSignatureHash(item as AbiItem.AbiItem), + })) as never } export declare namespace from { + type Options = { + /** + * Whether or not to prepare each ABI item (optimization for encoding performance). + * When `true`, the `hash` property is precomputed and attached to every item. + * + * @default true + */ + prepare?: boolean | undefined + } + type ReturnType< abi extends Abi | readonly string[] | readonly unknown[] = Abi, > = abi extends readonly string[] ? abitype.ParseAbi : abi diff --git a/src/core/AbiConstructor.ts b/src/core/AbiConstructor.ts index f62acf7f..fb5c8328 100644 --- a/src/core/AbiConstructor.ts +++ b/src/core/AbiConstructor.ts @@ -17,7 +17,7 @@ export type AbiConstructor = abitype.AbiConstructor * ```ts twoslash * import { AbiConstructor } from 'ox' * - * const constructor = AbiConstructor.from('constructor(address, uint256)') + * const constructor = AbiConstructor.fromHumanReadable('constructor(address, uint256)') * * const bytecode = '0x...' * @@ -126,7 +126,7 @@ export declare namespace decode { * ```ts twoslash * import { AbiConstructor } from 'ox' * - * const constructor = AbiConstructor.from('constructor(address, uint256)') + * const constructor = AbiConstructor.fromHumanReadable('constructor(address, uint256)') * * const data = AbiConstructor.encode(constructor, { * bytecode: '0x...', @@ -161,7 +161,7 @@ export declare namespace decode { * import { AbiConstructor, Hex } from 'ox' * * // 1. Instantiate the ABI Constructor. - * const constructor = AbiConstructor.from( + * const constructor = AbiConstructor.fromHumanReadable( * 'constructor(address owner, uint256 amount)', * ) * @@ -299,127 +299,141 @@ export declare namespace format { type ErrorType = Errors.GlobalErrorType } -/** @internal */ -export function from< - const abiConstructor extends AbiConstructor | string | readonly string[], ->( - abiConstructor: (abiConstructor | string | readonly string[]) & - ( - | (abiConstructor extends string - ? internal.Signature - : never) - | (abiConstructor extends readonly string[] - ? internal.Signatures - : never) - | AbiConstructor - ), -): from.ReturnType /** - * Parses an arbitrary **JSON ABI Constructor** or **Human Readable ABI Constructor** into a typed {@link ox#AbiConstructor.AbiConstructor}. + * Parses a **Human Readable ABI Constructor** signature (or array of signatures with optional structs) into a typed {@link ox#AbiConstructor.AbiConstructor}. * * @example - * ### JSON ABIs - * * ```ts twoslash * import { AbiConstructor } from 'ox' * - * const constructor = AbiConstructor.from({ - * inputs: [ - * { name: 'owner', type: 'address' }, - * ], - * payable: false, - * stateMutability: 'nonpayable', - * type: 'constructor', - * }) + * const constructor = AbiConstructor.fromHumanReadable( + * 'constructor(address owner)' + * ) * * constructor * //^? * * * - * - * - * - * - * - * - * - * - * * ``` * * @example - * ### Human Readable ABIs - * - * A Human Readable ABI can be parsed into a typed ABI object: + * It is possible to specify `struct`s along with your definitions by passing an array: * * ```ts twoslash * import { AbiConstructor } from 'ox' * - * const constructor = AbiConstructor.from( - * 'constructor(address owner)' // [!code hl] - * ) + * const constructor = AbiConstructor.fromHumanReadable([ + * 'struct Foo { address owner; uint256 amount; }', + * 'constructor(Foo foo)', + * ]) * * constructor * //^? * * * - * - * - * - * - * - * - * - * - * - * * ``` * - * @example - * It is possible to specify `struct`s along with your definitions: + * @param signature - The human-readable signature (or array of signatures with optional structs) to parse. + * @param options - Parsing options. + * @returns Typed ABI Constructor. + */ +export function fromHumanReadable< + const signature extends string | readonly string[], +>( + signature: signature & + ( + | (signature extends string ? internal.Signature : never) + | (signature extends readonly string[] + ? internal.Signatures + : never) + ), + options?: AbiItem.fromHumanReadable.Options, +): fromHumanReadable.ReturnType { + return AbiItem.fromHumanReadable(signature as never, options) as never +} + +export declare namespace fromHumanReadable { + type ReturnType = + AbiItem.fromHumanReadable.ReturnType + + type ErrorType = AbiItem.fromHumanReadable.ErrorType | Errors.GlobalErrorType +} + +/** + * Parses a **JSON ABI Constructor** into a typed {@link ox#AbiConstructor.AbiConstructor}. * + * @example * ```ts twoslash * import { AbiConstructor } from 'ox' * - * const constructor = AbiConstructor.from([ - * 'struct Foo { address owner; uint256 amount; }', // [!code hl] - * 'constructor(Foo foo)', - * ]) + * const constructor = AbiConstructor.fromJson({ + * inputs: [ + * { name: 'owner', type: 'address' }, + * ], + * payable: false, + * stateMutability: 'nonpayable', + * type: 'constructor', + * }) * * constructor * //^? * * * - * - * - * - * - * - * - * - * - * * ``` * - * - * - * @param abiConstructor - The ABI Constructor to parse. + * @param abiConstructor - The JSON ABI Constructor to parse. + * @param options - Parsing options. * @returns Typed ABI Constructor. */ -export function from( - abiConstructor: AbiConstructor | string | readonly string[], -): AbiConstructor -/** @internal */ -export function from( - abiConstructor: AbiConstructor | string | readonly string[], -): from.ReturnType { - return AbiItem.from(abiConstructor as AbiConstructor) +export function fromJson( + abiConstructor: abiConstructor | AbiConstructor, + options?: AbiItem.fromJson.Options, +): fromJson.ReturnType { + return AbiItem.fromJson(abiConstructor as never, options) as never +} + +export declare namespace fromJson { + type ReturnType = + AbiItem.fromJson.ReturnType + + type ErrorType = AbiItem.fromJson.ErrorType | Errors.GlobalErrorType +} + +/** + * Internal dispatcher used by `decode` / `encode` shorthand overloads. + * Picks {@link AbiConstructor.fromHumanReadable} or {@link AbiConstructor.fromJson} + * based on the input shape. + * + * @internal + */ +export function from< + const abiConstructor extends AbiConstructor | string | readonly string[], +>( + abiConstructor: ( + | abiConstructor + | AbiConstructor + | string + | readonly string[] + ) & + ( + | (abiConstructor extends string + ? internal.Signature + : never) + | (abiConstructor extends readonly string[] + ? internal.Signatures + : never) + | AbiConstructor + ), + options?: AbiItem.from.Options, +): from.ReturnType { + return AbiItem.from(abiConstructor as never, options) as never } export declare namespace from { + /** @internal */ type ReturnType< abiConstructor extends | AbiConstructor @@ -427,6 +441,7 @@ export declare namespace from { | readonly string[] = AbiConstructor, > = AbiItem.from.ReturnType + /** @internal */ type ErrorType = AbiItem.from.ErrorType | Errors.GlobalErrorType } @@ -489,7 +504,7 @@ export declare namespace fromAbi { * import { AbiConstructor } from 'ox' * * AbiConstructor.decode( - * AbiConstructor.from('constructor(address)'), + * AbiConstructor.fromHumanReadable('constructor(address)'), * { bytecode: '0x6080...', data: '0xdeadbeef' }, * ) * // @error: AbiConstructor.BytecodeMismatchError: Provided `data` does not start with the provided `bytecode`. diff --git a/src/core/AbiError.ts b/src/core/AbiError.ts index 26226ba4..81df2b7e 100644 --- a/src/core/AbiError.ts +++ b/src/core/AbiError.ts @@ -513,6 +513,12 @@ export declare namespace format { * @param abiError - The ABI Error to parse. * @returns Typed ABI Error. */ +/** + * Internal dispatcher used by `decode` / `encode` shorthand overloads on {@link AbiError}. + * Picks {@link AbiError.fromHumanReadable} or {@link AbiError.fromJson} based on the input shape. + * + * @internal + */ export function from< const abiError extends AbiError | string | readonly string[], >( @@ -530,6 +536,7 @@ export function from< } export declare namespace from { + /** @internal */ type Options = { /** * Whether or not to prepare the extracted function (optimization for encoding performance). @@ -540,75 +547,103 @@ export declare namespace from { prepare?: boolean | undefined } + /** @internal */ type ReturnType = AbiItem.from.ReturnType + /** @internal */ 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. + * Parses a **Human Readable ABI Error** signature (or array of signatures with optional structs) into a typed {@link ox#AbiError.AbiError}. * * @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] - * // ^? - * + * import { AbiError } from 'ox' * + * const badSignatureVError = AbiError.fromHumanReadable( + * 'error BadSignatureV(uint8 v)' + * ) * + * badSignatureVError + * //^? * * * * ``` * - * @example - * ### Extracting by Selector - * - * ABI Errors can be extract by their selector when {@link ox#Hex.Hex} is provided to `name`. + * @param signature - The human-readable signature (or array of signatures with optional structs) to parse. + * @param options - Parsing options. + * @returns Typed ABI Error. + */ +export function fromHumanReadable< + const signature extends string | readonly string[], +>( + signature: signature & + ( + | (signature extends string ? internal.Signature : never) + | (signature extends readonly string[] + ? internal.Signatures + : never) + ), + options?: AbiItem.fromHumanReadable.Options, +): fromHumanReadable.ReturnType { + return AbiItem.fromHumanReadable(signature as never, options) as never +} + +export declare namespace fromHumanReadable { + type ReturnType = + AbiItem.fromHumanReadable.ReturnType + + type ErrorType = AbiItem.fromHumanReadable.ErrorType | Errors.GlobalErrorType +} + +/** + * Parses a **JSON ABI Error** into a typed {@link ox#AbiError.AbiError}. * + * @example * ```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] - * // ^? - * - * - * - * + * import { AbiError } from 'ox' * + * const badSignatureVError = AbiError.fromJson({ + * type: 'error', + * name: 'BadSignatureV', + * inputs: [{ name: 'v', type: 'uint8' }], + * }) * + * badSignatureVError + * //^? * * * * ``` * - * :::note - * - * Extracting via a hex selector is useful when extracting an ABI Error from JSON-RPC error data. - * - * ::: + * @param abiError - The JSON ABI Error to parse. + * @param options - Parsing options. + * @returns Typed ABI Error. + */ +export function fromJson( + abiError: abiError | AbiError, + options?: AbiItem.fromJson.Options, +): fromJson.ReturnType { + return AbiItem.fromJson(abiError as never, options) as never +} + +export declare namespace fromJson { + type ReturnType = + AbiItem.fromJson.ReturnType + + type ErrorType = AbiItem.fromJson.ErrorType | Errors.GlobalErrorType +} + +/** + * Internal dispatcher used by `decode` / `encode` shorthand overloads on {@link AbiError}. + * Picks {@link AbiError.fromAbiName} or {@link AbiError.fromAbiSelector} based on whether + * `name` parses as a hex selector. Also handles the built-in Solidity `Error` and `Panic` + * errors. * - * @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. + * @internal */ export function fromAbi< const abi extends Abi.Abi | readonly unknown[], @@ -636,13 +671,14 @@ export function fromAbi< if (selector === solidityPanicSelector) return solidityPanic as never } - const item = AbiItem.fromAbi(abi, name, options as any) + const item = AbiItem.fromAbi(abi, name, options as any) as AbiItem.AbiItem if (item.type !== 'error') throw new AbiItem.NotFoundError({ name, type: 'error' }) return item as never } export declare namespace fromAbi { + /** @internal */ type ReturnType< abi extends Abi.Abi | readonly unknown[] = Abi.Abi, name extends Name = Name, @@ -664,9 +700,146 @@ export declare namespace fromAbi { | typeof solidityError | typeof solidityPanic + /** @internal */ type ErrorType = AbiItem.fromAbi.ErrorType | Errors.GlobalErrorType } +/** + * Extracts an {@link ox#AbiError.AbiError} from an {@link ox#Abi.Abi} by error name. + * + * Special-cases the built-in Solidity `Error` and `Panic` errors. + * + * @example + * ```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.fromAbiName(abi, 'BadSignatureV') + * // ^? + * + * + * + * + * + * + * ``` + * + * @param abi - The ABI to extract from. + * @param name - The name of the error to extract. + * @param options - Extraction options. + * @returns The ABI Error. + */ +export function fromAbiName< + const abi extends Abi.Abi | readonly unknown[], + name extends Name, + const args extends + | AbiItem_internal.ExtractArgs + | undefined = undefined, + // + allNames = Name, +>( + abi: abi | Abi.Abi | readonly unknown[], + name: name extends allNames ? name : never, + options?: AbiItem.fromAbiName.Options< + abi, + name, + args, + AbiItem_internal.ExtractArgs + >, +): fromAbiName.ReturnType { + if ((name as unknown) === 'Error') return solidityError as never + if ((name as unknown) === 'Panic') return solidityPanic as never + const item = AbiItem.fromAbiName(abi, name, options as any) as AbiItem.AbiItem + if (item.type !== 'error') + throw new AbiItem.NotFoundError({ name, type: 'error' }) + return item as never +} + +export declare namespace fromAbiName { + type ReturnType< + abi extends Abi.Abi | readonly unknown[] = Abi.Abi, + name extends Name = Name, + args extends + | AbiItem_internal.ExtractArgs + | undefined = AbiItem_internal.ExtractArgs, + > = IsNarrowable> extends true + ? + | (name extends 'Error' ? typeof solidityError : never) + | (name extends 'Panic' + ? typeof solidityPanic + : never) extends infer result + ? IsNever extends true + ? AbiItem.fromAbiName.ReturnType + : result + : never + : + | AbiItem.fromAbiName.ReturnType + | typeof solidityError + | typeof solidityPanic + + type ErrorType = AbiItem.fromAbiName.ErrorType | Errors.GlobalErrorType +} + +/** + * Extracts an {@link ox#AbiError.AbiError} from an {@link ox#Abi.Abi} by 4-byte selector + * (or full error data; the first 4 bytes are sliced). + * + * Special-cases the built-in Solidity `Error` and `Panic` selectors. + * + * @example + * ```ts twoslash + * import { Abi, AbiError } from 'ox' + * + * const abi = Abi.from([ + * 'error BadSignatureV(uint8 v)', + * 'function bar(string a) returns (uint256 x)', + * ]) + * + * const item = AbiError.fromAbiSelector(abi, '0x6352211e') + * // ^? + * + * + * + * + * + * + * + * + * + * ``` + * + * @param abi - The ABI to extract from. + * @param selector - The 4-byte selector (or full error data) to look up. + * @param options - Extraction options. + * @returns The ABI Error. + */ +export function fromAbiSelector( + abi: abi | Abi.Abi | readonly unknown[], + selector: Hex.Hex, + options?: AbiItem.fromAbiSelector.Options, +): AbiError | typeof solidityError | typeof solidityPanic { + const selector_ = Hex.slice(selector, 0, 4) + if (selector_ === solidityErrorSelector) return solidityError + if (selector_ === solidityPanicSelector) return solidityPanic + const item = AbiItem.fromAbiSelector( + abi, + selector, + options, + ) as AbiItem.AbiItem + if (item.type !== 'error') + throw new AbiItem.NotFoundError({ name: selector, type: 'error' }) + return item as AbiError +} + +export declare namespace fromAbiSelector { + type ErrorType = AbiItem.fromAbiSelector.ErrorType | Errors.GlobalErrorType +} + /** * Computes the [4-byte selector](https://solidity-by-example.org/function-selector/) for an {@link ox#AbiError.AbiError}. * diff --git a/src/core/AbiEvent.ts b/src/core/AbiEvent.ts index dd1a17a1..ad3903ed 100644 --- a/src/core/AbiEvent.ts +++ b/src/core/AbiEvent.ts @@ -210,7 +210,7 @@ export function assertArgs( > : unknown, ) { - if (!args || !matchArgs) + if (!args || !matchArgs || abiEvent.inputs.length === 0) throw new ArgsMismatchError({ abiEvent, expected: args, @@ -560,7 +560,7 @@ export function decode( } } - return Object.values(args).length > 0 ? args : undefined + return args } export declare namespace decode { @@ -574,7 +574,7 @@ export declare namespace decode { AbiEvent > extends true ? abiEvent['inputs'] extends readonly [] - ? undefined + ? Record | readonly [] : internal.ParametersToPrimitiveTypes< abiEvent['inputs'], { EnableUnion: false; IndexedOnly: false; Required: true } @@ -952,6 +952,13 @@ export declare namespace format { * @param abiEvent - The ABI Event to parse. * @returns Typed ABI Event. */ +/** + * Internal dispatcher used by `decode` / `encode` shorthand overloads on {@link AbiEvent}. + * Picks {@link AbiEvent.fromHumanReadable} or {@link AbiEvent.fromJson} based on the + * input shape. + * + * @internal + */ export function from< const abiEvent extends AbiEvent | string | readonly string[], >( @@ -969,6 +976,7 @@ export function from< } export declare namespace from { + /** @internal */ type Options = { /** * Whether or not to prepare the extracted event (optimization for encoding performance). @@ -979,44 +987,140 @@ export declare namespace from { prepare?: boolean | undefined } + /** @internal */ type ReturnType = AbiItem.from.ReturnType + /** @internal */ type ErrorType = AbiItem.from.ErrorType | Errors.GlobalErrorType } /** - * Extracts an {@link ox#AbiEvent.AbiEvent} from an {@link ox#Abi.Abi} given a name and optional arguments. + * Parses a **Human Readable ABI Event** signature (or array of signatures with optional structs) into a typed {@link ox#AbiEvent.AbiEvent}. * * @example - * ### Extracting by Name + * ```ts twoslash + * import { AbiEvent } from 'ox' * - * ABI Events can be extracted by their name using the `name` option: + * const transfer = AbiEvent.fromHumanReadable( + * 'event Transfer(address indexed from, address indexed to, uint256 value)' + * ) * - * ```ts twoslash - * import { Abi, AbiEvent } from 'ox' + * transfer + * //^? * - * const abi = Abi.from([ - * 'function foo()', - * 'event Transfer(address owner, address to, uint256 tokenId)', - * 'function bar(string a) returns (uint256 x)', - * ]) * - * const item = AbiEvent.fromAbi(abi, 'Transfer') // [!code focus] - * // ^? * + * ``` * + * @param signature - The human-readable signature (or array of signatures with optional structs) to parse. + * @param options - Parsing options. + * @returns Typed ABI Event. + */ +export function fromHumanReadable< + const signature extends string | readonly string[], +>( + signature: signature & + ( + | (signature extends string ? internal.Signature : never) + | (signature extends readonly string[] + ? internal.Signatures + : never) + ), + options?: AbiItem.fromHumanReadable.Options, +): fromHumanReadable.ReturnType { + return AbiItem.fromHumanReadable(signature as never, options) as never +} + +export declare namespace fromHumanReadable { + type ReturnType = + AbiItem.fromHumanReadable.ReturnType + + type ErrorType = AbiItem.fromHumanReadable.ErrorType | Errors.GlobalErrorType +} + +/** + * Parses a **JSON ABI Event** into a typed {@link ox#AbiEvent.AbiEvent}. * + * @example + * ```ts twoslash + * import { AbiEvent } from 'ox' + * + * const transfer = AbiEvent.fromJson({ + * type: 'event', + * name: 'Transfer', + * inputs: [ + * { name: 'from', type: 'address', indexed: true }, + * { name: 'to', type: 'address', indexed: true }, + * { name: 'value', type: 'uint256', indexed: false }, + * ], + * }) + * + * transfer + * //^? * * * * ``` * - * @example - * ### Extracting by Selector + * @param abiEvent - The JSON ABI Event to parse. + * @param options - Parsing options. + * @returns Typed ABI Event. + */ +export function fromJson( + abiEvent: abiEvent | AbiEvent, + options?: AbiItem.fromJson.Options, +): fromJson.ReturnType { + return AbiItem.fromJson(abiEvent as never, options) as never +} + +export declare namespace fromJson { + type ReturnType = + AbiItem.fromJson.ReturnType + + type ErrorType = AbiItem.fromJson.ErrorType | Errors.GlobalErrorType +} + +/** + * Internal dispatcher used by `decode` / `encode` shorthand overloads on {@link AbiEvent}. + * Picks {@link AbiEvent.fromAbiName} or {@link AbiEvent.fromAbiSelector} based on whether + * `name` parses as a hex selector. * - * ABI Events can be extract by their selector when {@link ox#Hex.Hex} is provided to `name`. + * @internal + */ +export function fromAbi< + const abi extends Abi.Abi | readonly unknown[], + name extends Name, + const args extends + | AbiItem_internal.ExtractArgs + | undefined = undefined, + // + allNames = Name, +>( + abi: abi | Abi.Abi | readonly unknown[], + name: Hex.Hex | (name extends allNames ? name : never), + options?: AbiItem.fromAbi.Options< + abi, + name, + args, + AbiItem_internal.ExtractArgs + >, +): AbiItem.fromAbi.ReturnType { + const item = AbiItem.fromAbi(abi, name, options as any) as AbiItem.AbiItem + if (item.type !== 'event') + throw new AbiItem.NotFoundError({ name, type: 'event' }) + return item as never +} + +export declare namespace fromAbi { + /** @internal */ + type ErrorType = AbiItem.fromAbi.ErrorType | Errors.GlobalErrorType +} + +/** + * Extracts an {@link ox#AbiEvent.AbiEvent} from an {@link ox#Abi.Abi} given an event name. * + * @example * ```ts twoslash * import { Abi, AbiEvent } from 'ox' * @@ -1025,11 +1129,9 @@ export declare namespace from { * 'event Transfer(address owner, address to, uint256 tokenId)', * 'function bar(string a) returns (uint256 x)', * ]) - * const item = AbiEvent.fromAbi(abi, '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef') // [!code focus] - * // ^? - * - * * + * const item = AbiEvent.fromAbiName(abi, 'Transfer') + * // ^? * * * @@ -1038,18 +1140,12 @@ export declare namespace from { * * ``` * - * :::note - * - * Extracting via a hex selector is useful when extracting an ABI Event from the first topic of a Log. - * - * ::: - * * @param abi - The ABI to extract from. - * @param name - The name (or selector) of the ABI item to extract. + * @param name - The name of the event to extract. * @param options - Extraction options. - * @returns The ABI item. + * @returns The ABI Event. */ -export function fromAbi< +export function fromAbiName< const abi extends Abi.Abi | readonly unknown[], name extends Name, const args extends @@ -1059,22 +1155,71 @@ export function fromAbi< allNames = Name, >( abi: abi | Abi.Abi | readonly unknown[], - name: Hex.Hex | (name extends allNames ? name : never), - options?: AbiItem.fromAbi.Options< + name: name extends allNames ? name : never, + options?: AbiItem.fromAbiName.Options< abi, name, args, AbiItem_internal.ExtractArgs >, -): AbiItem.fromAbi.ReturnType { - const item = AbiItem.fromAbi(abi, name, options as any) +): AbiItem.fromAbiName.ReturnType { + const item = AbiItem.fromAbiName(abi, name, options as any) as AbiItem.AbiItem if (item.type !== 'event') throw new AbiItem.NotFoundError({ name, type: 'event' }) return item as never } -export declare namespace fromAbi { - type ErrorType = AbiItem.fromAbi.ErrorType | Errors.GlobalErrorType +export declare namespace fromAbiName { + type ErrorType = AbiItem.fromAbiName.ErrorType | Errors.GlobalErrorType +} + +/** + * Extracts an {@link ox#AbiEvent.AbiEvent} from an {@link ox#Abi.Abi} by topic-0 hash + * (a 32-byte event topic hash). + * + * @example + * ```ts twoslash + * import { Abi, AbiEvent } from 'ox' + * + * const abi = Abi.from([ + * 'event Transfer(address owner, address to, uint256 tokenId)', + * 'function bar(string a) returns (uint256 x)', + * ]) + * const item = AbiEvent.fromAbiSelector(abi, '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef') + * // ^? + * + * + * + * + * + * + * + * + * + * ``` + * + * @param abi - The ABI to extract from. + * @param selector - The 32-byte event topic hash to look up. + * @param options - Extraction options. + * @returns The ABI Event. + */ +export function fromAbiSelector( + abi: abi | Abi.Abi | readonly unknown[], + selector: Hex.Hex, + options?: AbiItem.fromAbiSelector.Options, +): AbiEvent { + const item = AbiItem.fromAbiSelector( + abi, + selector, + options, + ) as AbiItem.AbiItem + if (item.type !== 'event') + throw new AbiItem.NotFoundError({ name: selector, type: 'event' }) + return item as AbiEvent +} + +export declare namespace fromAbiSelector { + type ErrorType = AbiItem.fromAbiSelector.ErrorType | Errors.GlobalErrorType } /** @@ -1194,13 +1339,17 @@ export class ArgsMismatchError extends Errors.BaseError { expected: unknown given: unknown }) { + const isEmpty = (v: unknown) => + !v || + (Array.isArray(v) && v.length === 0) || + (typeof v === 'object' && Object.keys(v as object).length === 0) super('Given arguments do not match the expected arguments.', { metaMessages: [ `Event: ${format(abiEvent)}`, - `Expected Arguments: ${!expected ? 'None' : ''}`, - expected ? prettyPrint(expected) : undefined, - `Given Arguments: ${!given ? 'None' : ''}`, - given ? prettyPrint(given) : undefined, + `Expected Arguments: ${isEmpty(expected) ? 'None' : ''}`, + isEmpty(expected) ? undefined : prettyPrint(expected), + `Given Arguments: ${isEmpty(given) ? 'None' : ''}`, + isEmpty(given) ? undefined : prettyPrint(given), ], }) } diff --git a/src/core/AbiFunction.ts b/src/core/AbiFunction.ts index 46c99cbd..d1946eae 100644 --- a/src/core/AbiFunction.ts +++ b/src/core/AbiFunction.ts @@ -895,6 +895,13 @@ export declare namespace format { * @param abiFunction - The ABI Function to parse. * @returns Typed ABI Function. */ +/** + * Internal dispatcher used by `decode` / `encode` shorthand overloads. + * Picks {@link AbiFunction.fromHumanReadable} or {@link AbiFunction.fromJson} + * based on the input shape. + * + * @internal + */ export function from< const abiFunction extends AbiFunction | string | readonly string[], >( @@ -912,6 +919,7 @@ export function from< } export declare namespace from { + /** @internal */ type Options = { /** * Whether or not to prepare the extracted function (optimization for encoding performance). @@ -922,45 +930,160 @@ export declare namespace from { prepare?: boolean | undefined } + /** @internal */ type ReturnType< abiFunction extends AbiFunction | string | readonly string[], > = AbiItem.from.ReturnType + /** @internal */ type ErrorType = AbiItem.from.ErrorType | Errors.GlobalErrorType } /** - * Extracts an {@link ox#AbiFunction.AbiFunction} from an {@link ox#Abi.Abi} given a name and optional arguments. + * Parses a **Human Readable ABI Function** signature (or array of signatures with optional structs) into a typed {@link ox#AbiFunction.AbiFunction}. * * @example - * ### Extracting by Name + * ```ts twoslash + * import { AbiFunction } from 'ox' + * + * const approve = AbiFunction.fromHumanReadable( + * 'function approve(address spender, uint256 amount) returns (bool)' + * ) + * + * approve + * //^? + * * - * ABI Functions can be extracted by their name using the `name` option: + * + * ``` + * + * @example + * It is possible to specify `struct`s along with your definitions by passing an array: * * ```ts twoslash - * import { Abi, AbiFunction } from 'ox' + * import { AbiFunction } from 'ox' * - * const abi = Abi.from([ - * 'function foo()', - * 'event Transfer(address owner, address to, uint256 tokenId)', - * 'function bar(string a) returns (uint256 x)', + * const approve = AbiFunction.fromHumanReadable([ + * 'struct Foo { address spender; uint256 amount; }', + * 'function approve(Foo foo) returns (bool)', * ]) * - * const item = AbiFunction.fromAbi(abi, 'foo') // [!code focus] - * // ^? + * approve + * //^? * * * + * ``` + * + * @param signature - The human-readable signature (or array of signatures with optional structs) to parse. + * @param options - Parsing options. + * @returns Typed ABI Function. + */ +export function fromHumanReadable< + const signature extends string | readonly string[], +>( + signature: signature & + ( + | (signature extends string ? internal.Signature : never) + | (signature extends readonly string[] + ? internal.Signatures + : never) + ), + options?: AbiItem.fromHumanReadable.Options, +): fromHumanReadable.ReturnType { + return AbiItem.fromHumanReadable(signature as never, options) as never +} + +export declare namespace fromHumanReadable { + type ReturnType = + AbiItem.fromHumanReadable.ReturnType + + type ErrorType = AbiItem.fromHumanReadable.ErrorType | Errors.GlobalErrorType +} + +/** + * Parses a **JSON ABI Function** into a typed {@link ox#AbiFunction.AbiFunction}. + * + * @example + * ```ts twoslash + * import { AbiFunction } from 'ox' + * + * const approve = AbiFunction.fromJson({ + * type: 'function', + * name: 'approve', + * stateMutability: 'nonpayable', + * inputs: [ + * { name: 'spender', type: 'address' }, + * { name: 'amount', type: 'uint256' }, + * ], + * outputs: [{ type: 'bool' }], + * }) + * + * approve + * //^? * * * * ``` * - * @example - * ### Extracting by Selector + * @param abiFunction - The JSON ABI Function to parse. + * @param options - Parsing options. + * @returns Typed ABI Function. + */ +export function fromJson( + abiFunction: abiFunction | AbiFunction, + options?: AbiItem.fromJson.Options, +): fromJson.ReturnType { + return AbiItem.fromJson(abiFunction as never, options) as never +} + +export declare namespace fromJson { + type ReturnType = + AbiItem.fromJson.ReturnType + + type ErrorType = AbiItem.fromJson.ErrorType | Errors.GlobalErrorType +} + +/** + * Internal dispatcher used by `decode` / `encode` shorthand overloads on {@link AbiFunction}. + * Picks {@link AbiFunction.fromAbiName} or {@link AbiFunction.fromAbiSelector} based on + * whether `name` parses as a hex selector. * - * ABI Functions can be extract by their selector when {@link ox#Hex.Hex} is provided to `name`. + * @internal + */ +export function fromAbi< + const abi extends Abi.Abi | readonly unknown[], + name extends Name, + const args extends + | AbiItem_internal.ExtractArgs + | undefined = undefined, + // + allNames = Name, +>( + abi: abi | Abi.Abi | readonly unknown[], + name: Hex.Hex | (name extends allNames ? name : never), + options?: AbiItem.fromAbi.Options< + abi, + name, + args, + AbiItem_internal.ExtractArgs + >, +): AbiItem.fromAbi.ReturnType { + const item = AbiItem.fromAbi(abi, name, options as any) as AbiItem.AbiItem + if (item.type !== 'function') + throw new AbiItem.NotFoundError({ name, type: 'function' }) + return item as never +} + +export declare namespace fromAbi { + /** @internal */ + type ErrorType = AbiItem.fromAbi.ErrorType | Errors.GlobalErrorType +} + +/** + * Extracts an {@link ox#AbiFunction.AbiFunction} from an {@link ox#Abi.Abi} given a function name (and optional arguments to disambiguate overloads). * + * @example * ```ts twoslash * import { Abi, AbiFunction } from 'ox' * @@ -969,11 +1092,9 @@ export declare namespace from { * 'event Transfer(address owner, address to, uint256 tokenId)', * 'function bar(string a) returns (uint256 x)', * ]) - * const item = AbiFunction.fromAbi(abi, '0x095ea7b3') // [!code focus] - * // ^? - * - * * + * const item = AbiFunction.fromAbiName(abi, 'foo') + * // ^? * * * @@ -982,19 +1103,12 @@ export declare namespace from { * * ``` * - * :::note - * - * Extracting via a hex selector is useful when extracting an ABI Function from an `eth_call` RPC response or - * from a Transaction `input`. - * - * ::: - * * @param abi - The ABI to extract from. - * @param name - The name (or selector) of the ABI item to extract. + * @param name - The name of the function to extract. * @param options - Extraction options. - * @returns The ABI item. + * @returns The ABI Function. */ -export function fromAbi< +export function fromAbiName< const abi extends Abi.Abi | readonly unknown[], name extends Name, const args extends @@ -1004,22 +1118,72 @@ export function fromAbi< allNames = Name, >( abi: abi | Abi.Abi | readonly unknown[], - name: Hex.Hex | (name extends allNames ? name : never), - options?: AbiItem.fromAbi.Options< + name: name extends allNames ? name : never, + options?: AbiItem.fromAbiName.Options< abi, name, args, AbiItem_internal.ExtractArgs >, -): AbiItem.fromAbi.ReturnType { - const item = AbiItem.fromAbi(abi, name, options as any) +): AbiItem.fromAbiName.ReturnType { + const item = AbiItem.fromAbiName(abi, name, options as any) as AbiItem.AbiItem if (item.type !== 'function') throw new AbiItem.NotFoundError({ name, type: 'function' }) return item as never } -export declare namespace fromAbi { - type ErrorType = AbiItem.fromAbi.ErrorType | Errors.GlobalErrorType +export declare namespace fromAbiName { + type ErrorType = AbiItem.fromAbiName.ErrorType | Errors.GlobalErrorType +} + +/** + * Extracts an {@link ox#AbiFunction.AbiFunction} from an {@link ox#Abi.Abi} by 4-byte selector + * (or full calldata; the first 4 bytes are sliced). + * + * @example + * ```ts twoslash + * import { Abi, AbiFunction } from 'ox' + * + * const abi = Abi.from([ + * 'function foo()', + * 'function bar(string a) returns (uint256 x)', + * ]) + * + * const item = AbiFunction.fromAbiSelector(abi, '0x095ea7b3') + * // ^? + * + * + * + * + * + * + * + * + * + * ``` + * + * @param abi - The ABI to extract from. + * @param selector - The 4-byte selector (or full calldata) to look up. + * @param options - Extraction options. + * @returns The ABI Function. + */ +export function fromAbiSelector( + abi: abi | Abi.Abi | readonly unknown[], + selector: Hex.Hex, + options?: AbiItem.fromAbiSelector.Options, +): AbiFunction { + const item = AbiItem.fromAbiSelector( + abi, + selector, + options, + ) as AbiItem.AbiItem + if (item.type !== 'function') + throw new AbiItem.NotFoundError({ name: selector, type: 'function' }) + return item as AbiFunction +} + +export declare namespace fromAbiSelector { + type ErrorType = AbiItem.fromAbiSelector.ErrorType | Errors.GlobalErrorType } /** diff --git a/src/core/AbiItem.ts b/src/core/AbiItem.ts index b6be5253..a64002b0 100644 --- a/src/core/AbiItem.ts +++ b/src/core/AbiItem.ts @@ -109,57 +109,14 @@ export declare namespace format { } /** - * Parses an arbitrary **JSON ABI Item** or **Human Readable ABI Item** into a typed {@link ox#AbiItem.AbiItem}. + * Parses a **Human Readable ABI Item** signature (or array of signatures with optional structs) into a typed {@link ox#AbiItem.AbiItem}. * * @example - * ### JSON ABIs - * * ```ts twoslash * import { AbiItem } from 'ox' * - * const abiItem = AbiItem.from({ - * type: 'function', - * name: 'approve', - * stateMutability: 'nonpayable', - * inputs: [ - * { - * name: 'spender', - * type: 'address', - * }, - * { - * name: 'amount', - * type: 'uint256', - * }, - * ], - * outputs: [{ type: 'bool' }], - * }) - * - * abiItem - * //^? - * - * - * - * - * - * - * - * - * - * - * - * - * ``` - * - * @example - * ### Human Readable ABIs - * - * A Human Readable ABI can be parsed into a typed ABI object: - * - * ```ts twoslash - * import { AbiItem } from 'ox' - * - * const abiItem = AbiItem.from( - * 'function approve(address spender, uint256 amount) returns (bool)' // [!code hl] + * const abiItem = AbiItem.fromHumanReadable( + * 'function approve(address spender, uint256 amount) returns (bool)' * ) * * abiItem @@ -167,26 +124,16 @@ export declare namespace format { * * * - * - * - * - * - * - * - * - * - * - * * ``` * * @example - * It is possible to specify `struct`s along with your definitions: + * It is possible to specify `struct`s along with your definitions by passing an array of signatures: * * ```ts twoslash * import { AbiItem } from 'ox' * - * const abiItem = AbiItem.from([ - * 'struct Foo { address spender; uint256 amount; }', // [!code hl] + * const abiItem = AbiItem.fromHumanReadable([ + * 'struct Foo { address spender; uint256 amount; }', * 'function approve(Foo foo) returns (bool)', * ]) * @@ -195,22 +142,39 @@ export declare namespace format { * * * - * - * - * - * - * - * - * - * - * * ``` * - * - * - * @param abiItem - The ABI Item to parse. + * @param signature - The human-readable signature (or array of signatures with optional structs) to parse. + * @param options - Parsing options. * @returns The typed ABI Item. */ +export function fromHumanReadable< + const signature extends string | readonly string[], +>( + signature: signature & + ( + | (signature extends string ? internal.Signature : never) + | (signature extends readonly string[] + ? internal.Signatures + : never) + ), + options?: fromHumanReadable.Options, +): fromHumanReadable.ReturnType { + const { prepare = true } = options ?? {} + const item = abitype.parseAbiItem(signature as never) as AbiItem + return { + ...item, + ...(prepare ? { hash: getSignatureHash(item) } : {}), + } as never +} + +/** + * Internal dispatcher used by shorthand entry points that accept either a + * human-readable signature, an array of signatures, or a JSON ABI item. + * Picks `fromHumanReadable` or `fromJson` based on the input shape. + * + * @internal + */ export function from< const abiItem extends AbiItem | string | readonly string[], >( @@ -222,22 +186,32 @@ export function from< : never) | AbiItem ), - options: from.Options = {}, + options?: fromHumanReadable.Options, ): from.ReturnType { - const { prepare = true } = options - const item = (() => { - if (Array.isArray(abiItem)) return abitype.parseAbiItem(abiItem) - if (typeof abiItem === 'string') - return abitype.parseAbiItem(abiItem as never) - return abiItem - })() as AbiItem - return { - ...item, - ...(prepare ? { hash: getSignatureHash(item) } : {}), - } as never + if (Array.isArray(abiItem)) + return fromHumanReadable(abiItem as never, options) as never + if (typeof abiItem === 'string') + return fromHumanReadable(abiItem as never, options) as never + return fromJson(abiItem as never, options) as never } export declare namespace from { + /** @internal */ + type Options = fromHumanReadable.Options + + /** @internal */ + type ReturnType = + abiItem extends string + ? abitype.ParseAbiItem + : abiItem extends readonly string[] + ? abitype.ParseAbiItem + : abiItem + + /** @internal */ + type ErrorType = Errors.GlobalErrorType +} + +export declare namespace fromHumanReadable { type Options = { /** * Whether or not to prepare the extracted item (optimization for encoding performance). @@ -248,46 +222,131 @@ export declare namespace from { prepare?: boolean | undefined } - type ReturnType = - abiItem extends string - ? abitype.ParseAbiItem - : abiItem extends readonly string[] - ? abitype.ParseAbiItem - : abiItem + type ReturnType = + signature extends string + ? abitype.ParseAbiItem + : signature extends readonly string[] + ? abitype.ParseAbiItem + : never type ErrorType = Errors.GlobalErrorType } /** - * Extracts an {@link ox#AbiItem.AbiItem} from an {@link ox#Abi.Abi} given a name and optional arguments. + * Parses a **JSON ABI Item** into a typed {@link ox#AbiItem.AbiItem}. * * @example - * ABI Items can be extracted by their name using the `name` option: - * * ```ts twoslash - * import { Abi, AbiItem } from 'ox' - * - * const abi = Abi.from([ - * 'function foo()', - * 'event Transfer(address owner, address to, uint256 tokenId)', - * 'function bar(string a) returns (uint256 x)', - * ]) - * - * const item = AbiItem.fromAbi(abi, 'Transfer') // [!code focus] - * // ^? - * + * import { AbiItem } from 'ox' * + * const abiItem = AbiItem.fromJson({ + * type: 'function', + * name: 'approve', + * stateMutability: 'nonpayable', + * inputs: [ + * { + * name: 'spender', + * type: 'address', + * }, + * { + * name: 'amount', + * type: 'uint256', + * }, + * ], + * outputs: [{ type: 'bool' }], + * }) * + * abiItem + * //^? * * * * ``` * - * @example - * ### Extracting by Selector + * @param abiItem - The JSON ABI Item to parse. + * @param options - Parsing options. + * @returns The typed ABI Item. + */ +export function fromJson( + abiItem: abiItem | AbiItem, + options?: fromJson.Options, +): fromJson.ReturnType { + const { prepare = true } = options ?? {} + return { + ...(abiItem as AbiItem), + ...(prepare ? { hash: getSignatureHash(abiItem as AbiItem) } : {}), + } as never +} + +export declare namespace fromJson { + type Options = { + /** + * Whether or not to prepare the extracted item (optimization for encoding performance). + * When `true`, the `hash` property is computed and included in the returned value. + * + * @default true + */ + prepare?: boolean | undefined + } + + type ReturnType = abiItem + + type ErrorType = Errors.GlobalErrorType +} + +/** + * Internal dispatcher used by shorthand `decode`/`encode` overloads on `AbiFunction`, + * `AbiEvent`, and `AbiError` that accept `(abi, name | selector)`. Picks `fromAbiName` + * or `fromAbiSelector` based on whether `name` parses as a hex selector. * - * ABI Items can be extract by their selector when {@link ox#Hex.Hex} is provided to `name`. + * @internal + */ +export function fromAbi< + const abi extends Abi.Abi | readonly unknown[], + name extends Name, + const args extends internal.ExtractArgs | undefined = undefined, + // + allNames = Name, +>( + abi: abi | Abi.Abi | readonly unknown[], + name: Hex.Hex | (name extends allNames ? name : never), + options?: fromAbi.Options, +): fromAbi.ReturnType { + if (Hex.validate(name as string, { strict: false })) + return fromAbiSelector(abi, name as Hex.Hex, options as never) as never + return fromAbiName(abi, name as never, options as never) as never +} + +export declare namespace fromAbi { + /** @internal */ + type Options< + abi extends Abi.Abi | readonly unknown[] = Abi.Abi, + name extends Name = Name, + args extends + | internal.ExtractArgs + | undefined = internal.ExtractArgs, + /// + allArgs = internal.ExtractArgs, + > = fromAbiName.Options + + /** @internal */ + type ReturnType< + abi extends Abi.Abi | readonly unknown[] = Abi.Abi, + name extends Name = Name, + args extends + | internal.ExtractArgs + | undefined = internal.ExtractArgs, + fallback = AbiItem, + > = fromAbiName.ReturnType + + /** @internal */ + type ErrorType = Errors.GlobalErrorType +} + +/** + * Extracts an {@link ox#AbiItem.AbiItem} from an {@link ox#Abi.Abi} given a name and optional arguments. * + * @example * ```ts twoslash * import { Abi, AbiItem } from 'ox' * @@ -296,15 +355,9 @@ export declare namespace from { * 'event Transfer(address owner, address to, uint256 tokenId)', * 'function bar(string a) returns (uint256 x)', * ]) - * const item = AbiItem.fromAbi(abi, '0x095ea7b3') // [!code focus] - * // ^? - * - * - * - * - * - * * + * const item = AbiItem.fromAbiName(abi, 'Transfer') + * // ^? * * * @@ -313,19 +366,12 @@ export declare namespace from { * * ``` * - * :::note - * - * Extracting via a hex selector is useful when extracting an ABI Item from an `eth_call` RPC response, - * a Transaction `input`, or from Event Log `topics`. - * - * ::: - * * @param abi - The ABI to extract from. - * @param name - The name (or selector) of the ABI item to extract. + * @param name - The name of the ABI item to extract. * @param options - Extraction options. * @returns The ABI item. */ -export function fromAbi< +export function fromAbiName< const abi extends Abi.Abi | readonly unknown[], name extends Name, const args extends internal.ExtractArgs | undefined = undefined, @@ -333,31 +379,11 @@ export function fromAbi< allNames = Name, >( abi: abi | Abi.Abi | readonly unknown[], - name: Hex.Hex | (name extends allNames ? name : never), - options?: fromAbi.Options, -): fromAbi.ReturnType { + name: name extends allNames ? name : never, + options?: fromAbiName.Options, +): fromAbiName.ReturnType { const { args = [], prepare = true } = (options ?? - {}) as unknown as fromAbi.Options - - const isSelector = Hex.validate(name, { strict: false }) - - // Selector lookups are always unique on the ABI: precompute the - // function/error 4-byte selector once and short-circuit via `find` - // instead of building a full `filter` result we'd discard. - if (isSelector) { - const selector = Hex.slice(name, 0, 4) - const abiItem = (abi as Abi.Abi).find((abiItem) => { - if (abiItem.type === 'function' || abiItem.type === 'error') - return getSelector(abiItem) === selector - if (abiItem.type === 'event') return getSignatureHash(abiItem) === name - return false - }) - if (!abiItem) throw new NotFoundError({ name: name as string }) - return { - ...abiItem, - ...(prepare ? { hash: getSignatureHash(abiItem) } : {}), - } as never - } + {}) as unknown as fromAbiName.Options const abiItems = (abi as Abi.Abi).filter( (abiItem) => 'name' in abiItem && abiItem.name === name, @@ -390,7 +416,6 @@ export function fromAbi< return internal.isArgOfType(arg, abiParameter) }) if (matched) { - // Check for ambiguity against already matched parameters (e.g. `address` vs `bytes20`). if ( matchedAbiItem && 'inputs' in matchedAbiItem && @@ -431,7 +456,7 @@ export function fromAbi< } as never } -export declare namespace fromAbi { +export declare namespace fromAbiName { type Options< abi extends Abi.Abi | readonly unknown[] = Abi.Abi, name extends Name = Name, @@ -452,8 +477,7 @@ export declare namespace fromAbi { readonly [] extends allArgs ? { args?: - | allArgs // show all options - // infer value, widen inferred value of `args` conditionally to match `allArgs` + | allArgs | (abi extends Abi.Abi ? args extends allArgs ? internal.Widen @@ -463,8 +487,8 @@ export declare namespace fromAbi { } : { args?: - | allArgs // show all options - | (internal.Widen & (args extends allArgs ? unknown : never)) // infer value, widen inferred value of `args` match `allArgs` (e.g. avoid union `args: readonly [123n] | readonly [bigint]`) + | allArgs + | (internal.Widen & (args extends allArgs ? unknown : never)) | undefined } > @@ -491,6 +515,84 @@ export declare namespace fromAbi { type ErrorType = Errors.GlobalErrorType } +/** + * Extracts an {@link ox#AbiItem.AbiItem} from an {@link ox#Abi.Abi} by 4-byte selector (or event topic hash). + * + * @example + * ```ts twoslash + * import { Abi, AbiItem } from 'ox' + * + * const abi = Abi.from([ + * 'function foo()', + * 'event Transfer(address owner, address to, uint256 tokenId)', + * 'function bar(string a) returns (uint256 x)', + * ]) + * const item = AbiItem.fromAbiSelector(abi, '0x095ea7b3') + * // ^? + * + * + * + * + * + * + * + * + * + * + * + * + * + * ``` + * + * @remarks Selectors for functions/errors are the first 4 bytes; event topic hashes are 32 bytes. + * Either may be passed directly (or as full calldata for functions/errors — the first 4 bytes are sliced). + * + * @param abi - The ABI to extract from. + * @param selector - The 4-byte selector (or event topic hash, or full calldata) to look up. + * @param options - Extraction options. + * @returns The ABI item. + */ +export function fromAbiSelector( + abi: abi | Abi.Abi | readonly unknown[], + selector: Hex.Hex, + options?: fromAbiSelector.Options, +): fromAbiSelector.ReturnType { + const { prepare = true } = options ?? {} + const selector_ = Hex.slice(selector, 0, 4) + const abiItem = (abi as Abi.Abi).find((abiItem) => { + if (abiItem.type === 'function' || abiItem.type === 'error') + return getSelector(abiItem) === selector_ + if (abiItem.type === 'event') return getSignatureHash(abiItem) === selector + return false + }) + if (!abiItem) throw new NotFoundError({ name: selector }) + return { + ...abiItem, + ...(prepare ? { hash: getSignatureHash(abiItem) } : {}), + } as never +} + +export declare namespace fromAbiSelector { + type Options = { + /** + * Whether or not to prepare the extracted item (optimization for encoding performance). + * When `true`, the `hash` property is computed and included in the returned value. + * + * @default true + */ + prepare?: boolean | undefined + } + + type ReturnType = + abi extends Abi.Abi + ? Abi.Abi extends abi + ? AbiItem + : abi[number] + : AbiItem + + type ErrorType = Errors.GlobalErrorType +} + /** * Computes the [4-byte selector](https://solidity-by-example.org/function-selector/) for an {@link ox#AbiItem.AbiItem}. * @@ -546,7 +648,7 @@ export function getSelector( const abiItem = (() => { if (Array.isArray(parameters[0])) { const [abi, name] = parameters as [Abi.Abi | readonly unknown[], string] - return fromAbi(abi, name) + return fromAbiName(abi, name as never) } return parameters[0] as string | AbiItem })() @@ -613,7 +715,7 @@ export function getSignature( const abiItem = (() => { if (Array.isArray(parameters[0])) { const [abi, name] = parameters as [Abi.Abi | readonly unknown[], string] - return fromAbi(abi, name) + return fromAbiName(abi, name as never) } return parameters[0] as string | AbiItem })() @@ -687,7 +789,7 @@ export function getSignatureHash( const abiItem = (() => { if (Array.isArray(parameters[0])) { const [abi, name] = parameters as [Abi.Abi | readonly unknown[], string] - return fromAbi(abi, name) + return fromAbiName(abi, name as never) } return parameters[0] as string | AbiItem })() @@ -712,7 +814,7 @@ export declare namespace getSignatureHash { * import { Abi, AbiFunction } from 'ox' * * const foo = Abi.from(['function foo(address)', 'function foo(bytes20)']) - * AbiFunction.fromAbi(foo, 'foo', { + * AbiFunction.fromAbiName(foo, 'foo', { * args: ['0xA0Cf798816D4b9b9866b5330EEa46a18382f251e'], * }) * // @error: AbiItem.AmbiguityError: Found ambiguous types in overloaded ABI Items. @@ -733,7 +835,7 @@ export declare namespace getSignatureHash { * 'function foo(address)', * 'function foo(bytes20)' // [!code --] * ]) - * AbiFunction.fromAbi(foo, 'foo', { + * AbiFunction.fromAbiName(foo, 'foo', { * args: ['0xA0Cf798816D4b9b9866b5330EEa46a18382f251e'], * }) * // @error: AbiItem.AmbiguityError: Found ambiguous types in overloaded ABI Items. @@ -774,7 +876,7 @@ export class AmbiguityError extends Errors.BaseError { * 'function foo(address)', * 'function bar(uint)' * ]) - * AbiFunction.fromAbi(foo, 'baz') + * AbiFunction.fromAbiName(foo, 'baz') * // @error: AbiItem.NotFoundError: ABI function with name "baz" not found. * ``` * @@ -791,7 +893,7 @@ export class AmbiguityError extends Errors.BaseError { * 'function bar(uint)', * 'function baz(bool)' // [!code ++] * ]) - * AbiFunction.fromAbi(foo, 'baz') + * AbiFunction.fromAbiName(foo, 'baz') * ``` */ export class NotFoundError extends Errors.BaseError { @@ -825,7 +927,7 @@ export class NotFoundError extends Errors.BaseError { * 'function foo(address)', * 'function bar(uint)' * ]) - * AbiFunction.fromAbi(foo, '0xaaa') + * AbiFunction.fromAbiSelector(foo, '0xaaa') * // @error: AbiItem.InvalidSelectorSizeError: Selector size is invalid. Expected 4 bytes. Received 2 bytes ("0xaaa"). * ``` * @@ -841,7 +943,7 @@ export class NotFoundError extends Errors.BaseError { * 'function foo(address)', * 'function bar(uint)' * ]) - * AbiFunction.fromAbi(foo, '0x7af82b1a') + * AbiFunction.fromAbiSelector(foo, '0x7af82b1a') * ``` */ export class InvalidSelectorSizeError extends Errors.BaseError { diff --git a/src/core/_test/Abi.test.ts b/src/core/_test/Abi.test.ts index 5b2e9386..4741c3c8 100644 --- a/src/core/_test/Abi.test.ts +++ b/src/core/_test/Abi.test.ts @@ -57,29 +57,30 @@ describe('from', () => { }, ]) expect(abi).toMatchInlineSnapshot(` - [ - { - "inputs": [ - { - "name": "spender", - "type": "address", - }, - { - "name": "amount", - "type": "uint256", - }, - ], - "name": "approve", - "outputs": [ - { - "type": "bool", - }, - ], - "stateMutability": "nonpayable", - "type": "function", - }, - ] - `) + [ + { + "hash": "0x095ea7b334ae44009aa867bfb386f5c3b4b443ac6f0ee573fa91c4608fbadfba", + "inputs": [ + { + "name": "spender", + "type": "address", + }, + { + "name": "amount", + "type": "uint256", + }, + ], + "name": "approve", + "outputs": [ + { + "type": "bool", + }, + ], + "stateMutability": "nonpayable", + "type": "function", + }, + ] + `) } { @@ -87,29 +88,30 @@ describe('from', () => { 'function approve(address spender, uint256 amount) returns (bool)', ]) expect(abi).toMatchInlineSnapshot(` - [ - { - "inputs": [ - { - "name": "spender", - "type": "address", - }, - { - "name": "amount", - "type": "uint256", - }, - ], - "name": "approve", - "outputs": [ - { - "type": "bool", - }, - ], - "stateMutability": "nonpayable", - "type": "function", - }, - ] - `) + [ + { + "hash": "0x095ea7b334ae44009aa867bfb386f5c3b4b443ac6f0ee573fa91c4608fbadfba", + "inputs": [ + { + "name": "spender", + "type": "address", + }, + { + "name": "amount", + "type": "uint256", + }, + ], + "name": "approve", + "outputs": [ + { + "type": "bool", + }, + ], + "stateMutability": "nonpayable", + "type": "function", + }, + ] + `) } }) }) diff --git a/src/core/_test/AbiConstructor.test.ts b/src/core/_test/AbiConstructor.test.ts index 0956728d..ba9e987e 100644 --- a/src/core/_test/AbiConstructor.test.ts +++ b/src/core/_test/AbiConstructor.test.ts @@ -327,6 +327,8 @@ test('exports', () => { "decode", "encode", "format", + "fromHumanReadable", + "fromJson", "from", "fromAbi", "BytecodeMismatchError", diff --git a/src/core/_test/AbiError.test.ts b/src/core/_test/AbiError.test.ts index 2a301d66..a1bc56d1 100644 --- a/src/core/_test/AbiError.test.ts +++ b/src/core/_test/AbiError.test.ts @@ -113,21 +113,21 @@ describe('decode', () => { }) expect(result).toMatchInlineSnapshot(` - [ - { - "hash": "0x08c379a0afcc32b1a39302f7cb8073359698411ab5fd6e3edb2c02c0b5fba8aa", - "inputs": [ - { - "name": "message", - "type": "string", - }, - ], - "name": "Error", - "type": "error", - }, - "This is a revert message", - ] - `) + [ + { + "hash": "0x08c379a0afcc32b1a39302f7cb8073359698411ab5fd6e3edb2c02c0b5fba8aa", + "inputs": [ + { + "name": "message", + "type": "string", + }, + ], + "name": "Error", + "type": "error", + }, + "This is a revert message", + ] + `) }) test('solidity `Panic` (assert)', async () => { @@ -151,22 +151,22 @@ describe('decode', () => { }) expect(result).toMatchInlineSnapshot(` - [ - { - "hash": "0x25fba4f52ccf6f699393995e0f649c9072d088cffa43609495ffad9216c5b1dc", - "inputs": [ - { - "name": "reason", - "type": "uint8", - }, - ], - "name": "Panic", - "type": "error", - }, - 1, - "An \`assert\` condition failed.", - ] - `) + [ + { + "hash": "0x25fba4f52ccf6f699393995e0f649c9072d088cffa43609495ffad9216c5b1dc", + "inputs": [ + { + "name": "reason", + "type": "uint8", + }, + ], + "name": "Panic", + "type": "error", + }, + 1, + "An \`assert\` condition failed.", + ] + `) }) test('solidity `Panic` (overflow)', async () => { @@ -190,22 +190,22 @@ describe('decode', () => { }) expect(result).toMatchInlineSnapshot(` - [ - { - "hash": "0x25fba4f52ccf6f699393995e0f649c9072d088cffa43609495ffad9216c5b1dc", - "inputs": [ - { - "name": "reason", - "type": "uint8", - }, - ], - "name": "Panic", - "type": "error", - }, - 17, - "Arithmetic operation resulted in underflow or overflow.", - ] - `) + [ + { + "hash": "0x25fba4f52ccf6f699393995e0f649c9072d088cffa43609495ffad9216c5b1dc", + "inputs": [ + { + "name": "reason", + "type": "uint8", + }, + ], + "name": "Panic", + "type": "error", + }, + 17, + "Arithmetic operation resulted in underflow or overflow.", + ] + `) }) test('custom error', async () => { @@ -639,28 +639,29 @@ describe('fromAbi', () => { ]) const item = AbiError.fromAbi(abi, 'Foo') expect(item).toMatchInlineSnapshot(` - { - "hash": "0xefc9afd358f1472682cf8cc82e1d3ae36be2538ed858a4a604119399d6f22b48", - "inputs": [ - { - "type": "bytes", - }, - ], - "name": "Foo", - "overloads": [ - { - "inputs": [ - { - "type": "uint256", - }, - ], - "name": "Foo", - "type": "error", - }, - ], - "type": "error", - } - `) + { + "hash": "0xefc9afd358f1472682cf8cc82e1d3ae36be2538ed858a4a604119399d6f22b48", + "inputs": [ + { + "type": "bytes", + }, + ], + "name": "Foo", + "overloads": [ + { + "hash": "0x1176bd96090075e8a903f0c486668395688fc8c045fd7d1d173b9852e4613ca1", + "inputs": [ + { + "type": "uint256", + }, + ], + "name": "Foo", + "type": "error", + }, + ], + "type": "error", + } + `) }) test('behavior: overloads: no inputs', () => { @@ -974,7 +975,11 @@ test('exports', () => { "encode", "format", "from", + "fromHumanReadable", + "fromJson", "fromAbi", + "fromAbiName", + "fromAbiSelector", "getSelector", "panicReasons", "solidityError", diff --git a/src/core/_test/AbiEvent.test.ts b/src/core/_test/AbiEvent.test.ts index 4d1ee94f..b25f4f65 100644 --- a/src/core/_test/AbiEvent.test.ts +++ b/src/core/_test/AbiEvent.test.ts @@ -1287,30 +1287,31 @@ describe('fromAbi', () => { ]) const item = AbiEvent.fromAbi(abi, 'Foo') expect(item).toMatchInlineSnapshot(` - { - "hash": "0xe773a60b784586770a963a70fa6ba2bdf31c462939b6ba36852ed45f5f722358", - "inputs": [ - { - "indexed": true, - "type": "address", - }, - ], - "name": "Foo", - "overloads": [ - { - "inputs": [ - { - "indexed": true, - "type": "uint256", - }, - ], - "name": "Foo", - "type": "event", - }, - ], - "type": "event", - } - `) + { + "hash": "0xe773a60b784586770a963a70fa6ba2bdf31c462939b6ba36852ed45f5f722358", + "inputs": [ + { + "indexed": true, + "type": "address", + }, + ], + "name": "Foo", + "overloads": [ + { + "hash": "0x1176bd96090075e8a903f0c486668395688fc8c045fd7d1d173b9852e4613ca1", + "inputs": [ + { + "indexed": true, + "type": "uint256", + }, + ], + "name": "Foo", + "type": "event", + }, + ], + "type": "event", + } + `) }) test('behavior: overloads: no inputs', () => { @@ -1544,7 +1545,11 @@ test('exports', () => { "encode", "format", "from", + "fromHumanReadable", + "fromJson", "fromAbi", + "fromAbiName", + "fromAbiSelector", "getSelector", "ArgsMismatchError", "InputNotFoundError", diff --git a/src/core/_test/AbiFunction.test.ts b/src/core/_test/AbiFunction.test.ts index bb11cd16..940e28fd 100644 --- a/src/core/_test/AbiFunction.test.ts +++ b/src/core/_test/AbiFunction.test.ts @@ -618,32 +618,33 @@ describe('fromAbi', () => { ]) const item = AbiFunction.fromAbi(abi, 'foo') expect(item).toMatchInlineSnapshot(` - { - "hash": "0x30c8d1da93067416f4fed4bc024d665b120d7271f9d1000c7632a48d39765324", - "inputs": [ - { - "type": "bytes", - }, - ], - "name": "foo", - "outputs": [], - "overloads": [ - { - "inputs": [ - { - "type": "uint256", - }, - ], - "name": "foo", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function", - }, - ], - "stateMutability": "nonpayable", - "type": "function", - } - `) + { + "hash": "0x30c8d1da93067416f4fed4bc024d665b120d7271f9d1000c7632a48d39765324", + "inputs": [ + { + "type": "bytes", + }, + ], + "name": "foo", + "outputs": [], + "overloads": [ + { + "hash": "0x2fbebd3821c4e005fbe0a9002cc1bd25dc266d788dba1dbcb39cc66a07e7b38b", + "inputs": [ + { + "type": "uint256", + }, + ], + "name": "foo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + ], + "stateMutability": "nonpayable", + "type": "function", + } + `) }) test('behavior: overloads: no inputs', () => { @@ -1221,7 +1222,11 @@ test('exports', () => { "encodeResult", "format", "from", + "fromHumanReadable", + "fromJson", "fromAbi", + "fromAbiName", + "fromAbiSelector", "getSelector", ] `) diff --git a/src/core/_test/AbiItem.test.ts b/src/core/_test/AbiItem.test.ts index f4b22a6d..08051c7c 100644 --- a/src/core/_test/AbiItem.test.ts +++ b/src/core/_test/AbiItem.test.ts @@ -372,6 +372,7 @@ describe('fromAbi', () => { "outputs": [], "overloads": [ { + "hash": "0x2fbebd3821c4e005fbe0a9002cc1bd25dc266d788dba1dbcb39cc66a07e7b38b", "inputs": [ { "type": "uint256", @@ -2112,8 +2113,12 @@ test('exports', () => { expect(Object.keys(AbiItem)).toMatchInlineSnapshot(` [ "format", + "fromHumanReadable", "from", + "fromJson", "fromAbi", + "fromAbiName", + "fromAbiSelector", "getSelector", "getSignature", "getSignatureHash",