Skip to content

TevmNode Interface

The TevmNode interface represents the foundational layer of Tevm's architecture. It exposes powerful low-level access to all the essential components that make up an Ethereum node - from the EVM execution engine to transaction processing and state management.

Interface Overview

export type TevmNode<TMode extends 'fork' | 'normal' = 'fork' | 'normal', TExtended = {}> = {
  // Logging & status
  readonly logger: Logger
  status: 'INITIALIZING' | 'READY' | 'SYNCING' | 'MINING' | 'STOPPED'
  readonly ready: () => Promise<true>
 
  // Core components
  readonly getVm: () => Promise<Vm>
  readonly getTxPool: () => Promise<TxPool>
  readonly getReceiptsManager: () => Promise<ReceiptsManager>
  readonly miningConfig: MiningConfig
 
  // Forking support
  readonly mode: TMode
  readonly forkTransport?: { request: EIP1193RequestFn }
 
  // Account management
  readonly getImpersonatedAccount: () => Address | undefined
  readonly setImpersonatedAccount: (address: Address | undefined) => void
 
  // Event filtering
  readonly setFilter: (filter: Filter) => void
  readonly getFilters: () => Map<Hex, Filter>
  readonly removeFilter: (id: Hex) => void
 
  // Extensibility
  readonly extend: <TExtension>(
    decorator: (client: TevmNode<TMode, TExtended>) => TExtension
  ) => TevmNode<TMode, TExtended & TExtension>
 
  // State management
  readonly deepCopy: () => Promise<TevmNode<TMode, TExtended>>
} & EIP1193EventEmitter & TExtended

Key Capabilities

Initialization & Status

import { createTevmNode } from 'tevm'
 
// Create a node instance
const node = createTevmNode()
 
// Wait for initialization to complete
await node.ready()
 
// Check the current status
console.log(`Node status: ${node.status}`) // 'READY'
 
// Access the logger for debugging
node.logger.debug('Node successfully initialized')

The node status can be one of the following values:

  • INITIALIZING: Node is starting up and components are being created
  • READY: Node is fully initialized and ready for use
  • SYNCING: Node is synchronizing state (usually in fork mode)
  • MINING: Node is currently mining a block
  • STOPPED: Node has been stopped or encountered a fatal error

Virtual Machine Access

import { createTevmNode } from 'tevm'
import { createAddress } from 'tevm/address'
import { hexToBytes } from 'viem'
 
const node = createTevmNode()
await node.ready()
 
// Get access to the VM
const vm = await node.getVm()
 
// Execute a transaction directly
const txResult = await vm.runTx({
  tx: {
    to: createAddress('0x1234567890123456789012345678901234567890'),
    value: 1000000000000000000n, // 1 ETH
    nonce: 0n,
    gasLimit: 21000n,
    gasPrice: 10000000000n
  }
})
 
console.log('Transaction executed:', txResult.execResult.exceptionError
  ? `Failed: ${txResult.execResult.exceptionError}`
  : 'Success!')
 
// Execute EVM bytecode directly
const evmResult = await vm.evm.runCall({
  to: createAddress('0x1234567890123456789012345678901234567890'),
  caller: createAddress('0x5678901234567890123456789012345678901234'),
  data: hexToBytes('0xa9059cbb000000000000000000000000abcdef0123456789abcdef0123456789abcdef0000000000000000000000000000000000000000000000008ac7230489e80000'), // transfer(address,uint256)
  gasLimit: 100000n,
})
 
console.log('EVM call result:', evmResult.execResult)
 
// Hook into EVM execution for debugging
vm.evm.events.on('step', (data, next) => {
  console.log(`${data.pc}: ${data.opcode.name}`)
  next?.() // Continue to next step
})

Transaction Pool Management

import { createTevmNode } from 'tevm'
import { parseEther } from 'viem'
 
const node = createTevmNode()
await node.ready()
 
// Get the transaction pool
const txPool = await node.getTxPool()
 
