- Consolidated duplicate UndoManagers to single instance - Fixed connection promise to only resolve on 'connected' status - Fixed WebSocketProvider import (WebsocketProvider) - Added proper doc.destroy() cleanup - Renamed isPresenceInitialized property to avoid conflict Co-Authored-By: Paperclip <noreply@paperclip.ing>
252 lines
7.6 KiB
TypeScript
252 lines
7.6 KiB
TypeScript
// TODO(v3): checksum address.
|
|
|
|
import type { Abi, AbiEvent, AbiEventParameter, Address } from 'abitype'
|
|
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 { formatLog } from '../formatters/log.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]
|
|
})()
|
|
|
|
const abiTopics = (abi as Abi)
|
|
.filter((abiItem) => abiItem.type === 'event')
|
|
.map((abiItem) => ({
|
|
abi: abiItem,
|
|
selector: toEventSelector(abiItem),
|
|
}))
|
|
|
|
return logs
|
|
.map((log) => {
|
|
// Normalize RpcLog (hex-encoded quantities) to Log (bigint/number).
|
|
// When logs come directly from an RPC response (e.g. eth_getLogs),
|
|
// fields like blockNumber are hex strings instead of bigints.
|
|
const formattedLog =
|
|
typeof log.blockNumber === 'string' ? formatLog(log as RpcLog) : log
|
|
|
|
// Find all matching ABI items with the same selector.
|
|
// Multiple events can share the same selector but differ in indexed parameters
|
|
// (e.g., ERC20 vs ERC721 Transfer events).
|
|
const abiItems = abiTopics.filter(
|
|
(abiTopic) => formattedLog.topics[0] === abiTopic.selector,
|
|
)
|
|
if (abiItems.length === 0) return null
|
|
|
|
// Try each matching ABI item until one successfully decodes.
|
|
let event: { eventName: string; args: unknown } | undefined
|
|
let abiItem: { abi: AbiEvent; selector: Address } | undefined
|
|
|
|
for (const item of abiItems) {
|
|
try {
|
|
event = decodeEventLog({
|
|
...formattedLog,
|
|
abi: [item.abi],
|
|
strict: true,
|
|
})
|
|
abiItem = item
|
|
break
|
|
} catch {
|
|
// Try next ABI item
|
|
}
|
|
}
|
|
|
|
// If strict decoding failed for all, and we're in non-strict mode,
|
|
// fall back to the first matching ABI item.
|
|
if (!event && !strict) {
|
|
abiItem = abiItems[0]
|
|
try {
|
|
event = decodeEventLog({
|
|
data: formattedLog.data,
|
|
topics: formattedLog.topics,
|
|
abi: [abiItem.abi],
|
|
strict: false,
|
|
})
|
|
} catch {
|
|
// If decoding still fails, return partial log in non-strict mode.
|
|
const isUnnamed = abiItem.abi.inputs?.some(
|
|
(x) => !('name' in x && x.name),
|
|
)
|
|
return {
|
|
...formattedLog,
|
|
args: isUnnamed ? [] : {},
|
|
eventName: abiItem.abi.name,
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no event was found, return null.
|
|
if (!event || !abiItem) return null
|
|
|
|
// 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.abi.inputs,
|
|
matchArgs: args,
|
|
})
|
|
)
|
|
return null
|
|
|
|
return { ...event, ...formattedLog }
|
|
})
|
|
.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
|
|
}
|