Skip to content

Getting Started with Viem

This guide will help you integrate Tevm with viem, the modern TypeScript interface for Ethereum. By the end, you'll have a working setup with Tevm Node and understand how to leverage viem's actions with Tevm.

Installation

Install Dependencies

First, install Tevm along with viem as a peer dependency:

npm
npm install tevm viem@latest

Create Your Client

For the quickest start, create a memory client:

import { createMemoryClient } from "tevm";
 
const client = createMemoryClient();

Or, to fork from an existing chain:

import { createMemoryClient, http } from "tevm";
import { optimism } from "tevm/chains";
 
const client = createMemoryClient({
  fork: {
    transport: http("https://mainnet.optimism.io"),
    common: optimism,
  },
});
 
// Wait for the node to be ready before using it
await client.tevmReady();

A MemoryClient is a batteries included client that includes all PublicActions, WalletActions, and TestActions from viem. It also includes special tevm specific actions prefixed with tevm* such as tevmCall and tevmSetAccount

You're Ready!

Start using your client with familiar viem actions:

// Get the current block number
const blockNumber = await client.getBlockNumber();
console.log(`Current block: ${blockNumber}`);

Complete Example

The following example demonstrates the key capabilities of Tevm with viem:

Forking Example
import { createMemoryClient, http } from "tevm"; 
import { optimism } from "tevm/common"; 
import { parseAbi, parseEther } from "viem"; 
 
// 1. Create a memory client forked from Optimism mainnet
const client = createMemoryClient({
  fork: {
    transport: http("https://mainnet.optimism.io"), 
    common: optimism, 
  }, 
}); 
 
// Wait for the node to be ready
await client.tevmReady();
 
// 2. Get current block number (from the fork point)
const blockNumber = await client.getBlockNumber();
console.log(`Current block number: ${blockNumber}`);
 
// Setup addresses and contract interfaces
const account = `0x${"baD60A7".padStart(40, "0")}` as const;
const greeterContractAddress = "0x10ed0b176048c34d69ffc0712de06CbE95730748";
 
// Define contract interfaces with parseAbi
const greeterAbi = parseAbi([
  "function greet() view returns (string)", 
  "function setGreeting(string memory _greeting) public", 
]); 
 
// 3. Modify blockchain state with test actions
// Fund our test account with 1 ETH
await client.setBalance({
  address: account,
  value: parseEther("1"),
});
 
// Read the current greeting using viem's readContract
const currentGreeting = await client.readContract({
  address: greeterContractAddress,
  abi: greeterAbi,
  functionName: "greet",
});
console.log(`Current greeting: ${currentGreeting}`);
 
// Update the greeting with writeContract
const txHash = await client.writeContract({

  account,
  address: greeterContractAddress,
  abi: greeterAbi,
  functionName: "setGreeting",
  args: ["Hello from Tevm!"],
  chain: optimism,
});
console.log(`Transaction sent: ${txHash}`);
 
// 4. Mine a new block to include our transaction
await client.mine({ blocks: 1 }); 
 
// Verify the greeting was updated
const updatedGreeting = await client.readContract({
  address: greeterContractAddress,
  abi: greeterAbi,
  functionName: "greet",
});
console.log(`Updated greeting: ${updatedGreeting}`);
Code Walkthrough

1. Imports & Client Creation

import { createMemoryClient, http } from "tevm";
import { optimism } from "tevm/common";
import { parseAbi, parseEther } from "viem";
 
const client = createMemoryClient({
  fork: {
    transport: http("https://mainnet.optimism.io"),
    common: optimism,
  },
});
 
await client.tevmReady();
  • We create a client that forks from Optimism mainnet
  • This gives us a local sandbox with all of mainnet's state
  • client.tevmReady() ensures the fork is complete before proceeding

2. Contract Interaction

// Define the contract interface
const greeterAbi = parseAbi([
  "function greet() view returns (string)",
  "function setGreeting(string memory _greeting) public",
]);
 
// Read from contract
const currentGreeting = await client.readContract({
  address: greeterContractAddress,
  abi: greeterAbi,
  functionName: "greet",
});
 
// Write to contract
const txHash = await client.writeContract({
  account,
  address: greeterContractAddress,
  abi: greeterAbi,
  functionName: "setGreeting",
  args: ["Hello from Tevm!"],
  chain: optimism,
});
  • The API matches viem exactly - anyone familiar with viem can use this immediately
  • Write operations return a transaction hash just like on a real network

3. Mining Control

// Mine a block to include our transaction
await client.mine({ blocks: 1 });
  • Unlike real networks, you control exactly when blocks are mined
  • This gives you complete determinism for testing and development

Key Viem-Compatible Features

Tevm's viem client implements the full viem API, maintaining compatibility while adding powerful features:

Standard viem API
// These standard viem actions work exactly as expected
await client.getBalance({ address: '0x...' })
await client.getBlockNumber()
await client.readContract({ ... })
await client.writeContract({ ... })
await client.estimateGas({ ... })
await client.sendTransaction({ ... })
// And all other viem actions

Common patterns and Best Practices

Creating multiple clients

It is common to create a viem client and a tevm client side by side.

import { createPublicClient, http } from "viem";
import { createMemoryClient } from "tevm";
import { optimism } from "tevm/common";
 
export const publicClient = createPublicClient({
  transport: http("https://mainnet.optimism.io"),
});
 
