Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

EVM Events

Tevm Node exposes low-level EVM events for monitoring and debugging contract execution.

Available Events

type EVMEvent = {
  newContract: (data: { address: Address, code: Uint8Array }, next?: () => void) => void
  beforeMessage: (data: Message, next?: () => void) => void
  afterMessage: (data: EVMResult, next?: () => void) => void
  step: (data: InterpreterStep, next?: () => void) => void
}

InterpreterStep

interface InterpreterStep {
  pc: number                    // program counter
  opcode: {
    name: string                // e.g. 'SSTORE', 'CALL'
    fee: number                 // base gas fee
    dynamicFee?: bigint         // additional dynamic gas
    isAsync: boolean
  }
  gasLeft: bigint
  gasRefund: bigint
  stateManager: StateManager
  stack: Uint8Array[]
  returnStack: bigint[]         // for RETURNSUB
  account: Account              // account owning the running code
  address: Address
  depth: number                 // 0 for top-level call
  memory: Uint8Array
  memoryWordCount: bigint       // size in 32-byte words
  codeAddress: Address          // differs from address in DELEGATECALL/CALLCODE
}

NewContractEvent

interface NewContractEvent {
  address: Address
  code: Uint8Array
}

Message

interface Message {
  to?: Address                  // undefined for contract creation
  value: bigint
  caller: Address
  gasLimit: bigint
  data: Uint8Array
  code?: Uint8Array
  codeAddress?: Address
  depth: number
  isStatic: boolean
  isCompiled: boolean           // precompiled contract
  delegatecall: boolean
  callcode: boolean
  salt?: Uint8Array             // CREATE2
  authcallOrigin?: Address      // AUTH
}

EVMResult

interface EVMResult {
  execResult: {
    returnValue: Uint8Array
    executionGasUsed: bigint
    gasRefund?: bigint
    exceptionError?: {
      error: string             // e.g. 'revert', 'out of gas'
      errorType?: string
    }
    logs?: Log[]
    selfdestruct?: Record<string, true>
    gas?: bigint
  }
  gasUsed: bigint               // includes intrinsic costs
  createdAddress?: Address
  gasRefund?: bigint
}

Using with tevmCall Family

The recommended access pattern is through the tevmCall family. Two API styles:

Client-based API

import { createMemoryClient } from 'tevm'
import { encodeFunctionData } from 'viem'
 
const client = createMemoryClient()
 
const result = await client.tevmCall({
  to: contractAddress,
  data: encodeFunctionData({ abi, functionName: 'myFunction', args: [arg1, arg2] }),
  onStep: (step, next) => {
    console.log('EVM Step:', {
      pc: step.pc,
      opcode: step.opcode,
      gasLeft: step.gasLeft,
      stack: step.stack,
      depth: step.depth,
    })
    next?.()
  },
  onNewContract: (data, next) => {
    console.log('New contract deployed:', {
      address: data.address.toString(),
      codeSize: data.code.length,
    })
    next?.()
  },
  onBeforeMessage: (message, next) => {
    console.log('Executing message:', {
      to: message.to?.toString(),
      value: message.value.toString(),
      delegatecall: message.delegatecall,
    })
    next?.()
  },
  onAfterMessage: (result, next) => {
    console.log('Message result:', {
      gasUsed: result.execResult.executionGasUsed.toString(),
      returnValue: result.execResult.returnValue.toString('hex'),
      error: result.execResult.exceptionError?.error,
    })
    next?.()
  }
})

Tree-shakable API

import { tevmCall } from 'tevm/actions'
import { createClient } from 'viem'
import { createTevmNode } from 'tevm/node'
import { requestEip1193 } from 'tevm/decorators'
import { encodeFunctionData } from 'viem'
 
const tevmTransport = createTevmTransport()
const client = createClient({ transport: tevmTransport })
 
const result = await tevmCall(client, {
  to: contractAddress,
  data: encodeFunctionData({ abi, functionName: 'myFunction', args: [arg1, arg2] }),
  onStep: (step, next) => {
    console.log('EVM Step:', { pc: step.pc, opcode: step.opcode, gasLeft: step.gasLeft, stack: step.stack, depth: step.depth })
    next?.()
  },
  onNewContract: (data, next) => {
    console.log('New contract:', { address: data.address.toString(), codeSize: data.code.length })
    next?.()
  },
  onBeforeMessage: (message, next) => {
    console.log('Message:', { to: message.to?.toString(), value: message.value.toString(), delegatecall: message.delegatecall })
    next?.()
  },
  onAfterMessage: (result, next) => {
    console.log('Result:', {
      gasUsed: result.execResult.executionGasUsed.toString(),
      returnValue: result.execResult.returnValue.toString('hex'),
      error: result.execResult.exceptionError?.error,
    })
    next?.()
  }
})

