import type { Abi, AbiEvent, AbiEventParameter, Address } from 'abitype' import { isAddressEqual } from '../address/isAddressEqual.js' import { toBytes } from '../encoding/toBytes.js' import { AbiEventSignatureNotFoundError, DecodeLogDataMismatch, DecodeLogTopicsMismatch, } from '../errors/abi.js' import type { ErrorType } from '../errors/utils.js' import { keccak256 } from '../hash/keccak256.js' import type { ContractEventName, GetEventArgs } from '../types/contract.js' import type { Log, RpcLog } from '../types/log.js' import { type DecodeLogErrorType, decodeLog } from './decodeLog.js' import { getAbiItem } from './getItem.js' export type ParseLogsParameters< abi extends Abi | readonly unknown[] = Abi, eventName extends | ContractEventName | ContractEventName[] | undefined = ContractEventName, strict extends boolean | undefined = boolean | undefined, /// allArgs = GetEventArgs< abi, eventName extends ContractEventName ? eventName : ContractEventName, { EnableUnion: true IndexedOnly: false Required: false } >, > = { /** Contract ABI. */ abi: abi /** Arguments for the event. */ args?: allArgs | undefined /** Contract event. */ eventName?: | eventName | ContractEventName | ContractEventName[] | undefined /** List of logs. */ logs: (Log | RpcLog)[] strict?: strict | boolean | undefined } export type ParseLogsReturnType< abi extends Abi | readonly unknown[] = Abi, eventName extends | ContractEventName | ContractEventName[] | undefined = ContractEventName, strict extends boolean | undefined = boolean | undefined, /// derivedEventName extends | ContractEventName | undefined = eventName extends ContractEventName[] ? eventName[number] : eventName, > = Log[] export type ParseLogsErrorType = DecodeLogErrorType | ErrorType /** * Extracts & decodes logs matching the provided signature(s) (`abi` + optional `eventName`) * from a set of opaque logs. * * @param parameters - {@link ParseLogsParameters} * @returns The logs. {@link ParseLogsReturnType} * * @example * import { createClient, http } from 'viem' * import { mainnet } from 'viem/chains' * import { parseLogs } from 'viem/op-stack' * * const client = createClient({ * chain: mainnet, * transport: http(), * }) * * const receipt = await getTransactionReceipt(client, { * hash: '0xec23b2ba4bc59ba61554507c1b1bc91649e6586eb2dd00c728e8ed0db8bb37ea', * }) * * const logs = parseLogs({ logs: receipt.logs }) * // [{ args: { ... }, eventName: 'TransactionDeposited', ... }, ...] */ export function parseLogs< abi extends Abi | readonly unknown[], strict extends boolean | undefined = true, eventName extends | ContractEventName | ContractEventName[] | undefined = undefined, >( parameters: ParseLogsParameters, ): ParseLogsReturnType { const { abi, args, logs, strict = true } = parameters const eventName = (() => { if (!parameters.eventName) return undefined if (Array.isArray(parameters.eventName)) return parameters.eventName return [parameters.eventName as string] })() return logs .map((log) => { try { const abiItem = getAbiItem({ abi: abi as Abi, name: log.topics[0] as string, }) as AbiEvent if (!abiItem) return null const event = decodeLog({ ...log, abi: [abiItem], strict, }) // Check that the decoded event name matches the provided event name. if (eventName && !eventName.includes(event.eventName)) return null // Check that the decoded event args match the provided args. if ( !includesArgs({ args: event.args, inputs: abiItem.inputs, matchArgs: args, }) ) return null return { ...event, ...log } } catch (err) { let eventName: string | undefined let isUnnamed: boolean | undefined if (err instanceof AbiEventSignatureNotFoundError) return null if ( err instanceof DecodeLogDataMismatch || err instanceof DecodeLogTopicsMismatch ) { // If strict mode is on, and log data/topics do not match event definition, skip. if (strict) return null eventName = err.abiItem.name isUnnamed = err.abiItem.inputs?.some((x) => !('name' in x && x.name)) } // Set args to empty if there is an error decoding (e.g. indexed/non-indexed params mismatch). return { ...log, args: isUnnamed ? [] : {}, eventName } } }) .filter(Boolean) as unknown as ParseLogsReturnType } function includesArgs(parameters: { args: unknown inputs: AbiEvent['inputs'] matchArgs: unknown }) { const { args, inputs, matchArgs } = parameters if (!matchArgs) return true if (!args) return false function isEqual(input: AbiEventParameter, value: unknown, arg: unknown) { try { if (input.type === 'address') return isAddressEqual(value as Address, arg as Address) if (input.type === 'string' || input.type === 'bytes') return keccak256(toBytes(value as string)) === arg return value === arg } catch { return false } } if (Array.isArray(args) && Array.isArray(matchArgs)) { return matchArgs.every((value, index) => { if (!value) return true const input = inputs[index] if (!input) return false const value_ = Array.isArray(value) ? value : [value] return value_.some((value) => isEqual(input, value, args[index])) }) } if ( typeof args === 'object' && !Array.isArray(args) && typeof matchArgs === 'object' && !Array.isArray(matchArgs) ) return Object.entries(matchArgs).every(([key, value]) => { if (!value) return true const input = inputs.find((input) => input.name === key) if (!input) return false const value_ = Array.isArray(value) ? value : [value] return value_.some((value) => isEqual(input, value, (args as Record)[key]), ) }) return false }