UNPKG

ox

Version:

Ethereum Standard Library

558 lines 14.6 kB
import * as abitype from 'abitype'; import * as Errors from './Errors.js'; import * as Hash from './Hash.js'; import * as Hex from './Hex.js'; import * as internal from './internal/abiItem.js'; /** * Formats an {@link ox#AbiItem.AbiItem} into a **Human Readable ABI Item**. * * @example * ```ts twoslash * import { AbiItem } from 'ox' * * const formatted = AbiItem.format({ * type: 'function', * name: 'approve', * stateMutability: 'nonpayable', * inputs: [ * { * name: 'spender', * type: 'address', * }, * { * name: 'amount', * type: 'uint256', * }, * ], * outputs: [{ type: 'bool' }], * }) * * formatted * // ^? * * * ``` * * @param abiItem - The ABI Item to format. * @returns The formatted ABI Item . */ export function format(abiItem) { return abitype.formatAbiItem(abiItem); } /** * Parses an arbitrary **JSON ABI Item** or **Human Readable ABI Item** 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] * ) * * abiItem * //^? * * * * * * * * * * * * * * ``` * * @example * It is possible to specify `struct`s along with your definitions: * * ```ts twoslash * import { AbiItem } from 'ox' * * const abiItem = AbiItem.from([ * 'struct Foo { address spender; uint256 amount; }', // [!code hl] * 'function approve(Foo foo) returns (bool)', * ]) * * abiItem * //^? * * * * * * * * * * * * * ``` * * * * @param abiItem - The ABI Item to parse. * @returns The typed ABI Item. */ export function from(abiItem, options = {}) { const { prepare = true } = options; const item = (() => { if (Array.isArray(abiItem)) return abitype.parseAbiItem(abiItem); if (typeof abiItem === 'string') return abitype.parseAbiItem(abiItem); return abiItem; })(); return { ...item, ...(prepare ? { hash: getSignatureHash(item) } : {}), }; } /** * Extracts an {@link ox#AbiItem.AbiItem} from an {@link ox#Abi.Abi} given a name and optional arguments. * * @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] * // ^? * * * * * * * ``` * * @example * ### Extracting by Selector * * ABI Items can be extract by their selector when {@link ox#Hex.Hex} is provided to `name`. * * ```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, '0x095ea7b3') // [!code focus] * // ^? * * * * * * * * * * * * * * ``` * * :::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 options - Extraction options. * @returns The ABI item. */ export function fromAbi(abi, name, options) { const { args = [], prepare = true } = (options ?? {}); const isSelector = Hex.validate(name, { strict: false }); const abiItems = abi.filter((abiItem) => { if (isSelector) { if (abiItem.type === 'function' || abiItem.type === 'error') return getSelector(abiItem) === Hex.slice(name, 0, 4); if (abiItem.type === 'event') return getSignatureHash(abiItem) === name; return false; } return 'name' in abiItem && abiItem.name === name; }); if (abiItems.length === 0) throw new NotFoundError({ name: name }); if (abiItems.length === 1) return { ...abiItems[0], ...(prepare ? { hash: getSignatureHash(abiItems[0]) } : {}), }; let matchedAbiItem = undefined; for (const abiItem of abiItems) { if (!('inputs' in abiItem)) continue; if (!args || args.length === 0) { if (!abiItem.inputs || abiItem.inputs.length === 0) return { ...abiItem, ...(prepare ? { hash: getSignatureHash(abiItem) } : {}), }; continue; } if (!abiItem.inputs) continue; if (abiItem.inputs.length === 0) continue; if (abiItem.inputs.length !== args.length) continue; const matched = args.every((arg, index) => { const abiParameter = 'inputs' in abiItem && abiItem.inputs[index]; if (!abiParameter) return false; return internal.isArgOfType(arg, abiParameter); }); if (matched) { // Check for ambiguity against already matched parameters (e.g. `address` vs `bytes20`). if (matchedAbiItem && 'inputs' in matchedAbiItem && matchedAbiItem.inputs) { const ambiguousTypes = internal.getAmbiguousTypes(abiItem.inputs, matchedAbiItem.inputs, args); if (ambiguousTypes) throw new AmbiguityError({ abiItem, type: ambiguousTypes[0], }, { abiItem: matchedAbiItem, type: ambiguousTypes[1], }); } matchedAbiItem = abiItem; } } const abiItem = (() => { if (matchedAbiItem) return matchedAbiItem; const [abiItem, ...overloads] = abiItems; return { ...abiItem, overloads }; })(); if (!abiItem) throw new NotFoundError({ name: name }); return { ...abiItem, ...(prepare ? { hash: getSignatureHash(abiItem) } : {}), }; } /** * Computes the [4-byte selector](https://solidity-by-example.org/function-selector/) for an {@link ox#AbiItem.AbiItem}. * * Useful for computing function selectors for calldata. * * @example * ```ts twoslash * import { AbiItem } from 'ox' * * const selector = AbiItem.getSelector('function ownerOf(uint256 tokenId)') * // @log: '0x6352211e' * ``` * * @example * ```ts twoslash * import { AbiItem } from 'ox' * * const selector = AbiItem.getSelector({ * inputs: [{ type: 'uint256' }], * name: 'ownerOf', * outputs: [], * stateMutability: 'view', * type: 'function' * }) * // @log: '0x6352211e' * ``` * * @param abiItem - The ABI item to compute the selector for. Can be a signature or an ABI item for an error, event, function, etc. * @returns The first 4 bytes of the {@link ox#Hash.(keccak256:function)} hash of the function signature. */ export function getSelector(abiItem) { return Hex.slice(getSignatureHash(abiItem), 0, 4); } /** * Computes the stringified signature for a given {@link ox#AbiItem.AbiItem}. * * @example * ```ts twoslash * import { AbiItem } from 'ox' * * const signature = AbiItem.getSignature('function ownerOf(uint256 tokenId)') * // @log: 'ownerOf(uint256)' * ``` * * @example * ```ts twoslash * import { AbiItem } from 'ox' * * const signature = AbiItem.getSignature({ * name: 'ownerOf', * type: 'function', * inputs: [{ name: 'tokenId', type: 'uint256' }], * outputs: [], * stateMutability: 'view', * }) * // @log: 'ownerOf(uint256)' * ``` * * @param abiItem - The ABI Item to compute the signature for. * @returns The stringified signature of the ABI Item. */ export function getSignature(abiItem) { const signature = (() => { if (typeof abiItem === 'string') return abiItem; return abitype.formatAbiItem(abiItem); })(); return internal.normalizeSignature(signature); } /** * Computes the signature hash for an {@link ox#AbiItem.AbiItem}. * * Useful for computing Event Topic values. * * @example * ```ts twoslash * import { AbiItem } from 'ox' * * const hash = AbiItem.getSignatureHash('event Transfer(address indexed from, address indexed to, uint256 amount)') * // @log: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' * ``` * * @example * ```ts twoslash * import { AbiItem } from 'ox' * * const hash = AbiItem.getSignatureHash({ * name: 'Transfer', * type: 'event', * inputs: [ * { name: 'from', type: 'address', indexed: true }, * { name: 'to', type: 'address', indexed: true }, * { name: 'amount', type: 'uint256', indexed: false }, * ], * }) * // @log: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' * ``` * * @param abiItem - The ABI Item to compute the signature hash for. * @returns The {@link ox#Hash.(keccak256:function)} hash of the ABI item's signature. */ export function getSignatureHash(abiItem) { if (typeof abiItem !== 'string' && 'hash' in abiItem && abiItem.hash) return abiItem.hash; return Hash.keccak256(Hex.fromString(getSignature(abiItem))); } /** * Throws when ambiguous types are found on overloaded ABI items. * * @example * ```ts twoslash * import { Abi, AbiFunction } from 'ox' * * const foo = Abi.from(['function foo(address)', 'function foo(bytes20)']) * AbiFunction.fromAbi(foo, 'foo', { * args: ['0xA0Cf798816D4b9b9866b5330EEa46a18382f251e'], * }) * // @error: AbiItem.AmbiguityError: Found ambiguous types in overloaded ABI Items. * // @error: `bytes20` in `foo(bytes20)`, and * // @error: `address` in `foo(address)` * // @error: These types encode differently and cannot be distinguished at runtime. * // @error: Remove one of the ambiguous items in the ABI. * ``` * * ### Solution * * Remove one of the ambiguous types from the ABI. * * ```ts twoslash * import { Abi, AbiFunction } from 'ox' * * const foo = Abi.from([ * 'function foo(address)', * 'function foo(bytes20)' // [!code --] * ]) * AbiFunction.fromAbi(foo, 'foo', { * args: ['0xA0Cf798816D4b9b9866b5330EEa46a18382f251e'], * }) * // @error: AbiItem.AmbiguityError: Found ambiguous types in overloaded ABI Items. * // @error: `bytes20` in `foo(bytes20)`, and * // @error: `address` in `foo(address)` * // @error: These types encode differently and cannot be distinguished at runtime. * // @error: Remove one of the ambiguous items in the ABI. * ``` */ export class AmbiguityError extends Errors.BaseError { constructor(x, y) { super('Found ambiguous types in overloaded ABI Items.', { metaMessages: [ // TODO: abitype to add support for signature-formatted ABI items. `\`${x.type}\` in \`${internal.normalizeSignature(abitype.formatAbiItem(x.abiItem))}\`, and`, `\`${y.type}\` in \`${internal.normalizeSignature(abitype.formatAbiItem(y.abiItem))}\``, '', 'These types encode differently and cannot be distinguished at runtime.', 'Remove one of the ambiguous items in the ABI.', ], }); Object.defineProperty(this, "name", { enumerable: true, configurable: true, writable: true, value: 'AbiItem.AmbiguityError' }); } } /** * Throws when an ABI item is not found in the ABI. * * @example * ```ts twoslash * // @noErrors * import { Abi, AbiFunction } from 'ox' * * const foo = Abi.from([ * 'function foo(address)', * 'function bar(uint)' * ]) * AbiFunction.fromAbi(foo, 'baz') * // @error: AbiItem.NotFoundError: ABI function with name "baz" not found. * ``` * * ### Solution * * Ensure the ABI item exists on the ABI. * * ```ts twoslash * // @noErrors * import { Abi, AbiFunction } from 'ox' * * const foo = Abi.from([ * 'function foo(address)', * 'function bar(uint)', * 'function baz(bool)' // [!code ++] * ]) * AbiFunction.fromAbi(foo, 'baz') * ``` */ export class NotFoundError extends Errors.BaseError { constructor({ name, data, type = 'item', }) { const selector = (() => { if (name) return ` with name "${name}"`; if (data) return ` with data "${data}"`; return ''; })(); super(`ABI ${type}${selector} not found.`); Object.defineProperty(this, "name", { enumerable: true, configurable: true, writable: true, value: 'AbiItem.NotFoundError' }); } } /** * Throws when the selector size is invalid. * * @example * ```ts twoslash * import { Abi, AbiFunction } from 'ox' * * const foo = Abi.from([ * 'function foo(address)', * 'function bar(uint)' * ]) * AbiFunction.fromAbi(foo, '0xaaa') * // @error: AbiItem.InvalidSelectorSizeError: Selector size is invalid. Expected 4 bytes. Received 2 bytes ("0xaaa"). * ``` * * ### Solution * * Ensure the selector size is 4 bytes. * * ```ts twoslash * // @noErrors * import { Abi, AbiFunction } from 'ox' * * const foo = Abi.from([ * 'function foo(address)', * 'function bar(uint)' * ]) * AbiFunction.fromAbi(foo, '0x7af82b1a') * ``` */ export class InvalidSelectorSizeError extends Errors.BaseError { constructor({ data }) { super(`Selector size is invalid. Expected 4 bytes. Received ${Hex.size(data)} bytes ("${data}").`); Object.defineProperty(this, "name", { enumerable: true, configurable: true, writable: true, value: 'AbiItem.InvalidSelectorSizeError' }); } } //# sourceMappingURL=AbiItem.js.map