UNPKG

ox

Version:

Ethereum Standard Library

1,660 lines (1,579 loc) 59.3 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 * as Address from './Address.js' import * as Bytes from './Bytes.js' import * as Errors from './Errors.js' import * as Hash from './Hash.js' import * as Hex from './Hex.js' import type * as internal from './internal/abiEvent.js' import type * as AbiItem_internal from './internal/abiItem.js' import * as Cursor from './internal/cursor.js' import { prettyPrint } from './internal/errors.js' import * as formatAbiItem from './internal/human-readable/formatAbiItem.js' import type { Compute, IsNarrowable } from './internal/types.js' /** Root type for an {@link ox#AbiItem.AbiItem} with an `event` type. */ export type AbiEvent = abitype.AbiEvent & { hash?: Hex.Hex | undefined overloads?: readonly AbiEvent[] | undefined } /** * Module-scope regex for matching an array suffix on an indexed event * parameter type (e.g. `uint256[]`, `bytes32[3]`). Hoisted out of * `decode` / `encode` so we don't recompile per call. * * @internal */ const arraySuffixRegex = /^(.*)\[(\d+)?\]$/ /** * Encodes a primitive (32-byte) indexed event topic directly to a * left-padded 32-byte hex word, skipping the `AbiParameters.encode` * → `prepareParameters` → `Hex.concat` round-trip used for general * parameters. Returns `undefined` if the parameter type isn't a * supported primitive. * * @internal */ function encodePrimitiveTopic( type: string, value: unknown, ): Hex.Hex | undefined { if (type === 'address') { Address.assert(value as Address.Address, { strict: false }) return `0x000000000000000000000000${(value as string).slice(2).toLowerCase()}` as Hex.Hex } if (type === 'bool') { if (typeof value !== 'boolean') return undefined return value ? '0x0000000000000000000000000000000000000000000000000000000000000001' : '0x0000000000000000000000000000000000000000000000000000000000000000' } // `uintN` / `intN` (covers all 8-bit-aligned widths up to 256). if (type.startsWith('uint') || type.startsWith('int')) { const signed = type[0] === 'i' const sizeStr = signed ? type.slice(3) : type.slice(4) const size = sizeStr === '' ? 256 : Number(sizeStr) if (!Number.isFinite(size)) return undefined return Hex.fromNumber(value as number | bigint, { size: 32, signed }) } // `bytesN` (fixed): right-pad to 32 bytes. if (type.startsWith('bytes') && type.length > 5) { if (typeof value !== 'string') return undefined return Hex.padRight(value as Hex.Hex, 32) } return undefined } /** * Decodes a primitive (32-byte) indexed event topic directly to a * primitive value, skipping the `AbiParameters.decode` round-trip. * Returns a `[value]` tuple on hit, or `undefined` on miss. * * @internal */ function decodePrimitiveTopic( type: string, topic: Hex.Hex, options: decode.Options = {}, ): [value: unknown] | undefined { if (type === 'address') { // Trailing 20 bytes; topic is always 66 chars (`0x` + 64 hex). const address = `0x${topic.slice(26)}` as Address.Address return [ options.checksumAddress === false ? address : Address.checksum(address), ] } if (type === 'bool') { // Last byte determines truthiness (Solidity left-pads booleans). return [topic.charCodeAt(65) === 49 /* '1' */] } if (type.startsWith('uint') || type.startsWith('int')) { const signed = type[0] === 'i' const sizeStr = signed ? type.slice(3) : type.slice(4) const size = sizeStr === '' ? 256 : Number(sizeStr) if (!Number.isFinite(size)) return undefined const big = size > 48 ? Hex.toBigInt(topic, { signed }) : Hex.toNumber(topic, { signed }) return [big] } if (type.startsWith('bytes') && type.length > 5) { const sizeStr = type.slice(5) const size = Number(sizeStr) if (!Number.isFinite(size)) return undefined // Take leading `size` bytes of the 32-byte topic. return [`0x${topic.slice(2, 2 + size * 2)}` as Hex.Hex] } return undefined } /** * Extracts an {@link ox#AbiEvent.AbiEvent} item from an {@link ox#Abi.Abi}, given a name. * * @example * ```ts twoslash * import { Abi, AbiEvent } from 'ox' * * const abi = Abi.from([ * 'event Foo(string)', * 'event Bar(uint256)' * ]) * * type Foo = AbiEvent.FromAbi<typeof abi, 'Foo'> * // ^? * ``` */ export type FromAbi< abi extends Abi.Abi, name extends ExtractNames<abi>, > = abitype.ExtractAbiEvent<abi, name> /** * Extracts the names of all {@link ox#AbiError.AbiError} items in an {@link ox#Abi.Abi}. * * @example * ```ts twoslash * import { Abi, AbiEvent } from 'ox' * * const abi = Abi.from([ * 'event Foo(string)', * 'event Bar(uint256)' * ]) * * type names = AbiEvent.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.ExtractAbiEventNames<abi> /** * Asserts that the provided arguments match the decoded log arguments. * * @example * ```ts twoslash * import { AbiEvent } from 'ox' * * const abiEvent = AbiEvent.from( * 'event Transfer(address indexed from, address indexed to, uint256 value)' * ) * * const args = AbiEvent.decode(abiEvent, { * data: '0x0000000000000000000000000000000000000000000000000000000000000001', * topics: [ * '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', * '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac', * '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac' * ] * }) * * AbiEvent.assertArgs(abiEvent, args, { * from: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ad', * to: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac', * value: 1n * }) * * // @error: AbiEvent.ArgsMismatchError: Given arguments to not match the arguments decoded from the log. * // @error: Event: event Transfer(address indexed from, address indexed to, uint256 value) * // @error: Expected Arguments: * // @error: from: 0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac * // @error: to: 0xa5cc3c03994db5b0d9a5eedd10cabab0813678ad * // @error: value: 1 * // @error: Given Arguments: * // @error: from: 0xa5cc3c03994db5b0d9a5eedd10cabab0813678ad * // @error: to: 0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac * // @error: value: 1 * ``` * * @param abiEvent - ABI Event to check. * @param args - Decoded arguments. * @param matchArgs - The arguments to check. */ export function assertArgs<const abiEvent extends AbiEvent>( abiEvent: abiEvent | AbiEvent, args: unknown, matchArgs: IsNarrowable<abiEvent, AbiEvent> extends true ? abiEvent['inputs'] extends readonly [] ? never : internal.ParametersToPrimitiveTypes< abiEvent['inputs'], { EnableUnion: true; IndexedOnly: false; Required: false } > : unknown, ) { if (!args || !matchArgs) throw new ArgsMismatchError({ abiEvent, expected: args, given: matchArgs, }) function isEqual( input: abitype.AbiEventParameter, value: unknown, arg: unknown, ) { if (input.type === 'address') return Address.isEqual(value as Address.Address, arg as Address.Address) if (input.type === 'string') return ( Hash.keccak256(Bytes.fromString(value as string), { as: 'Hex' }) === arg ) if (input.type === 'bytes') { const hex = typeof value === 'string' ? (value as Hex.Hex) : Hex.fromBytes(value as Bytes.Bytes) return Hash.keccak256(hex, { as: 'Hex' }) === arg } return value === arg } if (Array.isArray(args) && Array.isArray(matchArgs)) { for (const [index, value] of matchArgs.entries()) { if (value === null || value === undefined) continue const input = abiEvent.inputs[index] if (!input) throw new InputNotFoundError({ abiEvent, name: `${index}`, }) const value_ = Array.isArray(value) ? value : [value] let equal = false for (const value of value_) { if (isEqual(input, value, args[index])) equal = true } if (!equal) throw new ArgsMismatchError({ abiEvent, expected: args, given: matchArgs, }) } } if ( typeof args === 'object' && !Array.isArray(args) && typeof matchArgs === 'object' && !Array.isArray(matchArgs) ) for (const [key, value] of Object.entries(matchArgs)) { if (value === null || value === undefined) continue const input = abiEvent.inputs.find((input) => input.name === key) if (!input) throw new InputNotFoundError({ abiEvent, name: key }) const value_ = Array.isArray(value) ? value : [value] let equal = false for (const value of value_) { if (isEqual(input, value, (args as Record<string, unknown>)[key])) equal = true } if (!equal) throw new ArgsMismatchError({ abiEvent, expected: args, given: matchArgs, }) } } export declare namespace assertArgs { type ErrorType = | Address.isEqual.ErrorType | Bytes.fromString.ErrorType | Hash.keccak256.ErrorType | ArgsMismatchError | Errors.GlobalErrorType } /** * ABI-Decodes the provided [Log Topics and Data](https://info.etherscan.com/what-is-event-logs/) according to the ABI Event's parameter types (`input`). * * :::tip * * This function is typically used to decode an [Event Log](https://info.etherscan.com/what-is-event-logs/) that may be returned from a Log Query (e.g. `eth_getLogs`) or Transaction Receipt. * * See the [End-to-end Example](#end-to-end). * * ::: * * @example * ```ts twoslash * import { AbiEvent } from 'ox' * * const transfer = AbiEvent.from( * 'event Transfer(address indexed from, address indexed to, uint256 value)' * ) * * const log = { * // ... * data: '0x0000000000000000000000000000000000000000000000000000000000000001', * topics: [ * '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', * '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac', * '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac' * ] * } as const * * const decoded = AbiEvent.decode(transfer, log) * // @log: { * // @log: from: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', * // @log: to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', * // @log: value: 1n * // @log: } * ``` * * @example * ### ABI-shorthand * * You can also specify an entire ABI object and an event name as parameters to {@link ox#AbiEvent.(decode:function)}: * * ```ts twoslash * // @noErrors * import { Abi, AbiEvent } from 'ox' * * const abi = Abi.from([...]) * const log = { * // ... * data: '0x0000000000000000000000000000000000000000000000000000000000000001', * topics: [ * '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', * '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac', * '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac', * ], * } as const * * const decoded = AbiEvent.decode( * abi, // [!code focus] * 'Transfer', // [!code focus] * log * ) * // @log: { * // @log: from: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', * // @log: to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', * // @log: value: 1n * // @log: } * ``` * * @example * ### End-to-end * * Below is an end-to-end example of using `AbiEvent.decode` to decode the topics of a `Transfer` event on the [Wagmi Mint Example contract](https://etherscan.io/address/0xfba3912ca04dd458c843e2ee08967fc04f3579c2). * * ```ts twoslash * import 'ox/window' * import { AbiEvent, Hex } from 'ox' * * // 1. Instantiate the `Transfer` ABI Event. * const transfer = AbiEvent.from( * 'event Transfer(address indexed from, address indexed to, uint256 value)' * ) * * // 2. Encode the ABI Event into Event Topics. * const { topics } = AbiEvent.encode(transfer) * * // 3. Query for events matching the encoded Topics. * const logs = await window.ethereum!.request({ * method: 'eth_getLogs', * params: [ * { * address: '0xfba3912ca04dd458c843e2ee08967fc04f3579c2', * fromBlock: Hex.fromNumber(19760235n), * toBlock: Hex.fromNumber(19760240n), * topics * } * ] * }) * * // 4. Decode the Log. // [!code focus] * const decoded = AbiEvent.decode(transfer, logs[0]!) // [!code focus] * // @log: { * // @log: from: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', * // @log: to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', * // @log: value: 603n * // @log: } * ``` * * :::note * * For simplicity, the above example uses `window.ethereum.request`, but you can use any * type of JSON-RPC interface. * * ::: * * @param abiEvent - The ABI Event to decode. * @param log - `topics` & `data` to decode. * @param options - Decoding options. * @returns The decoded event. */ export function decode< const abi extends Abi.Abi | readonly unknown[], name extends Name<abi>, const args extends AbiItem_internal.ExtractArgs<abi, name> | undefined = undefined, // abiEvent extends AbiEvent = AbiItem.fromAbi.ReturnType< abi, name, args, AbiEvent >, allNames = Name<abi>, >( abi: abi | Abi.Abi | readonly unknown[], name: Hex.Hex | (name extends allNames ? name : never), log: decode.Log, options?: decode.Options, ): decode.ReturnType<abiEvent> export function decode<const abiEvent extends AbiEvent>( abiEvent: abiEvent | AbiEvent, log: decode.Log, options?: decode.Options, ): decode.ReturnType<abiEvent> // eslint-disable-next-line jsdoc-js/require-jsdoc export function decode( ...parameters: | [ abi: Abi.Abi | readonly unknown[], name: Hex.Hex | string, log: decode.Log, options?: decode.Options | undefined, ] | [ abiEvent: AbiEvent, log: decode.Log, options?: decode.Options | undefined, ] ): decode.ReturnType { const [abiEvent, log, options] = (() => { if (Array.isArray(parameters[0])) { const [abi, name, log, options] = parameters as [ Abi.Abi | readonly unknown[], Hex.Hex | string, decode.Log, decode.Options | undefined, ] return [fromAbi(abi, name), log, options] as const } const [abiEvent, log, options] = parameters as [ AbiEvent, decode.Log, decode.Options | undefined, ] return [abiEvent, log, options] as const })() const { data, topics } = log const isAnonymous = abiEvent.anonymous === true const argTopics = isAnonymous ? [...topics] : topics.slice(1) if (!isAnonymous) { const selector_ = topics[0] const selector = getSelector(abiEvent) if (selector_ !== selector) throw new SelectorTopicMismatchError({ abiEvent, actual: selector_, expected: selector, }) } const { inputs } = abiEvent const isUnnamed = inputs?.every((x) => !('name' in x && x.name)) let args: any = isUnnamed ? [] : {} // Single-pass partition: keep both the input and its original index so we // don't have to call `inputs.indexOf(...)` per non-indexed entry below // (which is O(n^2) on event width). const indexedInputs: abitype.AbiEventParameter[] = [] const nonIndexedInputs: abitype.AbiEventParameter[] = [] const nonIndexedOriginalIndex: number[] = [] for (let i = 0; i < inputs.length; i++) { const input = inputs[i]! if ('indexed' in input && input.indexed) indexedInputs.push(input) else { nonIndexedInputs.push(input) nonIndexedOriginalIndex.push(i) } } // Decode topics (indexed args). for (let i = 0; i < indexedInputs.length; i++) { const param = indexedInputs[i]! const topic = argTopics[i] if (!topic) throw new TopicsMismatchError({ abiEvent, param: param as abitype.AbiParameter & { indexed: boolean }, }) args[isUnnamed ? i : param.name || i] = (() => { const t = param.type if ( t === 'string' || t === 'bytes' || t === 'tuple' || arraySuffixRegex.test(t) ) return topic // Fast path: 32-byte primitive (`address`, `bool`, `uintN`, // `intN`, `bytesN`). Reads the topic word directly without // allocating a `Cursor` or running `Bytes.fromHex`. const fast = decodePrimitiveTopic(t, topic, options) if (fast) return fast[0] const decoded = AbiParameters.decode([param], topic, options) || [] return decoded[0] })() } // Decode data (non-indexed args). if (nonIndexedInputs.length > 0) { if (data && data !== '0x') { try { const decodedData = AbiParameters.decode( nonIndexedInputs, data, options, ) if (decodedData) { if (isUnnamed) args = [...args, ...decodedData] else { for (let i = 0; i < nonIndexedInputs.length; i++) { const index = nonIndexedOriginalIndex[i]! args[nonIndexedInputs[i]!.name! || index] = decodedData[i] } } } } catch (err) { if ( err instanceof AbiParameters.DataSizeTooSmallError || err instanceof Cursor.PositionOutOfBoundsError ) throw new DataMismatchError({ abiEvent, data: data, parameters: nonIndexedInputs, size: Hex.size(data), }) throw err } } else { throw new DataMismatchError({ abiEvent, data: '0x', parameters: nonIndexedInputs, size: 0, }) } } return Object.values(args).length > 0 ? args : undefined } export declare namespace decode { type Log = { data?: Hex.Hex | undefined topics: readonly Hex.Hex[] } type Options = { /** * Whether decoded addresses should be checksummed. * * @default true */ checksumAddress?: boolean | undefined } type ReturnType<abiEvent extends AbiEvent = AbiEvent> = IsNarrowable<abiEvent, AbiEvent> extends true ? abiEvent['inputs'] extends readonly [] ? undefined : internal.ParametersToPrimitiveTypes< abiEvent['inputs'], { EnableUnion: false; IndexedOnly: false; Required: true } > : unknown type ErrorType = | AbiParameters.decode.ErrorType | getSelector.ErrorType | DataMismatchError | SelectorTopicMismatchError | TopicsMismatchError | Errors.GlobalErrorType } /** * Extracts an {@link ox#AbiEvent.AbiEvent} from an {@link ox#Abi.Abi} and decodes its arguments from a Log. * * @example * ```ts twoslash * import { Abi, AbiEvent } from 'ox' * * const abi = Abi.from([ * 'event Transfer(address indexed from, address indexed to, uint256 value)' * ]) * * const decoded = AbiEvent.decodeLog(abi, { * data: '0x0000000000000000000000000000000000000000000000000000000000000001', * topics: [ * '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', * '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac', * '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac' * ] * }) * // @log: { * // @log: event: { name: 'Transfer', type: 'event', ... }, * // @log: args: { * // @log: from: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', * // @log: to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', * // @log: value: 1n, * // @log: }, * // @log: } * ``` * * @param abi - The ABI to extract an event from. * @param log - `topics` & `data` to decode. * @param options - Decoding options. * @returns The decoded event and arguments. */ export function decodeLog<const abi extends Abi.Abi | readonly unknown[]>( abi: abi | Abi.Abi | readonly unknown[], log: decodeLog.Log, options?: decodeLog.Options, ): decodeLog.ReturnType<decodeLog.ExtractEvent<abi>> { const selector = log.topics[0] if (!selector) throw new SelectorTopicNotFoundError() const abiEvent = fromAbi(abi, selector) as decodeLog.ExtractEvent<abi> return { event: abiEvent, args: decode(abiEvent, log, options), } } export declare namespace decodeLog { type ExtractEvent<abi extends Abi.Abi | readonly unknown[]> = AbiItem.fromAbi.ReturnType<abi, Name<abi>, undefined, AbiEvent> type Log = decode.Log type Options = decode.Options type ReturnType<abiEvent extends AbiEvent = AbiEvent> = { event: abiEvent args: decode.ReturnType<abiEvent> } type ErrorType = | decode.ErrorType | fromAbi.ErrorType | SelectorTopicNotFoundError | Errors.GlobalErrorType } /** * Extracts and decodes Logs that match an ABI Event in an ABI. * * @example * ```ts twoslash * import { Abi, AbiEvent } from 'ox' * * const abi = Abi.from([ * 'event Transfer(address indexed from, address indexed to, uint256 value)' * ]) * * const logs = AbiEvent.extractLogs(abi, [ * { * data: '0x0000000000000000000000000000000000000000000000000000000000000001', * topics: [ * '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', * '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac', * '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac' * ] * } * ]) * // @log: [{ * // @log: eventName: 'Transfer', * // @log: args: { * // @log: from: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', * // @log: to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', * // @log: value: 1n, * // @log: }, * // @log: topics: [...], * // @log: data: '0x...', * // @log: }] * ``` * * @param abi - The ABI to extract events from. * @param logs - Logs to extract. * @param options - Extraction options. * @returns The extracted logs. */ export function extractLogs< const abi extends Abi.Abi | readonly unknown[], const logs extends readonly extractLogs.Log[], eventName extends | extractLogs.EventName<abi> | readonly extractLogs.EventName<abi>[] | undefined = undefined, strict extends boolean | undefined = true, >( abi: abi | Abi.Abi | readonly unknown[], logs: logs | readonly extractLogs.Log[], options: extractLogs.Options<abi, eventName, strict> = {}, ): extractLogs.ReturnType< extractLogs.ExtractEvent<abi, eventName>, logs[number], strict >[] { const { args, checksumAddress, strict = true, } = options as extractLogs.Options const eventName = (() => { if (!options.eventName) return undefined if (Array.isArray(options.eventName)) return options.eventName return [options.eventName as string] })() const abiEvents = (abi as Abi.Abi).filter( (item): item is AbiEvent => item.type === 'event', ) const out: extractLogs.ReturnType[] = [] for (const log of logs) { const log_ = formatExtractLog(log) const selector = log_.topics[0] if (!selector) continue const items = abiEvents.filter((item) => getSelector(item) === selector) if (items.length === 0) continue let event: | { args: unknown event: AbiEvent } | undefined for (const item of items) { try { event = { args: decodeForExtract(item, log_, { checksumAddress, strict: true, }), event: item, } break } catch {} } if (!event && !strict) { const item = items[0]! try { event = { args: decodeForExtract(item, log_, { checksumAddress, strict: false, }), event: item, } } catch { const isUnnamed = item.inputs.some((x) => !('name' in x && x.name)) event = { args: isUnnamed ? [] : {}, event: item, } } } if (!event) continue if (eventName && !eventName.includes(event.event.name)) continue if (!includesArgs(event.event, event.args, args)) continue out.push({ ...log_, args: event.args, eventName: event.event.name, }) } return out as never } export declare namespace extractLogs { type EventName<abi extends Abi.Abi | readonly unknown[]> = Name<abi> type ExtractEvent< abi extends Abi.Abi | readonly unknown[], eventName extends EventName<abi> | readonly EventName<abi>[] | undefined = EventName<abi>, > = AbiItem.fromAbi.ReturnType< abi, eventName extends readonly (infer name)[] ? name & EventName<abi> : eventName extends EventName<abi> ? eventName : EventName<abi>, undefined, AbiEvent > type Args<abiEvent extends AbiEvent = AbiEvent> = IsNarrowable<abiEvent, AbiEvent> extends true ? abiEvent['inputs'] extends readonly [] ? never : internal.ParametersToPrimitiveTypes< abiEvent['inputs'], { EnableUnion: true; IndexedOnly: false; Required: false } > : unknown type Log = decode.Log & { address?: Address.Address | undefined blockHash?: Hex.Hex | null | undefined blockNumber?: bigint | Hex.Hex | null | undefined blockTimestamp?: bigint | Hex.Hex | null | undefined logIndex?: number | Hex.Hex | null | undefined removed?: boolean | undefined transactionHash?: Hex.Hex | null | undefined transactionIndex?: number | Hex.Hex | null | undefined } type Options< abi extends Abi.Abi | readonly unknown[] = Abi.Abi, eventName extends EventName<abi> | readonly EventName<abi>[] | undefined = EventName<abi>, strict extends boolean | undefined = boolean | undefined, abiEvent extends AbiEvent = ExtractEvent<abi, eventName>, > = decode.Options & { /** Arguments to match against decoded event arguments. */ args?: Args<abiEvent> | undefined /** Event name, or event names, to extract. */ eventName?: | eventName | EventName<abi> | readonly EventName<abi>[] | undefined /** * Whether to strictly decode log topics and data. * * @default true */ strict?: strict | boolean | undefined } type ReturnType< abiEvent extends AbiEvent = AbiEvent, log extends Log = Log, strict extends boolean | undefined = boolean | undefined, > = Compute< log & { args: IsNarrowable<abiEvent, AbiEvent> extends true ? abiEvent['inputs'] extends readonly [] ? undefined : internal.ParametersToPrimitiveTypes< abiEvent['inputs'], { EnableUnion: false IndexedOnly: false Required: strict extends boolean ? strict : false } > : unknown eventName: abiEvent['name'] } > type ErrorType = | AbiParameters.decode.ErrorType | decode.ErrorType | getSelector.ErrorType | Errors.GlobalErrorType } function decodeForExtract( abiEvent: AbiEvent, log: decode.Log, options: decode.Options & { strict: boolean }, ) { const { data, topics } = log const { strict } = options const selector = topics[0] if (selector !== getSelector(abiEvent)) throw new SelectorTopicMismatchError({ abiEvent, actual: selector, expected: getSelector(abiEvent), }) const { inputs } = abiEvent const isUnnamed = inputs.some((x) => !('name' in x && x.name)) const args: any = isUnnamed ? [] : {} const argTopics = topics.slice(1) const indexedInputs: [abitype.AbiEventParameter, number][] = [] const nonIndexedInputs: abitype.AbiEventParameter[] = [] const missingIndexedInputs: [abitype.AbiEventParameter, number][] = [] for (let i = 0; i < inputs.length; i++) { const input = inputs[i]! if ('indexed' in input && input.indexed) indexedInputs.push([input, i]) else nonIndexedInputs.push(input) } for (let i = 0; i < indexedInputs.length; i++) { const [param, index] = indexedInputs[i]! const topic = argTopics[i] if (!topic) { if (strict) throw new TopicsMismatchError({ abiEvent, param: param as abitype.AbiParameter & { indexed: boolean }, }) missingIndexedInputs.push([param, index]) continue } args[isUnnamed ? index : param.name || index] = decodeTopic(param, topic, { checksumAddress: options.checksumAddress, }) } const inputsToDecode = strict ? nonIndexedInputs : [...missingIndexedInputs.map(([param]) => param), ...nonIndexedInputs] if (inputsToDecode.length > 0) { if (data && data !== '0x') { try { const decodedData = AbiParameters.decode(inputsToDecode, data, { checksumAddress: options.checksumAddress, }) if (decodedData) { let dataIndex = 0 if (!strict) { for (const [param, index] of missingIndexedInputs) { args[isUnnamed ? index : param.name || index] = decodedData[dataIndex++] } } if (isUnnamed) { for (let i = 0; i < inputs.length; i++) if (args[i] === undefined && dataIndex < decodedData.length) args[i] = decodedData[dataIndex++] } else { for (let i = 0; i < nonIndexedInputs.length; i++) { const input = nonIndexedInputs[i]! args[input.name!] = decodedData[dataIndex++] } } } } catch (err) { if (strict) { if ( err instanceof AbiParameters.DataSizeTooSmallError || err instanceof Cursor.PositionOutOfBoundsError ) throw new DataMismatchError({ abiEvent, data: data, parameters: inputsToDecode, size: Hex.size(data), }) throw err } } } else if (strict) { throw new DataMismatchError({ abiEvent, data: '0x', parameters: inputsToDecode, size: 0, }) } } return Object.values(args).length > 0 ? args : undefined } function decodeTopic( param: abitype.AbiEventParameter, topic: Hex.Hex, options: decode.Options, ) { const type = param.type if ( type === 'string' || type === 'bytes' || type === 'tuple' || arraySuffixRegex.test(type) ) return topic const fast = decodePrimitiveTopic(type, topic, options) if (fast) return fast[0] const decoded = AbiParameters.decode([param], topic, options) || [] return decoded[0] } function formatExtractLog<const log extends extractLogs.Log>(log: log) { if (typeof log.blockNumber !== 'string') return log return { ...log, blockHash: log.blockHash ? log.blockHash : null, blockNumber: log.blockNumber ? BigInt(log.blockNumber) : null, blockTimestamp: log.blockTimestamp ? BigInt(log.blockTimestamp) : log.blockTimestamp === null ? null : undefined, logIndex: log.logIndex ? Number(log.logIndex) : null, transactionHash: log.transactionHash ? log.transactionHash : null, transactionIndex: log.transactionIndex ? Number(log.transactionIndex) : null, } } function includesArgs(abiEvent: AbiEvent, args: unknown, matchArgs: unknown) { try { if (!matchArgs) return true assertArgs(abiEvent, args, matchArgs) return true } catch { return false } } /** * ABI-encodes the provided event input (`inputs`) into an array of [Event Topics](https://info.etherscan.com/what-is-event-logs/). * * :::tip * * This function is typically used to encode event arguments into [Event Topics](https://info.etherscan.com/what-is-event-logs/). * * See the [End-to-end Example](#end-to-end). * * ::: * * @example * ```ts twoslash * import { AbiEvent } from 'ox' * * const transfer = AbiEvent.from( * 'event Transfer(address indexed from, address indexed to, uint256 value)' * ) * * const { topics } = AbiEvent.encode(transfer) * // @log: ['0x406dade31f7ae4b5dbc276258c28dde5ae6d5c2773c5745802c493a2360e55e0'] * ``` * * @example * ### Passing Arguments * * You can pass `indexed` parameter values to `AbiEvent.encode`. * * TypeScript types will be inferred from the ABI Event, to guard you from inserting the wrong values. * * For example, the `Transfer` event below accepts an `address` type for the `from` and `to` attributes. * * ```ts twoslash * import { AbiEvent } from 'ox' * * const transfer = AbiEvent.from( * 'event Transfer(address indexed from, address indexed to, uint256 value)' * ) * * const { topics } = AbiEvent.encode(transfer, { * from: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', // [!code hl] * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8' // [!code hl] * }) * // @log: [ * // @log: '0x406dade31f7ae4b5dbc276258c28dde5ae6d5c2773c5745802c493a2360e55e0', * // @log: '0x00000000000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266', * // @log: '0x0000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8' * // @log: ] * ``` * * @example * ### ABI-shorthand * * You can also specify an entire ABI object and an event name as parameters to {@link ox#AbiEvent.(encode:function)}: * * ```ts twoslash * // @noErrors * import { Abi, AbiEvent } from 'ox' * * const abi = Abi.from([...]) * * const { topics } = AbiEvent.encode( * abi, // [!code focus] * 'Transfer', // [!code focus] * { * from: '0xf39fd6e51aad88f6f4ce6ab882779cfffb92266', // [!code focus] * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', * } * ) * // @log: [ * // @log: '0x406dade31f7ae4b5dbc276258c28dde5ae6d5c2773c5745802c493a2360e55e0', * // @log: '0x00000000000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266', * // @log: '0x0000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8' * // @log: ] * ``` * * @example * ### End-to-end * * Below is an end-to-end example of using `AbiEvent.encode` to encode the topics of a `Transfer` event and query for events matching the encoded topics on the [Wagmi Mint Example contract](https://etherscan.io/address/0xfba3912ca04dd458c843e2ee08967fc04f3579c2). * * ```ts twoslash * import 'ox/window' * import { AbiEvent, Hex } from 'ox' * * // 1. Instantiate the `Transfer` ABI Event. * const transfer = AbiEvent.from( * 'event Transfer(address indexed from, address indexed to, uint256 value)' * ) * * // 2. Encode the ABI Event into Event Topics. * const { topics } = AbiEvent.encode(transfer) * * // 3. Query for events matching the encoded Topics. * const logs = await window.ethereum!.request({ * method: 'eth_getLogs', * params: [ * { * address: '0xfba3912ca04dd458c843e2ee08967fc04f3579c2', * fromBlock: Hex.fromNumber(19760235n), * toBlock: Hex.fromNumber(19760240n), * topics * } * ] * }) * // @log: [ * // @log: "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", * // @log: "0x0000000000000000000000000000000000000000000000000000000000000000", * // @log: "0x0000000000000000000000000c04d9e9278ec5e4d424476d3ebec70cb5d648d1", * // @log: "0x000000000000000000000000000000000000000000000000000000000000025b", * // @log: ] * ``` * * :::note * * For simplicity, the above example uses `window.ethereum.request`, but you can use any * type of JSON-RPC interface. * * ::: * * @param abiEvent - The event to encode. * @param args - The arguments to encode. * @returns The encoded event topics. */ export function encode< const abi extends Abi.Abi | readonly unknown[], name extends Name<abi>, const args extends AbiItem_internal.ExtractArgs<abi, name> | undefined = undefined, // abiEvent extends AbiEvent = AbiItem.fromAbi.ReturnType< abi, name, args, AbiEvent >, allNames = Name<abi>, >( abi: abi | Abi.Abi | readonly unknown[], name: Hex.Hex | (name extends allNames ? name : never), ...[args]: encode.Args<abiEvent> ): encode.ReturnType export function encode<const abiEvent extends AbiEvent>( abiEvent: abiEvent | AbiEvent, ...[args]: encode.Args<abiEvent> ): encode.ReturnType // eslint-disable-next-line jsdoc-js/require-jsdoc export function encode( ...parameters: | [ abi: Abi.Abi | readonly unknown[], name: Hex.Hex | string, args?: readonly unknown[] | Record<string, unknown>, ] | [abiEvent: AbiEvent, args?: readonly unknown[] | Record<string, unknown>] ): encode.ReturnType { const [abiEvent, args] = (() => { if (Array.isArray(parameters[0])) { const [abi, name, args] = parameters as [ Abi.Abi | readonly unknown[], Hex.Hex | string, readonly unknown[] | Record<string, unknown> | undefined, ] return [fromAbi(abi, name), args] } const [abiEvent, args] = parameters as [ AbiEvent, readonly unknown[] | Record<string, unknown> | undefined, ] return [abiEvent, args] })() let topics: (Hex.Hex | Hex.Hex[] | null)[] = [] if (args && abiEvent.inputs) { const indexedInputs = abiEvent.inputs.filter( (param) => 'indexed' in param && param.indexed, ) const args_ = Array.isArray(args) ? args : Object.values(args).length > 0 ? (indexedInputs?.map( (x: any, i: number) => (args as any)[x.name ?? i], ) ?? []) : [] if (args_.length > 0) { const encode = (param: abitype.AbiParameter, value: unknown) => { const t = param.type if (t === 'string') return Hash.keccak256(Hex.fromString(value as string), { as: 'Hex' }) if (t === 'bytes') { const hex = typeof value === 'string' ? (value as Hex.Hex) : Hex.fromBytes(value as Bytes.Bytes) return Hash.keccak256(hex, { as: 'Hex' }) } if (t === 'tuple' || arraySuffixRegex.test(t)) throw new FilterTypeNotSupportedError(t) // Fast path for 32-byte primitives. const fast = encodePrimitiveTopic(t, value) if (fast) return fast return AbiParameters.encode([param], [value]) } topics = indexedInputs?.map((param, i) => { if (Array.isArray(args_[i])) return args_[i].map((_: any, j: number) => encode(param, args_[i][j]), ) return typeof args_[i] !== 'undefined' && args_[i] !== null ? encode(param, args_[i]) : null }) ?? [] } } if (abiEvent.anonymous === true) return { topics } const selector = (() => { if (abiEvent.hash) return abiEvent.hash return getSelector(abiEvent) })() return { topics: [selector, ...topics] } } export declare namespace encode { type Args<abiEvent extends AbiEvent> = IsNarrowable<abiEvent, AbiEvent> extends true ? abiEvent['inputs'] extends readonly [] ? [] : internal.ParametersToPrimitiveTypes< abiEvent['inputs'] > extends infer result ? result extends readonly [] ? [] : [result] | [] : [] : [readonly unknown[] | Record<string, unknown>] | [] type ReturnType = { topics: | Compute<[selector: Hex.Hex, ...(Hex.Hex | readonly Hex.Hex[] | null)[]]> | readonly (Hex.Hex | readonly Hex.Hex[] | null)[] } type ErrorType = | AbiParameters.encode.ErrorType | getSelector.ErrorType | Hex.fromString.ErrorType | Hash.keccak256.ErrorType | Errors.GlobalErrorType } /** * Formats an {@link ox#AbiEvent.AbiEvent} into a **Human Readable ABI Error**. * * @example * ```ts twoslash * import { AbiEvent } from 'ox' * * const formatted = AbiEvent.format({ * type: 'event', * name: 'Transfer', * inputs: [ * { name: 'from', type: 'address', indexed: true }, * { name: 'to', type: 'address', indexed: true }, * { name: 'value', type: 'uint256' } * ] * }) * * formatted * // ^? * ``` * * @param abiEvent - The ABI Event to format. * @returns The formatted ABI Event. */ export function format<const abiEvent extends AbiEvent>( abiEvent: abiEvent | AbiEvent, ): format.ReturnType<abiEvent> { return formatAbiItem.formatAbiItem(abiEvent) as never } export declare namespace format { type ReturnType<abiEvent extends AbiEvent = AbiEvent> = formatAbiItem.FormatAbiItem<abiEvent> type ErrorType = Errors.GlobalErrorType } /** * Parses an arbitrary **JSON ABI Event** or **Human Readable ABI Event** into a typed {@link ox#AbiEvent.AbiEvent}. * * @example * ### JSON ABIs * * ```ts twoslash * import { AbiEvent } from 'ox' * * const transfer = AbiEvent.from({ * name: 'Transfer', * type: 'event', * inputs: [ * { name: 'from', type: 'address', indexed: true }, * { name: 'to', type: 'address', indexed: true }, * { name: 'value', type: 'uint256' } * ] * }) * * transfer * //^? * ``` * * @example * ### Human Readable ABIs * * A Human Readable ABI can be parsed into a typed ABI object: * * ```ts twoslash * import { AbiEvent } from 'ox' * * const transfer = AbiEvent.from( * 'event Transfer(address indexed from, address indexed to, uint256 value)' // [!code hl] * ) * * transfer * //^? * ``` * * @param abiEvent - The ABI Event to parse. * @returns Typed ABI Event. */ export function from< const abiEvent extends AbiEvent | string | readonly string[], >( abiEvent: (abiEvent | AbiEvent | string | readonly string[]) & ( | (abiEvent extends string ? internal.Signature<abiEvent> : never) | (abiEvent extends readonly string[] ? internal.Signatures<abiEvent> : never) | AbiEvent ), options: from.Options = {}, ): from.ReturnType<abiEvent> { return AbiItem.from(abiEvent as AbiEvent, options) as never } export declare namespace from { type Options = { /** * Whether or not to prepare the extracted event (optimization for encoding performance). * When `true`, the `hash` property is computed and included in the returned value. * * @default true */ prepare?: boolean | undefined } type ReturnType<abiEvent extends AbiEvent | string | readonly string[]> = AbiItem.from.ReturnType<abiEvent> 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. * * @example * ### Extracting by Name * * ABI Events can be extracted by their name using the `name` option: * * ```ts twoslash * import { Abi, AbiEvent } 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 = AbiEvent.fromAbi(abi, 'Transfer') // [!code focus] * // ^? * ``` * * @example * ### Extracting by Selector * * ABI Events can be extract by their selector when {@link ox#Hex.Hex} is provided to `name`. * * ```ts twoslash * import { Abi, AbiEvent } from 'ox' * * const abi = Abi.from([ * 'function foo()', * 'event Transfer(address owner, address to, uint256 tokenId)', * 'function bar(string a) returns (uint256 x)' * ]) * const selector = * '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' * const item = AbiEvent.fromAbi(abi, selector) // [!code focus] * // ^? * ``` * * :::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 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> >, ): AbiItem.fromAbi.ReturnType<abi, name, args, AbiEvent> { const item = AbiItem.fromAbi(abi, name, options as any) 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 } /** * Computes the event selector (hash of event signature) for an {@link ox#AbiEvent.AbiEvent}. * * @example * ```ts twoslash * import { AbiEvent } from 'ox' * * const selector = AbiEvent.getSelector( * 'event Transfer(address indexed from, address indexed to, uint256 value)' * ) * // @log: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f556a2' * ``` * * @example * ```ts twoslash * import { AbiEvent } from 'ox' * * const selector = AbiEvent.getSelector({ * name: 'Transfer', * type: 'event', * inputs: [ * { name: 'from', type: 'address', indexed: true }, * { name: 'to', type: 'address', indexed: true }, * { name: 'value', type: 'uint256' } * ] * }) * // @log: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f556a2' * ``` * * @param abiItem - The ABI event to compute the selector for. * @returns The {@link ox#Hash.(keccak256:function)} hash of the event signature. */ export function getSelector(abiItem: string | AbiEvent): Hex.Hex { return AbiItem.getSignatureHash(abiItem) } export declare namespace getSelector { type ErrorType = AbiItem.getSignatureHash.ErrorType | Errors.GlobalErrorType } /** * Thrown when the provided arguments do not match the expected arguments. * * @example * ```ts twoslash * import { AbiEvent } from 'ox' * * const abiEvent = AbiEvent.from( * 'event Transfer(address indexed from, address indexed to, uint256 value)' * ) * * const args = AbiEvent.decode(abiEvent, { * data: '0x0000000000000000000000000000000000000000000000000000000000000001', * topics: [ * '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', * '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac', * '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ad' * ] * }) * * AbiEvent.assertArgs(abiEvent, args, { * from: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ad', * to: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac', * value: 1n * }) * // @error: AbiEvent.ArgsMismatchError: Given arguments do not match the expected arguments. * // @error: Event: event Transfer(address indexed from, address indexed to, uint256 value) * // @error: Expected Arguments: * // @error: from: 0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac * // @error: to: 0xa5cc3c03994db5b0d9a5eedd10cabab0813678ad * // @error: value: 1 * // @error: Given Arguments: * // @error: from: 0xa5cc3c03994db5b0d9a5eedd10cabab0813678ad * // @error: to: 0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac * // @error: value: 1 * ``` * * ### Solution * * The provided arguments need to match the expected arguments. * * ```ts twoslash * // @noErrors * import { AbiEvent } from 'ox' * * const abiEvent = AbiEvent.from( * 'event Transfer(address indexed from, address indexed to, uint256 value)' * ) * * const args = AbiEvent.decode(abiEvent, { * data: '0x0000000000000000000000000000000000000000000000000000000000000001', * topics: [ * '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', * '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac', * '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ad' * ] * }) * * AbiEvent.assertArgs(abiEvent, args, { * from: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ad', // [!code --] * from: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac', // [!code ++] * to: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac', // [!code --] * to: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ad', // [!code ++] * value: 1n * }) * ``` */ export class ArgsMismatchError extends Errors.BaseError { override readonly name = 'AbiEvent.ArgsMismatchError' constructor({ abiEvent, expected, given, }: { abiEvent: AbiEvent expected: unknown given: unknown }) { 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, ], }) } } /** * Thrown when no argument was found on the event signature. * * @example * ```ts twoslash * // @noErrors * import { AbiEvent } from 'ox' * * const abiEvent = AbiEvent.from( * 'event Transfer(address indexed from, address indexed to, uint256 value)' * ) * * const args = AbiEvent.decode(abiEvent, { * data: '0x0000000000000000000000000000000000000000000000000000000000000001', * topics: [ * '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', * '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac', * '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ad' * ] * }) * * AbiEvent.assertArgs(abiEvent, args, { * a: 'b', * from: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac', * to: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ad', * value: 1n * }) * // @error: AbiEvent.InputNotFoundError: Parameter "a" not found on `event Transfer(address indexed from, address indexe