Skip to content

Quick start guide

Tevm Quick Start Guide

Introduction

This guide will get you familiar with the most essential features of Tevm and start interacting with the Ethereum Virtual Machine (EVM) in Node.js or browser environments. By the end of this guide you will understand:

  1. How to create a forked EVM in JavaScript using createMemoryClient
  2. How to write, build, and execute solidity scripts with a TevmClient
  3. How to streamline your workflow using tevm contract imports with the tevm bundler
  4. How to write solidity scripts with the tevm script action

Prerequisites

This tutorial uses Bun but you can follow along in Node.js >18.0 as well. Bun can be installed with NPM.

Terminal window
npm install --global bun

For more details visit the Bun Installation Guide.

Creating Your Tevm Project

  1. Create a new project directory:
Terminal window
mkdir tevm-app && cd tevm-app
  1. Initialize your project with bun init:
Terminal window
bun init
  1. Install tevm
Terminal window
bun install tevm

Creating a Tevm VM

Now let’s create a Tevm VM to execute Ethereum bytecode in our JavaScript

  1. Open the index.ts file

  2. Now initialize a MemoryClient with createMemoryClient

import { createMemoryClient } from 'tevm';
const tevm = await createMemoryClient();

This initializes an an ethereum VM instance akin to starting anvil but in memory.

Using ethereum JSON-RPC

The entrypoint to using Tevm is TevmClient.request. It implements the

Let’s use eth_getBalance

  1. Create a eth_getBalance
import { createMemoryClient } from 'tevm';
import { EthGetBalanceJsonRpcRequest } from 'tevm/procedures-types'
const tevm = await createMemoryClient();
const request: EthGetBalanceJsonRpcRequest = {
jsonrpc: '2.0',
id: 1,
method: 'eth_getBalance',
params: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"]
}
  1. Now pass our request into @tevm/request
import { createMemoryClient } from 'tevm';
import { EthGetBalanceJsonRpcRequest } from 'tevm/procedures-types'
const tevm = await createMemoryClient();
const request: EthGetBalanceJsonRpcRequest = {
jsonrpc: '2.0',
id: 1,
method: 'eth_getBalance'
params: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "latest"]
}
const response = await tevm.request(request)
console.log(response.error)
console.log(response.result)
  1. Now run it
Terminal window
bun run index.ts

This address has 0 eth because we have a brand new vm. Let’s make our VM fork ethereum now.

Forking a live network

Similar to anvil or ganache Tevm has the ability to fork a live network.

  1. Update createMemoryClient to fork ethereum using forkUrl

Add any ethereum RPC url to the options.fork.url

import { createMemoryClient } from 'tevm';
import { EthGetBalanceJsonRpcRequest } from 'tevm/procedures-types'
const tevm = await createMemoryClient({
fork: {
url: 'https://mainnet.optimism.io'
}
});
const request: EthGetBalanceJsonRpcRequest = {
jsonrpc: '2.0',
id: 1,
method: 'eth_getBalance',
params: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"]
}
const response = await tevm.request(request)
console.log(response)

Tevm will fork latest block by default.

Now run script again vs the forked network

Terminal window
bun run index.js

This is equivelent to issuing a JSON-RPC request directly to the RPC

fetch("https://mainnet.optimism.io", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'eth_getBalance',
params: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"]
})
})
.then(response => response.json())
.then(console.log);

But with MemoryClient all requests will be issued to the same block number we forked and cached in memory once made.

Using Actions API to execute a contract

Tevm exposes a viem-like actions api to provide a higher level of abstraction than the JSON-RPC interface.

  1. Replace tevm_getAccount JSON-RPC procedure with the getAccount action
import { createMemoryClient } from 'tevm';
- import { EthGetBalanceJsonRpcRequest } from 'tevm/procedures-types'
const tevm = await createMemoryClient({
fork: {
url: 'https://mainnet.optimism.io'
}
});
- const request: EthGetBalanceJsonRpcRequest = {
- jsonrpc: '2.0',
- id: 1,
- method: 'eth_getBalance',
- params: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"]
- }
+ const address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
- const response = await tevm.request(request)
+ const result = await tevm.eth.getBalance({address})
- console.log(response)
+ console.log(result)
  1. Handle errors

