Skip to content

Tevm clients guide

Tevm Clients

The interface to Tevm api is called TevmClient. This api provides a uniform API for interacting with Tevm whether interacting with a MemoryClient directly or remotely interacting via an HttpCLient. Tevm clients share the same actions based interface along with a request method for handling JSON-RPC requests.

The following clients are available

  • MemoryClient - An in memory instance of the EVM that can run in Node.js, bun or the browser
  • HttpClient - A client that talks to a remote MemoryClient running in an http server
  • Viem extensions - Provides a viem based client instance and some experimental optimistic updating apis.
  • 🚧 Under construction Ethers extensions - An ethers based memory client and http client
  • 🚧 Under construction WebsocketClient - A web socket based TevmClient similar to the HttpClient

Browser usage

The MemoryClient runs in the browser but may require top-level-await support to be added to your JavaScript build tool. It’s also possible you need some node-polyfills.

If using vite see the vite example for reference. Examples for other bundlers are coming soon.

Actions

The main interface for interacting with any Tevm client is it’s actions api. See actions api guide or the TevmClient reference for more information.

Procedures

Tevm also has an client.request method for doing JSON procedure calls. For MemoryClient this procedure call happens in memory and for HttpClient this procedure call is resolved remotely using JSON-RPC (json remote procedure call). For more information see the JSON-RPC docs and the client.request generated reference docs.

Modes

The underlying Tevm MemoryClient can run in three different modes

Normal mode

In normal mode the Tevm client initializes a new empty EVM instance in memory. It will have a chainId of 420 and start from block 0.

If you want to add any contracts to your normal clients the NormalMode supports the predeploy and precompile options as do all modes. You can also use client.setAccount to manually add bytecode to a contract address.

import { createMemoryClient } from 'tevm'
const memoryClient = createMemoryClient()
console.log(memoryClient.mode) // normal
console.log(await memoryClient.eth.blockNumber()) // 0n

Fork mode

Fork mode will fork a chain at a given block number defaulting to the latest block number when given an RPC url and an optional block tag. When you fork the chain all state is cached. No future changes in future blocks will be included in this forked chain. This mode is analogous to what happens with anvil if you use a forkUrl

Fork mode is initialized by setting the fork property in createMemoryClient

import { createMemoryClient } from 'tevm'
const memoryClient = createMemoryClient({
fork: {
url: 'https://mainnet.optimism.io'
}
})
console.log(memoryClient.mode) // forked
console.log(await memoryClient.eth.blockNumber()) // returns optimism block number at time of fork unless future actions simulate mining a new block
console.log(await memoryClient.eth.getStorageAt(...)) // Storage is always read from cache or fetched from the forked block

Proxy mode

Proxy mode is similar to ForkMode but always tracks latest block instead of forking. It will temporarily fork the chain for 2 seconds at a time. But if the cache is more than 2 seconds old the state manager will first check to see if the blockNumber has changed. If it does change it invalidates the cache,

Proxy mode is initialized by setting the proxy option in createMemoryClient

The expectedBlockTime property will configure how long the client waits before starting to check if block changed. It can be set arbitrarily large if you simply want to cache state for that period of time.

import { createMemoryClient } from 'tevm'
const memoryClient = createMemoryClient({
proxy: {
url: 'https://mainnet.optimism.io'
// optionally configure the expected block time in milliseconds
// defaults to 2_000 (2s)
expectedBlockTime: 10_000,
}
})
console.log(memoryClient.mode) // proxy
console.log(await memoryClient.eth.blockNumber()) // will return latest block number

Differences between forked and proxy mode

Fork and proxy mode are very similar with the only difference being how they handle cache invalidation. Fork mode forks the chain at a very specific block height and will never invalidate the cache. Proxy mode attempts to invalidate the cache whenever new blocks come in.

It is always suggested to use forked mode if you can get away with it.

  • Forked mode is much more RPC efficient via never invalidating the cache
  • Proxy mode works best if you expect external changes to the chain to potentially change the expected results of your contract calls
  • If you want a bit of both you can use proxy mode with a large expectedBlockTime to invalidate the cache infrequently

State persistence

It is possible to persist the tevm client to a syncronous source using the persister option. This will initialize the state with with the persisted storage if it exists and back up the state to this storage after state updates happen.

  • Note that proxy mode invalidates the cache every block so there isn’t much gained from persisting state in proxy mode
  • There is currently a known bug where fork mode will not persist the block tag and thus will be fetching state from a newer block when reinitialized.
  • The memory client still keeps the state in memory as source of truth even with state persistence. It is simply backing up the state to storage so it can rehydrate itself on future initializations
import {createMemoryClient, createSyncPersister} from 'tevm'
import {createMemoryClient} from 'tevm/sync-storage-persister'
// Client state will be hydrated and persisted from/to local storage
const clientWithLocalStoragePersistence = await createMemoryClient({
persister: createSyncPersister({
storage: localStorage
})
})