export const memoryClient = createMemoryClient({
  fork: {
    // use your public client as the fork transport
    transport: publicClient,
    // (comming soon)
    rebase: true,
  },
});
  • Generally you will still want to be using normal viem clients while building Tevm applications
  • As a best practice use your viem client as the transport so any caching viem does is shared with Tevm
  • tevm/common is a superset of a viem chain so it can be used for both

Racing JSON-RPC requests

When doing this a pattern you can do to improve the performance of your app is what is called racing. This is when you execute a call with tevm and viem and return the one that returns first.

function raceExample() {
  const {resolve, reject, promise} = Promise.withResolvers()
  // estimateGas with both viem and tevm in parallel resolving the one that finishes first
  publicClient.estimateGas(...).then(result => resolve(result))
  memoryClient.estimateGas(...).then(result => resolve(result))
 
  return promise
}

If the Tevm cache is warm it will finish much faster (nearly instantly) than the remote RPC call will. If the cache is cold the remote rpc call will finish first while Tevm warms the cache in background for next time. Racing allows you to improve the performance of your app.

Using the Tevm Bundler

The Tevm Bundler is an optional tool for importing contract abis into TypeScript and it is built for Wagmi, Viem, Ethers and Tevm. Users have reported using it with other tools like Ponder as well.

It is common to use the Tevm Bundler even when not using TevmNode as a TevmContract is a library agnostic typesafe instance representing a contract abi.

import { MyContract } from "./MyContract.sol";
 
function useExample() {
  return useReadContract({
    abi: MyContract.abi,
    address: `0x...`,
    method: "balanceOf",
    args: address,
  });
  // Alternatively use the typesafe `read.method()` api
  return useReadContract(
    MyContract.withAddress(`0x...`).read.balanceOf(address),
  );
}

Tree-Shakeable API

For production applications, especially in browser environments, you may want to use Tevm's tree-shakeable API to minimize bundle size:

import { createClient, http } from "viem";
import { createTevmTransport } from "tevm/transport"; 
import { tevmCall, tevmDumpState } from "tevm/actions"; 
 
// Create a standard viem client with Tevm transport
const client = createClient({
  transport: createTevmTransport({

    fork: {

      transport: http("https://mainnet.optimism.io"), 
    }, 
  }), 
});
 
// Import only the actions you need
await tevmDumpState(client);

To do this you use createTevmTransport which takes the same options as a memoryClient but unlike a MemoryClient only supports a client.request method.

You should ALWAYS use createTevmTransport rather than passing a TevmClient directly in as transport using custom(TevmNode).

Using viem to talk to Tevm over http

By default Tevm runs in memory but it does support running as a traditional http server as well. This can be useful if using Tevm as an anvil-like testing tool.

There are two ways to run tevm as a sever. THe easiest way is using the CLI

npx tevm serve --fork-url https://mainnet.optimism.io

Or you can run Tevm as a Http, Express, Hono, or Next.js server directly in node.js and Bun

import { createMemoryClient, http } from "tevm";
import { createServer } from "tevm/server";
 
const memoryClient = createMemoryClient();
const server = createServer(memoryClient);
 
server.listen(8545, () => {
  console.log("server started on port 8545");
  // test a request vs server
  http("http://localhost:8545")({})
    .request({
      method: "eth_blockNumber",
    })
    .then(console.log)
    .catch(console.error);
});

Once you start Tevm as a server you can talk to it using viem http as normal.

Tevm-Specific Actions

Tevm extends viem with specialized actions that provide enhanced capabilities:

ActionDescriptionUse Case
tevmCallLow-level EVM call with execution hooksDeep inspection of contract execution
tevmContractEnhanced contract interaction with EVM hooksDetailed debugging of contract calls
tevmDeployDeploy with execution hooksUnderstanding deployment execution flow
tevmMineControl block miningPrecise transaction inclusion control
tevmSetAccountModify account stateTest different account scenarios
tevmGetAccountRead detailed account stateInspect nonce, code, storage
tevmDumpStateExport full EVM stateState persistence and analysis
tevmLoadStateImport saved EVM stateRestore a specific state for testing
tevmReadyWait for fork to initializeEnsure node is ready before use

Hook into the EVM

One of Tevm's most powerful features is the ability to hook directly into EVM execution using the tevmCall and tevmContract actions:

await client.tevmContract({
  address: greeterContractAddress,
  abi: greeterAbi,
  functionName: "setGreeting",
  args: ["Hello!"],
 
  // onStep is called for each EVM operation 
  onStep: (stepInfo, next) => {

    console.log(`Executing: ${stepInfo.opcode.name} at PC=${stepInfo.pc}`); 
    console.log(`Stack: ${stepInfo.stack.map((val) => val.toString())}`); 
    console.log(`Memory: ${stepInfo.memory.toString("hex")}`); 
 
    // You can also modify EVM state here if needed 
 
    // Call next() to continue execution 
    next?.(); 
  }, 
 
  // You can also access the detailed result after execution
  onResult: (result) => {
    console.log(`Gas used: ${result.executionGasUsed}`);
    console.log(`Return value: 0x${result.returnValue?.toString("hex")}`);
  },
});

This enables advanced use cases like:

  • Visual Debuggers: Create step-by-step transaction debuggers
  • Educational Tools: Explain EVM execution for learning purposes
  • Custom Instrumentation: Profile and analyze contract execution
  • Intercepting Execution: Modify execution behavior for testing

Next Steps

Now that you're familiar with using Tevm with viem, you can:

Explore More Tevm Features

Dive deeper into Tevm's powerful capabilities:

Check Out Examples

See how Tevm solves real-world problems:

Advanced API Usage

Master the Tevm API for more sophisticated applications:

← Back to Overview

Ethers Integration →