Let’s see what happens when we throw an error

const {errors} = await tevm.setAccount({address: '0xnot a valid address', balance: BigInt(0)})
console.log(errors)

All tevm actions return errors as values. They do not every throw. This is consistent with the JSON-RPC api.

  1. Run a transaction

Now send a transaction using TevmClient.call. This is equivelent to using eth_call.

Tevm.call wraps tevm_call which is similar to eth_call or Tevm.eth.call but has extra parameters for modifying the VM.

import { createMemoryClient, parseEth } from 'tevm';
const tevm = await createMemoryClient({
fork: {
url: 'https://mainnet.optimism.io'
}
});
const fromAddress = `0x${'01'.repeat(20)}` as const
const toAddress = `0x${'02'.repeat(20)}` as const
// skipBalanceCheck will mint any eth if account has less than 0
await tevm.call({
from: fromAddress,
to: toAddress,
value: parseEth('1'),
skipBalanceCheck: true
})
const balance = await tevm.eth.balanceOf({address: toAddress})
console.log(balance)
  1. Now run script again to see the expected result of running the contract call.
Terminal window
bun run vm.js

To see more options check out CallParams docs

Executing contract calls

We can execute a contract call by sending encoded contract data just like eth_call

  1. Use encodeFunctionData to pass in a contract call to tevm.call
import { createMemoryClient, encodeFunctionData, decodeFunctionData, parseAbi } from 'tevm';
const tevm = await createMemoryClient({
fork: {
url: 'https://mainnet.optimism.io'
}
});
const abi = parseAbi(['function balanceOf(address owner) returns (uint256 balance)'])
const owner = `0x${'01'.repeat(20)}` as const
const contractAddress = `0x${'02'.repeat(20)}` as const
const {rawData} = await tevm.call({
to: contractAddress,
data: encodeFunctionData({
args: [owner]
functionName: 'balanceOf',
abi,
})
})
const balance = decodeFunctionData({
functionName: 'balanceOf',
abi,
data: rawData
})
console.log(balance)
  1. Use TevmClient.contract

Rather than encoding and decoding data with TevmClient.call we can instead use the TevmClient.contract method. It wraps the eth_call JSON-rpc method and matches much of viems readContract API but with some extra VM control.

Refactor our call to use Tevm.contract

import { createMemoryClient, parseAbi } from 'tevm';
const tevm = await createMemoryClient({
fork: {
url: 'https://mainnet.optimism.io'
}
});
const owner = `0x${'01'.repeat(20)}` as const
const contractAddress = `0x${'02'.repeat(20)}` as const
const {data: balance} = await tevm.contract({
to: contractAddress,
args: [owner],
functionName: 'balanceOf',
abi: parseAbi(['function balanceOf(address owner) returns (uint256 balance)']),
})
console.log(balance)

Scripting with Tevm

In the previous section we called the Dai which is deployed to optimism. But Tevm can also execute arbitrary contracts that are not deployed.

We could use the tevm.setAccount feature to deploy bytecode

await tevm.setAccount({address: `0x${'42'.repeat(20)}`, deployedBytecode: '0x000'})

And then use tevm.contract as we did in last section.

But Tevm provides a convenient tevm_script JSON-RPC request and matching TevmClient.script action

  1. First let’s make a new solidity file
Terminal window
touch HelloWorld.s.sol
  1. Next write a simple HelloWorld contract
// SPDX-License-Identifier: MIT
pragma solidity >0.8.0;
contract HelloWorld {
function greet(string memory name) public pure returns (string memory) {
return string(abi.encodePacked("Hello ", name));
}
}

We now need a way of turning our contract into bytecode.

  1. 🚧 Use Tevm to compile the contract into bytecode and abi (not yet implemented but will be soon)
Terminal window
bunx tevm compile HelloWorld.s.sol

You should see a .js file get generated with the JavaScript version of your contract. Inspect the file. We will talk more about this later.

  1. Import contract and use it in a Tevm.script action.
