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