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
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
💾 State Management
Understand the state management capabilities of Tevm
⛏️ Mining Modes
Explore the different block mining strategies🌐 JSON-RPC Support
Use Tevm's Ethereum-compatible JSON-RPC interface
🔔 EVM Events
Work with low-level EVM execution events🧩 Custom Precompiles
Add custom precompiled contracts to extend EVM functionality