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 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:
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:
// 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:
Action | Description | Use Case |
---|---|---|
tevmCall | Low-level EVM call with execution hooks | Deep inspection of contract execution |
tevmContract | Enhanced contract interaction with EVM hooks | Detailed debugging of contract calls |
tevmDeploy | Deploy with execution hooks | Understanding deployment execution flow |
tevmMine | Control block mining | Precise transaction inclusion control |
tevmSetAccount | Modify account state | Test different account scenarios |
tevmGetAccount | Read detailed account state | Inspect nonce, code, storage |
tevmDumpState | Export full EVM state | State persistence and analysis |
tevmLoadState | Import saved EVM state | Restore a specific state for testing |
tevmReady | Wait for fork to initialize | Ensure 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:
- Forking capabilities to simulate production chains
- State management for manipulating the blockchain
- Mining modes for controlling transaction inclusion
- Direct Solidity imports with the Tevm Bundler
Check Out Examples
See how Tevm solves real-world problems:
- Forking mainnet for production simulation
- Building a debugger UI with EVM insights
- Local testing flows for development
Advanced API Usage
Master the Tevm API for more sophisticated applications:
- EVM events & hooks for detailed execution analysis
- Custom precompiles for extending the EVM
- Transaction pool management for pending transaction control
- Contract Bundler for importing Solidity files directly