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
📥 Transaction Pool
Working with pending transactions👤 Account Control
Advanced account management and impersonation🔍 Event System
Creating and managing event filters and subscriptions🧰 Extensibility
Adding custom functionality through decoratorsInitialization & 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 createdREADY
: Node is fully initialized and ready for useSYNCING
: Node is synchronizing state (usually in fork mode)MINING
: Node is currently mining a blockSTOPPED
: 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
}