You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
222 lines
6.6 KiB
222 lines
6.6 KiB
// TODO(v3): checksum address.
|
|
|
|
import type { Abi, AbiEvent, AbiEventParameter, Address } from 'abitype'
|
|
import {
|
|
AbiEventSignatureNotFoundError,
|
|
DecodeLogDataMismatch,
|
|
DecodeLogTopicsMismatch,
|
|
} from '../../errors/abi.js'
|
|
import type { ErrorType } from '../../errors/utils.js'
|
|
import type { ContractEventName, GetEventArgs } from '../../types/contract.js'
|
|
import type { Log } from '../../types/log.js'
|
|
import type { RpcLog } from '../../types/rpc.js'
|
|
import { isAddressEqual } from '../address/isAddressEqual.js'
|
|
import { toBytes } from '../encoding/toBytes.js'
|
|
import { keccak256 } from '../hash/keccak256.js'
|
|
import { toEventSelector } from '../hash/toEventSelector.js'
|
|
import {
|
|
type DecodeEventLogErrorType,
|
|
decodeEventLog,
|
|
} from './decodeEventLog.js'
|
|
|
|
export type ParseEventLogsParameters<
|
|
abi extends Abi | readonly unknown[] = Abi,
|
|
eventName extends
|
|
| ContractEventName<abi>
|
|
| ContractEventName<abi>[]
|
|
| undefined = ContractEventName<abi>,
|
|
strict extends boolean | undefined = boolean | undefined,
|
|
///
|
|
allArgs = GetEventArgs<
|
|
abi,
|
|
eventName extends ContractEventName<abi>
|
|
? eventName
|
|
: ContractEventName<abi>,
|
|
{
|
|
EnableUnion: true
|
|
IndexedOnly: false
|
|
Required: false
|
|
}
|
|
>,
|
|
> = {
|
|
/** Contract ABI. */
|
|
abi: abi
|
|
/** Arguments for the event. */
|
|
args?: allArgs | undefined
|
|
/** Contract event. */
|
|
eventName?:
|
|
| eventName
|
|
| ContractEventName<abi>
|
|
| ContractEventName<abi>[]
|
|
| undefined
|
|
/** List of logs. */
|
|
logs: (Log | RpcLog)[]
|
|
strict?: strict | boolean | undefined
|
|
}
|
|
|
|
export type ParseEventLogsReturnType<
|
|
abi extends Abi | readonly unknown[] = Abi,
|
|
eventName extends
|
|
| ContractEventName<abi>
|
|
| ContractEventName<abi>[]
|
|
| undefined = ContractEventName<abi>,
|
|
strict extends boolean | undefined = boolean | undefined,
|
|
///
|
|
derivedEventName extends
|
|
| ContractEventName<abi>
|
|
| undefined = eventName extends ContractEventName<abi>[]
|
|
? eventName[number]
|
|
: eventName,
|
|
> = Log<bigint, number, false, undefined, strict, abi, derivedEventName>[]
|
|
|
|
export type ParseEventLogsErrorType = DecodeEventLogErrorType | ErrorType
|
|
|
|
/**
|
|
* Extracts & decodes logs matching the provided signature(s) (`abi` + optional `eventName`)
|
|
* from a set of opaque logs.
|
|
*
|
|
* @param parameters - {@link ParseEventLogsParameters}
|
|
* @returns The logs. {@link ParseEventLogsReturnType}
|
|
*
|
|
* @example
|
|
* import { createClient, http } from 'viem'
|
|
* import { mainnet } from 'viem/chains'
|
|
* import { parseEventLogs } from 'viem/op-stack'
|
|
*
|
|
* const client = createClient({
|
|
* chain: mainnet,
|
|
* transport: http(),
|
|
* })
|
|
*
|
|
* const receipt = await getTransactionReceipt(client, {
|
|
* hash: '0xec23b2ba4bc59ba61554507c1b1bc91649e6586eb2dd00c728e8ed0db8bb37ea',
|
|
* })
|
|
*
|
|
* const logs = parseEventLogs({ logs: receipt.logs })
|
|
* // [{ args: { ... }, eventName: 'TransactionDeposited', ... }, ...]
|
|
*/
|
|
export function parseEventLogs<
|
|
abi extends Abi | readonly unknown[],
|
|
strict extends boolean | undefined = true,
|
|
eventName extends
|
|
| ContractEventName<abi>
|
|
| ContractEventName<abi>[]
|
|
| undefined = undefined,
|
|
>(
|
|
parameters: ParseEventLogsParameters<abi, eventName, strict>,
|
|
): ParseEventLogsReturnType<abi, eventName, strict> {
|
|
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 = (abi as Abi).find(
|
|
(abiItem) =>
|
|
abiItem.type === 'event' &&
|
|
log.topics[0] === toEventSelector(abiItem),
|
|
) as AbiEvent
|
|
if (!abiItem) return null
|
|
|
|
const event = decodeEventLog({
|
|
...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 ParseEventLogsReturnType<
|
|
abi,
|
|
eventName,
|
|
strict
|
|
>
|
|
}
|
|
|
|
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 === null || value === undefined) 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 === null || value === undefined) 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<string, unknown>)[key]),
|
|
)
|
|
})
|
|
|
|
return false
|
|
}
|