Advanced Examples

Debug Tracer

import { createMemoryClient } from 'tevm'
import { tevmCall } from 'tevm/actions'
 
const client = createMemoryClient()
 
const trace = { steps: [], contracts: new Set(), errors: [] }
 
await tevmCall(client, {
  to: contractAddress,
  data: '0x...',
  onStep: (step, next) => {
    trace.steps.push({
      pc: step.pc,
      opcode: step.opcode.name,
      gasCost: step.opcode.fee,
      stack: step.stack.map(item => item.toString(16)),
    })
    next?.()
  },
  onNewContract: (data, next) => {
    trace.contracts.add(data.address.toString())
    next?.()
  },
  onAfterMessage: (result, next) => {
    if (result.execResult.exceptionError) {
      trace.errors.push({
        error: result.execResult.exceptionError.error,
        returnData: result.execResult.returnValue.toString('hex'),
      })
    }
    next?.()
  }
})
 
console.log('Trace:', {
  stepCount: trace.steps.length,
  contracts: Array.from(trace.contracts),
  errors: trace.errors,
})

Gas Profiler

import { createMemoryClient } from 'tevm'
import { tevmCall } from 'tevm/actions'
 
const client = createMemoryClient()
const profile = { opcodes: new Map(), totalGas: 0n }
 
await tevmCall(client, {
  to: contractAddress,
  data: '0x...',
  onStep: (step, next) => {
    const opName = step.opcode.name
    const gasCost = BigInt(step.opcode.fee)
    const stats = profile.opcodes.get(opName) || { count: 0, totalGas: 0n }
    stats.count++
    stats.totalGas += gasCost
    profile.totalGas += gasCost
    profile.opcodes.set(opName, stats)
    next?.()
  }
})
 
for (const [opcode, stats] of profile.opcodes) {
  console.log(`${opcode}:`, {
    count: stats.count,
    totalGas: stats.totalGas.toString(),
    percentageOfTotal: Number(stats.totalGas * 100n / profile.totalGas),
  })
}

Error Handling

import { createMemoryClient } from 'tevm'
import { tevmCall } from 'tevm/actions'
 
const client = createMemoryClient()
 
await tevmCall(client, {
  to: contractAddress,
  data: '0x...',
  onAfterMessage: (result, next) => {
    if (result.execResult.exceptionError) {
      const error = result.execResult.exceptionError
      switch (error.error) {
        case 'out of gas':
          console.error('Out of gas')
          break
        case 'revert':
          console.error('Reverted:', result.execResult.returnValue.toString('hex'))
          break
        case 'invalid opcode':
          console.error('Invalid opcode')
          break
        default:
          console.error('Unknown error:', error)
      }
    }
    next?.()
  }
})

Best Practices

Always call next?.() to continue execution:

onStep: (step, next) => {
  // process step
  next?.()
}

Handle errors so one bad handler doesn't break execution:

onStep: (step, next) => {
  try {
    // process step
  } catch (error) {
    console.error('Step handler error:', error)
  }
  next?.()
}

Filter to what you need for performance:

onStep: (step, next) => {
  if (step.opcode.name === 'SSTORE') {
    console.log('Storage write:', {
      key: step.stack[step.stack.length - 1].toString(16),
      value: step.stack[step.stack.length - 2].toString(16)
    })
  }
  next?.()
}

Mining Events

Mining operations also emit events:

import { createMemoryClient } from 'tevm'
import { mine } from 'tevm/actions'
 
const client = createMemoryClient()
 
await client.mine({
  blockCount: 1,
  onBlock: (block, next) => {
    console.log('Block:', {
      number: block.header.number,
      hash: block.hash().toString('hex'),
      timestamp: block.header.timestamp
    })
    next?.()
  },
  onReceipt: (receipt, blockHash, next) => {
    console.log('Receipt:', {
      txHash: receipt.transactionHash,
      blockHash,
      gasUsed: receipt.gasUsed
    })
    next?.()
  },
  onLog: (log, receipt, next) => {
    console.log('Log:', { address: log.address, topics: log.topics, data: log.data })
    next?.()
  }
})

Mining handlers also require next?.() to continue.

Related Topics