// Add transactions to the pool
await txPool.add({
  from: '0x1234567890123456789012345678901234567890',
  to: '0x5678901234567890123456789012345678901234',
  value: parseEther('1.5'),
  gasLimit: 21000n,
  maxFeePerGas: 30000000000n,
})
 
// Check pool content
const pendingTxs = await txPool.content()
console.log('Pending transactions:', pendingTxs.pending)
 
// Get ordered transactions (by price and nonce)
const orderedTxs = await txPool.txsByPriceAndNonce()
console.log('Ordered transactions:', orderedTxs)
 
// Get pending transactions for a specific address
const txsForAddress = await txPool.contentFrom('0x1234567890123456789012345678901234567890')
console.log('Transactions for address:', txsForAddress)

Receipt & Log Management

import { createTevmNode } from 'tevm'
 
const node = createTevmNode()
await node.ready()
 
// Get the receipts manager
const receiptsManager = await node.getReceiptsManager()
 
// After a transaction is executed, get its receipt
const receipt = await receiptsManager.getReceiptByTxHash('0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef')
 
// Query logs with filters
const transferLogs = await receiptsManager.getLogs({
  fromBlock: 0n,
  toBlock: 'latest',
  address: '0x1234567890123456789012345678901234567890', // Optional: contract address
  topics: [
    '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', // Transfer event signature
    null, // Any from address
    '0x0000000000000000000000005678901234567890123456789012345678901234'  // Filter by to address
  ],
})
 
console.log('Transfer logs:', transferLogs)
 
// Create a subscription for new logs
const subId = await receiptsManager.newLogSubscription({
  address: '0x1234567890123456789012345678901234567890',
  topics: ['0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef']
})
 
// Handle new matching logs
receiptsManager.on('log', (log) => {
  if (log.subscriptionId === subId) {
    console.log('New transfer detected:', log)
  }
})

Account Impersonation

import { createTevmNode, http } from 'tevm'
import { parseEther } from 'viem'
 
// Create a forked node
const node = createTevmNode({
  fork: {
    transport: http('https://mainnet.infura.io/v3/YOUR-KEY'),
  },
})
await node.ready()
 
// Impersonate a known address (like a whale or contract owner)
node.setImpersonatedAccount('0x28C6c06298d514Db089934071355E5743bf21d60') // Example whale address
 
// Get the VM
const vm = await node.getVm()
 
// Send a transaction as the impersonated account
const txResult = await vm.runTx({
  tx: {
    from: '0x28C6c06298d514Db089934071355E5743bf21d60', // Impersonated address
    to: '0x1234567890123456789012345678901234567890',
    value: parseEther('10'),
    gasLimit: 21000n,
  },
})
 
console.log('Transaction result:', txResult.execResult)
 
// Check if an address is being impersonated
const currentImpersonated = node.getImpersonatedAccount()
console.log('Currently impersonating:', currentImpersonated)
 
// Stop impersonating
node.setImpersonatedAccount(undefined)

Event Filtering

import { createTevmNode } from 'tevm'
import { formatEther, parseEther } from 'viem'
 
const node = createTevmNode()
await node.ready()
 
// Create a filter for all "Transfer" events
node.setFilter({
  id: '0x1', // Custom ID
  fromBlock: 0n,
  toBlock: 'latest',
  address: '0x1234567890123456789012345678901234567890', // Optional: Filter by contract
  topics: [
    '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', // Transfer event signature
  ],
})
 
// Create a filter for a specific address receiving tokens
node.setFilter({
  id: '0x2',
  fromBlock: 0n,
  toBlock: 'latest',
  topics: [
    '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', // Transfer
    null, // Any sender
    '0x0000000000000000000000001234567890123456789012345678901234567890', // Specific recipient (padded)
  ],
})
 
// Get all active filters
const filters = node.getFilters()
console.log(`Active filters: ${filters.size}`)
 
// After executing transactions, get the logs from a filter
const receiptManager = await node.getReceiptsManager()
const logs = await receiptManager.getFilterLogs('0x1')
 
