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

TevmNode Interface

TevmNode is the core low-level API. It exposes the EVM, transaction pool, state, receipts, account impersonation, filters, and extensibility hooks.

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";
 
const node = createTevmNode();
await node.ready();
 
console.log(`Node status: ${node.status}`); // 'READY'
node.logger.debug("Node successfully initialized");

Status values:

  • INITIALIZING — starting up
  • READY — ready for use
  • SYNCING — synchronizing (usually fork mode)
  • MINING — mining a block
  • STOPPED — stopped or fatal error

Virtual Machine Access

import { createTevmNode } from "tevm";
import { createAddress } from "tevm/address";
import { createImpersonatedTx } from "tevm/tx";
import { hexToBytes } from "viem";
 
const node = createTevmNode();
await node.ready();
 
const vm = await node.getVm();
 
// Run a transaction
const tx = createImpersonatedTx({
  to: "0x1234567890123456789012345678901234567890",
  value: 1000000000000000000n,
  nonce: 0n,
  gasLimit: 21000n,
  gasPrice: 10000000000n,
});
const txResult = await vm.runTx({ tx });
 
// Run a call directly on the EVM
const evmResult = await vm.evm.runCall({
  to: createAddress("0x1234567890123456789012345678901234567890"),
  caller: createAddress("0x5678901234567890123456789012345678901234"),
  data: hexToBytes(
    "0xa9059cbb000000000000000000000000abcdef0123456789abcdef0123456789abcdef0000000000000000000000000000000000000000000000008ac7230489e80000",
  ),
  gasLimit: 100000n,
});
 
// Step-level tracing
vm.evm.events.on("step", (data, next) => {
  console.log(`${data.pc}: ${data.opcode.name}`);
  next?.();
});

Transaction Pool Management

import { createTevmNode } from "tevm";
import { parseEther } from "viem";
 
const node = createTevmNode();
await node.ready();
 
const txPool = await node.getTxPool();
 
await txPool.add({
  from: "0x1234567890123456789012345678901234567890",
  to: "0x5678901234567890123456789012345678901234",
  value: parseEther("1.5"),
  gasLimit: 21000n,
  maxFeePerGas: 30000000000n,
});
 
const pendingTxs = await txPool.content();
const orderedTxs = await txPool.txsByPriceAndNonce();
const txsForAddress = await txPool.contentFrom(
  "0x1234567890123456789012345678901234567890",
);

Receipt & Log Management

import { createTevmNode } from "tevm";
 
const node = createTevmNode();
await node.ready();
 
const receiptsManager = await node.getReceiptsManager();
 
const receipt = await receiptsManager.getReceiptByTxHash(
  "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
);
 
const transferLogs = await receiptsManager.getLogs({
  fromBlock: 0n,
  toBlock: "latest",
  address: "0x1234567890123456789012345678901234567890",
  topics: [
    "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", // Transfer
    null,
    "0x0000000000000000000000005678901234567890123456789012345678901234",
  ],
});
 
const subId = await receiptsManager.newLogSubscription({
  address: "0x1234567890123456789012345678901234567890",
  topics: ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"],
});
 
receiptsManager.on("log", (log) => {
  if (log.subscriptionId === subId) {
    console.log("New transfer:", log);
  }
});

Account Impersonation

Impersonate any address in fork mode without its private key:

import { createTevmNode, http } from "tevm";
import { createImpersonatedTx } from "tevm/tx";
import { parseEther } from "viem";
 
const node = createTevmNode({
  fork: { transport: http("https://mainnet.infura.io/v3/YOUR-KEY") },
});
await node.ready();
 
node.setImpersonatedAccount("0x28C6c06298d514Db089934071355E5743bf21d60");
 
const vm = await node.getVm();
const tx = createImpersonatedTx({
  from: "0x28C6c06298d514Db089934071355E5743bf21d60",
  to: "0x1234567890123456789012345678901234567890",
  value: parseEther("10"),
  gasLimit: 21000n,
});
 
const txResult = await vm.runTx({ tx });
 
const currentImpersonated = node.getImpersonatedAccount();
node.setImpersonatedAccount(undefined); // stop

Event Filtering

import { createTevmNode } from "tevm";
import { formatEther } from "viem";
 
const node = createTevmNode();
await node.ready();
 
// All Transfer events
node.setFilter({
  id: "0x1",
  fromBlock: 0n,
  toBlock: "latest",
  address: "0x1234567890123456789012345678901234567890",
  topics: ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"],
});
 
// Transfers to a specific recipient
node.setFilter({
  id: "0x2",
  fromBlock: 0n,
  toBlock: "latest",
  topics: [
    "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
    null,
    "0x0000000000000000000000001234567890123456789012345678901234567890",
  ],
});
 