import { HelloWorld } from './HelloWorld.s.sol.js';
import { createMemoryClient, encodeFunctionData, parseAbi } from 'tevm';
const tevm = await createMemoryClient({
fork: {
url: 'https://mainnet.optimism.io'
}
});
const scriptResult = await tevm.script({
abi: HelloWorld.abi,
deployedBytecode: HelloWorld.deployedBytecode,
functionName: 'greet',
args: ['Vitalik'],
});
console.log(scriptResult.data); // Hello Vitalik!

Now run the script

Terminal window
bun run script.js

Working with Contract Action Creators

Tevm offers an streamlined typesafe dev experience for working with solidity scripts and contracts via TevmContracts.

TevmContracts are created via using the createContract method. You may have noticed it being used in the HelloWorld.s.sol.js file.

Let’s refactor our script code to take advantage of tevm contracts actionCreators.

- const scriptResult = await tevm.script({
- abi: HelloWorld.abi,
- functionName: 'greet',
- args: ['Vitalik'],
- deployedBytecode: HelloWorld.deployedBytecode
- });
+ const scriptResult = await tevm.script(
+ HelloWorld.read.greet('Vitalik')
+ );

Build Contracts and scripts directly from JavaScript imports

Remember before we used tevm generate to generate JavaScript from our contract. Tevm offers tooling to do this automatically. After installing this tooling you can simply just import your contract directly.

import {HelloWorld} from './MyContract.sol'

This direct solidity import will be recognized by Bun and Tevm at build time and automatically generate the JavaScript and typescript types behind the scene. You will also get enhanced LSP support in your editors such as Vim or VSCode. This includes

  • Instant update whenever the contract changes without an additional generation step
  • Great typesafety
  • Go-to-definition taking you directly to the solidity line of code a given method is defined
  • Natspec definitions on hover

First let’s configure Bun to recognize solidity files

  1. Install the @tevm/bundler package
Terminal window
bun install @tevm/bundler

This package installs two tools we need:

  1. Create a plugin.js file to install the Tevm bun plugin into Bun
import { tevmBunPlugin } from '@tevm/bundler/bun-plugin';
import { plugin } from 'bun';
plugin(tevmBunPlugin({}));
  1. Now add the plugin.js file to the bunfig.toml to tell bun to load our plugin in normal mode and dev mode
preload = ["./plugins.js"]
[test]
preload = ["./plugins.js"]
  1. Remove the generated files from before
Terminal window
rm -rf HelloWorld.sol.js HelloWorld.sol.d.ts
  1. Now rerun bun
Terminal window
bun run script.ts

You will see bun still generated the same files and cached them in the .tevm folder this time. The plugin is taking care of this generation for you whenever you run bun.

  1. Configure the TypeScript LSP

Though bun is working you may notice your editor is not recognizing the solidity import. We need to also configure the TypeScript language server protocol that your editor such as VIM or VSCode uses.

Add {"name": "@tevm/bundler/ts-plugin"} to compilerOptions.plugins array to enable tevm in typescript language server.

{
"compilerOptions": {
"plugins": [
{"name": "@tevm/plugin/ts-plugin"}
]
}
}

Note: ts-plugins only operate on the language server. Running tsc from command line will still trigger errors on solidity imports. A command line tool for this is coming soon.

Use external contracts

Contracts from external repo can be used via installing with npm.

Terminal window
bun install @openzeppelin/contracts -D

You can now use any common contract implementation in your code via extending the contracts or importing them directly into JavaScript.

The following executes a ERC721 balanceOf call against a forked contract on mainnet.

import { ERC721 } from '@openzeppelin/contracts/tokens/ERC721/ERC721.sol'
import { createMemoryClient } from './vm.js'
// Note it is recomended to use a more reliable rpc provider than the free tier cloudflare rpc
const result = await createMemoryClient({fork: {url: 'https://cloudflare-eth.com'}}).contract(
ERC721
.withAddress('0x5180db8F5c931aaE63c74266b211F580155ecac8')
.balanceOf(' 0xB72900a2e885dF6A2824969B6e40B969C8ae3CB7')
)
console.log(result)

Summary

Congrats. You now have learned all the basics you need to start building with Tevm. Consider joining the telegram to discuss Tevm.