import type { AbiHolder, EventArgsOf } from "../../../abi-types.js"; import type { HardhatViemHelpers } from "@nomicfoundation/hardhat-viem/types"; import type { ChainType } from "hardhat/types/network"; import type { Abi, AbiEvent, ContractEventName, Hash } from "viem"; import assert from "node:assert/strict"; import { settle, stringifyArgs } from "../../helpers.js"; import { isArgumentMatch } from "../../predicates.js"; import { handleEmit } from "./core.js"; export async function emitWithArgs< TContract extends AbiHolder, TEventName extends ContractEventName, ChainTypeT extends ChainType | string = "generic", >( viem: HardhatViemHelpers, txHash: Hash | Promise, contract: TContract, eventName: TEventName, expectedArgs: EventArgsOf, ): Promise { // Settle `txHash` first so the tx doesn't leak into the next test, but // defer rethrowing so ABI errors still take precedence over tx reverts. const txHashResult = await settle(txHash); const abiEvents: AbiEvent[] = contract.abi.filter( (item): item is AbiEvent => item.type === "event" && item.name === eventName && item.inputs.length === expectedArgs.length, ); assert.ok( abiEvents.length !== 0, `Event "${eventName}" with argument count ${expectedArgs.length} not found in the contract ABI`, ); assert.ok( abiEvents.length === 1, `There are multiple events named "${eventName}" that accepts ${expectedArgs.length} input arguments. This scenario is currently not supported.`, ); if (txHashResult.ok === false) { // eslint-disable-next-line no-restricted-syntax -- propagate the original tx-revert error throw txHashResult.error; } const expectedAbiEvent = abiEvents[0]; // txHash has already been awaited above; pass an already-resolved promise // so handleEmit's internal await is a no-op and we don't double-submit. const parsedLogs = await handleEmit( viem, Promise.resolve(txHashResult.value), contract, eventName, ); for (const { args: logArgs } of parsedLogs) { let emittedArgs: any[] = []; if (logArgs === undefined) { if (expectedArgs.length === 0) { // If the logs contain no arguments and none are expected, we can return, this is a valid match return; } continue; } if (Array.isArray(logArgs)) { // All the expected args are listed in an array, this happens when some of the event parameters do not have parameter names. // Example: event EventX(uint u, uint) -> mapped to -> [bigint, bigint] emittedArgs = logArgs; } else { // The event parameters have names, so they are represented as an object. // They must be mapped into a sorted array that matches the order of the ABI event parameters. // Example: event EventY(uint u, uint v) -> mapped to -> { u: bigint, v: bigint } for (const [index, param] of expectedAbiEvent.inputs.entries()) { assert.ok( param.name !== undefined, `The event parameter at index ${index} does not have a name`, ); emittedArgs.push(logArgs[param.name]); } } if (await isArgumentMatch(emittedArgs, expectedArgs)) { return; } if (parsedLogs.length === 1) { // Provide additional error details only if a single event was emitted if (expectedArgs.some((arg) => typeof arg === "function")) { // If there are predicate matchers, we can't use the built-in deepEqual with diff const displayExpectedArgs = expectedArgs.map((expectedArg) => { if (typeof expectedArg === "function") { const hasName = expectedArg.name !== undefined && expectedArg.name !== ""; return `<${hasName ? expectedArg.name : "predicate"}>`; } else { return expectedArg; } }); assert.fail( `The event arguments do not match the expected ones:\nExpected: ${stringifyArgs(displayExpectedArgs)}\nEmitted: ${stringifyArgs(emittedArgs)}`, ); } else { // Otherwise, we can use it assert.deepEqual( emittedArgs, expectedArgs, "The event arguments do not match the expected ones.", ); } } } assert.fail( "Multiple events were emitted, but none of them match the expected arguments.", ); }