const filters = node.getFilters();
 
const receiptManager = await node.getReceiptsManager();
const logs = await receiptManager.getFilterLogs("0x1");
 
logs.forEach((log) => {
  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`);
});
 
node.removeFilter("0x1");

Extensibility

extend adds custom methods via decorators:

import { createTevmNode } from "tevm";
import { createImpersonatedTx } from "tevm/tx";
import { parseEther, formatEther } from "viem";
 
const node = createTevmNode();
await node.ready();
 
const enhancedNode = node.extend((baseNode) => ({
  async getBalance(address) {
    const vm = await baseNode.getVm();
    const account = await vm.stateManager.getAccount(address);
    return account.balance;
  },
 
  async transferETH(from, to, amount) {
    const vm = await baseNode.getVm();
    const tx = createImpersonatedTx({ from, to, value: amount, gasLimit: 21000n });
    const result = await vm.runTx({ tx });
    return {
      success: !result.execResult.exceptionError,
      gasUsed: result.gasUsed,
    };
  },
 
  async getAllBalances(addresses) {
    const results = {};
    for (const addr of addresses) {
      results[addr] = formatEther(await this.getBalance(addr));
    }
    return results;
  },
}));
 
const balance = await enhancedNode.getBalance("0x1234567890123456789012345678901234567890");
const transfer = await enhancedNode.transferETH(
  "0x1234567890123456789012345678901234567890",
  "0x5678901234567890123456789012345678901234",
  parseEther("1.5"),
);
const balances = await enhancedNode.getAllBalances([
  "0x1234567890123456789012345678901234567890",
  "0x5678901234567890123456789012345678901234",
]);

State Management

deepCopy clones a node with identical but independent state:

import { createTevmNode } from "tevm";
import { parseEther } from "viem";
 
const baseNode = createTevmNode();
await baseNode.ready();
 
const vm = await baseNode.getVm();
await vm.stateManager.putAccount("0x1234567890123456789012345678901234567890", {
  nonce: 0n,
  balance: parseEther("100"),
  codeHash: "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470",
  storageRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
});
 
const scenarioNode = await baseNode.deepCopy();
const scenarioVm = await scenarioNode.getVm();
await scenarioVm.stateManager.putAccount("0x1234567890123456789012345678901234567890", {
  nonce: 0n,
  balance: parseEther("200"),
  codeHash: "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470",
  storageRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
});
 
// Independent state — original is unchanged.

Practical Examples

Contract Deployment
import { createTevmNode } from "tevm";
import { hexToBytes } from "viem";
 
const node = createTevmNode();
await node.ready();
 
const bytecode = "0x60806040..."; // truncated
 
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}`);
}
 
const contractAddress = deployResult.createdAddress;
 
const callResult = await vm.runTx({
  tx: {
    to: contractAddress,
    data: hexToBytes(
      "0x70a08231000000000000000000000000" +
        "1234567890123456789012345678901234567890".slice(2),
    ), // balanceOf(address)
    gasLimit: 100000n,
  },
});

Best Practices

1. Initialization Flow

const node = createTevmNode();
await node.ready();
const vm = await node.getVm();

2. Error Handling

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}`);
  }
} catch (error) {
  console.error("Unexpected error:", error.message);
}

3. Resource Management

// Filters
node.getFilters().forEach((_, id) => node.removeFilter(id));
 
// VM listeners
const vm = await node.getVm();
vm.evm.events.removeAllListeners("step");
 
// Receipt subscriptions
const receiptsManager = await node.getReceiptsManager();
receiptsManager.removeAllListeners("log");

4. State Isolation

const baseNode = createTevmNode();
await baseNode.ready();
 
async function runTestCase(scenario) {
  const testNode = await baseNode.deepCopy();
  // mutate testNode; baseNode unchanged
}

5. Performance

const node = createTevmNode({ profiler: false });
 
const vm = await node.getVm();
const stateManager = vm.stateManager;
 
const addresses = ["0x1111...", "0x2222...", "0x3333..."];
for (const address of addresses) {
  await stateManager.putAccount(address, { /* ... */ });
}
 
await node.mine({ blocks: 1 });

Type Safety

TevmNode is fully typed:

import type { TevmNode } from "tevm/node";
 
function setupNode<TMode extends "fork" | "normal">(node: TevmNode<TMode>) {
  return async () => {
    await node.ready();
    if (node.mode === "fork") {
      node.setImpersonatedAccount("0x...");
      return { mode: "fork", impersonatedAccount: node.getImpersonatedAccount() };
    }
    return { mode: "normal" };
  };
}
 
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;
    },
  }));
  return enhancedNode;
}

Next Steps

Forking · State Management · Mining Modes · JSON-RPC · EVM Events · Custom Precompiles