// Format and display the transfer logs
logs.forEach(log => {
  // Decode Transfer(address,address,uint256)
  const from = '0x' + log.topics[1].slice(26)
  const to = '0x' + log.topics[2].slice(26)
  const value = BigInt(log.data)
 
  console.log(`Transfer: ${from} -> ${to}: ${formatEther(value)} ETH`)
})
 
// Remove a filter when done
node.removeFilter('0x1')

Extensibility

import { createTevmNode } from 'tevm'
import { parseEther, formatEther } from 'viem'
 
const node = createTevmNode()
await node.ready()
 
// Extend the node with custom methods
const enhancedNode = node.extend((baseNode) => ({
  // Add a method to get an account's balance
  async getBalance(address) {
    const vm = await baseNode.getVm()
    const account = await vm.stateManager.getAccount(address)
    return account.balance
  },
 
  // Add a method to transfer ETH between accounts
  async transferETH(from, to, amount) {
    const vm = await baseNode.getVm()
 
    // Execute the transfer
    const result = await vm.runTx({
      tx: {
        from,
        to,
        value: amount,
        gasLimit: 21000n,
      }
    })
 
    return {
      success: !result.execResult.exceptionError,
      gasUsed: result.gasUsed,
    }
  },
 
  // Add a method to get all account balances
  async getAllBalances(addresses) {
    const results = {}
 
    for (const addr of addresses) {
      results[addr] = formatEther(await this.getBalance(addr))
    }
 
    return results
  }
}))
 
// Use the extended methods
const balance = await enhancedNode.getBalance('0x1234567890123456789012345678901234567890')
console.log(`Balance: ${formatEther(balance)} ETH`)
 
// Transfer ETH
const transfer = await enhancedNode.transferETH(
  '0x1234567890123456789012345678901234567890',
  '0x5678901234567890123456789012345678901234',
  parseEther('1.5')
)
console.log(`Transfer ${transfer.success ? 'succeeded' : 'failed'}, gas used: ${transfer.gasUsed}`)
 
// Get multiple balances at once
const balances = await enhancedNode.getAllBalances([
  '0x1234567890123456789012345678901234567890',
  '0x5678901234567890123456789012345678901234'
])
console.log('Account balances:', balances)

State Management

import { createTevmNode } from 'tevm'
import { parseEther } from 'viem'
 
// Create a base node and perform initial setup
const baseNode = createTevmNode()
await baseNode.ready()
 
// Set up initial state
const vm = await baseNode.getVm()
await vm.stateManager.putAccount(
  '0x1234567890123456789012345678901234567890',
  {
    nonce: 0n,
    balance: parseEther('100'),
    codeHash: '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470',
    storageRoot: '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421'
  }
)
 
// Create an independent copy for a specific test scenario
const scenarioNode = await baseNode.deepCopy()
 
// Modify state in the copy (without affecting the original)
const scenarioVm = await scenarioNode.getVm()
await scenarioVm.stateManager.putAccount(
  '0x1234567890123456789012345678901234567890',
  {
    nonce: 0n,
    balance: parseEther('200'), // Different balance in this scenario
    codeHash: '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470',
    storageRoot: '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421'
  }
)
 
// Both nodes now have independent state
const originalAccount = await (await baseNode.getVm()).stateManager.getAccount(
  '0x1234567890123456789012345678901234567890'
)
const modifiedAccount = await (await scenarioNode.getVm()).stateManager.getAccount(
  '0x1234567890123456789012345678901234567890'
)
 
console.log('Original balance:', originalAccount.balance) // 100 ETH
console.log('Scenario balance:', modifiedAccount.balance) // 200 ETH

Practical Examples

Contract Deployment
import { createTevmNode } from 'tevm'
import { hexToBytes } from 'viem'
 
const node = createTevmNode()
await node.ready()
 
// ERC20 contract bytecode (simplified for example)
const bytecode = '0x60806040...'  // Contract bytecode (truncated)
 
