Skip to content
LogoLogo

Events and filters

Every event in the ABI becomes an action creator on contract.events. Calling one returns an event-filter object suitable for eth_getLogs-style APIs.

The basics

import { createContract } from '@tevm/contract'
 
const Dai = createContract({
  name: 'Dai',
  humanReadableAbi: [
    'function balanceOf(address owner) view returns (uint256)',
    'event Transfer(address indexed from, address indexed to, uint256 value)',
    'event Approval(address indexed owner, address indexed spender, uint256 value)',
  ],
  address: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
})
 
const filter = Dai.events.Transfer({
  fromBlock: 'latest',
  toBlock: 'latest',
})
 
console.log(filter.eventName) // 'Transfer'
console.log(filter.address)   // '0x6B175474E89094C44Da98b954EedeAC495271d0F'
console.log(filter.humanReadableAbi)
// ['event Transfer(address indexed from, address indexed to, uint256 value)']

Filtering on indexed arguments

args filters on indexed event parameters only. The keys available in args are derived from the ABI, so filtering on a non-indexed parameter is a compile error.

import { createContract } from '@tevm/contract'
 
const Dai = createContract({
  name: 'Dai',
  humanReadableAbi: [
    'event Transfer(address indexed from, address indexed to, uint256 value)',
  ],
  address: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
})
 
// All transfers from one address
const outgoing = Dai.events.Transfer({
  args: { from: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' },
  fromBlock: 0n,
  toBlock: 'latest',
})
 
// Transfers between two specific addresses
const between = Dai.events.Transfer({
  args: {
    from: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
    to: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
  },
  fromBlock: 0n,
  toBlock: 'latest',
})
 
// @ts-expect-error — `value` is not indexed, so it cannot be filtered on
Dai.events.Transfer({ args: { value: 1n } })

Filter parameters

ParameterTypeMeaning
fromBlockBlockNumber | BlockTagFirst block to search.
toBlockBlockNumber | BlockTagLast block to search.
argsobjectValues for indexed parameters. Omitted keys are wildcards.
strictbooleanWhen true, only logs whose data matches the ABI exactly are returned; malformed logs are dropped rather than partially decoded.

Every parameter is optional. Calling the creator with no argument at all yields a filter for every occurrence of the event at that address:

const all = Dai.events.Transfer()

Using it as a value

Like read and write creators, an event creator carries its own metadata, which is handy when a consumer builds the filter itself:

console.log(Dai.events.Transfer.eventName) // 'Transfer'
console.log(Dai.events.Transfer.abi)       // [{ type: 'event', name: 'Transfer', ... }]
console.log(Dai.events.Transfer.address)   // '0x6B175474E89094C44Da98b954EedeAC495271d0F'

A complete round-trip

SimpleContract emits ValueSet(uint256) on every set. Deploy it, write to it, and read the log back:

import { SimpleContract } from '@tevm/contract'
import { createMemoryClient, PREFUNDED_ACCOUNTS } from 'tevm'
 
const client = createMemoryClient()
await client.tevmReady()
 
const deployResult = await client.tevmDeploy({
  from: PREFUNDED_ACCOUNTS[0].address,
  ...SimpleContract.deploy(0n),
  addToMempool: true,
})
await client.tevmMine({ blockCount: 1 })
 
if (!deployResult.createdAddress) throw new Error('deployment failed')
const Counter = SimpleContract.withAddress(deployResult.createdAddress)
 
await client.tevmContract({
  ...Counter.write.set(42n),
  from: PREFUNDED_ACCOUNTS[0].address,
  addToMempool: true,
})
await client.tevmMine({ blockCount: 1 })
 
const logs = await client.getLogs(
  Counter.events.ValueSet({ fromBlock: 0n, toBlock: 'latest' }),
)
 
console.log(logs[0]?.args) // { newValue: 42n }

Contracts with no events

If the ABI contains no event entries, contract.events is an empty object rather than undefined — you can always spread or enumerate it safely.

import { createContract } from '@tevm/contract'
 
const C = createContract({
  name: 'C',
  humanReadableAbi: ['function get() view returns (uint256)'],
})
 
console.log(Object.keys(C.events)) // []

Bytecode on filters

Event filters also carry bytecode and deployedBytecode when the contract has them. This lets an executor resolve logs for a contract that has not been deployed to a persistent address — the same trick withCode enables for reads.