// Deploy the contract
const vm = await node.getVm()
const deployResult = await vm.runTx({
  tx: {
    nonce: 0n,
    gasLimit: 2000000n,
    gasPrice: 10000000000n,
    data: hexToBytes(bytecode),
  }
})
 
if (deployResult.execResult.exceptionError) {
  throw new Error(`Deployment failed: ${deployResult.execResult.exceptionError}`)
}
 
// Get the created contract address
const contractAddress = deployResult.createdAddress
console.log(`Contract deployed at: ${contractAddress}`)
 
// Now you can interact with the contract
const callResult = await vm.runTx({
  tx: {
    to: contractAddress,
    data: hexToBytes('0x70a08231000000000000000000000000' + '1234567890123456789012345678901234567890'.slice(2)), // balanceOf(address)
    gasLimit: 100000n,
  }
})
 
console.log('Call result:', callResult.execResult.returnValue)

Best Practices

1. Initialization Flow

Always wait for the node to be fully initialized before accessing components:

const node = createTevmNode()
await node.ready() // Essential: Ensures all components are initialized
 
// Now safe to use the node
const vm = await node.getVm()

2. Error Handling

Implement robust error handling for EVM operations:

try {
  const vm = await node.getVm()
  const result = await vm.runTx({
    tx: { /* ... */ }
  })
 
  if (result.execResult.exceptionError) {
    console.error(`Execution failed: ${result.execResult.exceptionError}`)
    console.error(`At PC: ${result.execResult.exceptionError.pc}`)
    // Handle the specific error
  }
} catch (error) {
  // Handle unexpected errors
  console.error('Unexpected error:', error.message)
}

3. Resource Management

Clean up resources when they're no longer needed:

// Remove filters when done
node.getFilters().forEach((_, id) => node.removeFilter(id))
 
// Remove event listeners
const vm = await node.getVm()
vm.evm.events.removeAllListeners('step')
 
// For subscriptions
const receiptsManager = await node.getReceiptsManager()
receiptsManager.removeAllListeners('log')

4. State Isolation

Use deepCopy for testing different scenarios:

const baseNode = createTevmNode()
await baseNode.ready()
 
// Set up initial state
// ...
 
// For each test case, create an independent copy
async function runTestCase(scenario) {
  const testNode = await baseNode.deepCopy()
 
  // Modify state for this specific test
  // ...
 
  // Run the test
  // ...
 
  // Each test has isolated state that doesn't affect other tests
}

5. Optimizing Performance

For heavy workloads, consider these optimizations:

// Disable profiling when not needed
const node = createTevmNode({
  profiler: false,
})
 
// Use direct VM access for bulk operations
const vm = await node.getVm()
const stateManager = vm.stateManager
 
// Batch state changes
const addresses = ['0x1111...', '0x2222...', '0x3333...']
for (const address of addresses) {
  await stateManager.putAccount(address, {
    // Account data
  })
}
 
// Only mine when necessary (if using manual mining)
await node.mine({ blocks: 1 })

Type Safety

The TevmNode interface is fully typed with TypeScript, providing excellent development-time safety:

import type { TevmNode } from 'tevm/node'
 
// Function that works with any TevmNode
function setupNode<TMode extends 'fork' | 'normal'>(
  node: TevmNode<TMode>
) {
  return async () => {
    await node.ready()
 
    // Fork-specific operations with type checking
    if (node.mode === 'fork') {
      node.setImpersonatedAccount('0x...')
      return { mode: 'fork', impersonatedAccount: node.getImpersonatedAccount() }
    }
 
    return { mode: 'normal' }
  }
}
 
// With extension types
function createEnhancedNode() {
  const baseNode = createTevmNode()
 
  const enhancedNode = baseNode.extend((base) => ({
    async getBalance(address: string): Promise<bigint> {
      const vm = await base.getVm()
      const account = await vm.stateManager.getAccount(address)
      return account.balance
    }
  }))
 
  // TypeScript knows enhancedNode has getBalance method
  return enhancedNode
}

Next Steps