Skip to content

Getting started guide

Tevm Getting Started Guide

Introduction

We will be creating a simple counter app using the following technologies:

  • Tevm + Viem
  • HTML + TypeScript to build an ui with no framework
  • Vite + Tevm Bundler as a minimal build setup and dev server

This guide intentionally uses a straightforward setup to focus on the most essential features of Tevm, so every piece is understood.

Prerequisites

Creating your Tevm project

1. Create a new project directory:

Terminal window
mkdir tevm-app && cd tevm-app

2. Initialize your project

Terminal window
npm init --yes

3. Install tevm and viem as runtime dependencies

Terminal window
npm install tevm viem

4. Install vite and typescript as buildtime dependency

Vite will provide us a pretty minimal setup to import TypeScript into our HTML and start a dev server

Terminal window
npm install --save-dev vite typescript

Scaffold the initial project

1. Create a tsconfig

The following config works quite nicely with vite apps.

Feel free to use any tsconfig you prefer as long as it

  • uses strict mode
  • supports bigint (ES2020 or later)

See the tsconfig docs for more information about these options.

Terminal window
echo '{
"compilerOptions": {
"target": "ES2021",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2021", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}' > tsconfig.json

2. Create an HTML file

The HTML file will be the entrypoint to our app. Add it to index.html

Terminal window
echo '
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tevm Example</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
' > index.html

3. Add a typescript file

You will see the HTML file is importing a src/main.ts file in a script tag. Go ahead and add that too

Terminal window
mkdir src && echo '
const app = document.querySelector("#app") as Element;
app.innerHTML = `<div>Hello Tevm</div>`;
' > src/main.ts

Now double check that you can run your application

Terminal window
npx vite .

Hit o key and then <Enter> to open up http://localhost:5173 in your browser

You should see Hello Tevm rendered

Consider adding this command to your package.json for convenience

"scripts": {
"dev": "npx vite ."
}

4. Add polyfills

To use Tevm and viem in the browser it is necessary to add some polyfills. These will inject code into your final build that will allow apis that do not exist in the browser natively to work.

Install a polyfill library to polyfill node

Terminal window
npm i --save-dev vite-plugin-node-polyfills

Then create a basic vite config. This will build our app with ‘stream’, ‘process’, ‘Buffer’, and ‘global’ polyfills

If you are using Tevm in node.js or bun no polyfills are necessary. Over time tevm has been and will continue to remove the need for polyfills.

Terminal window
echo '
import { defineConfig } from "vite"
import { nodePolyfills } from "vite-plugin-node-polyfills"
// https://vitejs.dev/config/
export default defineConfig({
define: {
global: "globalThis",
},
plugins: [
nodePolyfills({
include: ["stream"],
globals: {
process: true,
Buffer: true,
global: true,
},
}),
],
})
' > vite.config.js

Restart your vite server and now vite has everything it needs to run an Ethereum devnet in the browser.

Create MemoryClient

Now let’s create a MemoryClient. A memory client is a viem client using an in-memory transport. This means instead of sending requests to an RPC provider like alchemy it will be processing requests with tevm in memory in a local EVM instance running in JavaScript.

Memory client is similar to anvil. It can:

  • Optionally fork an existing network
  • Run special scripts that have advanced functionality
  • Extremely hackable. Can mint yourself eth, run traces, modify storage, and more

1. In the src/main.ts file initialize a MemoryClient with createMemoryClient

import { createMemoryClient, http } from "tevm";
import { optimism } from "tevm/common";
const app = document.querySelector("#app") as Element;
const memoryClient = createMemoryClient({
common: optimism,
fork: {
// @warning we may face throttling using the public endpoint
// In production apps consider using `loadBalance` and `rateLimit` transports
transport: http("https://mainnet.optimism.io")({}),
},
});
async function runApp() {
app.innerHTML = `<div id="status">initializing...</div>
<div id="blocknumber"></div>
`;
const status = app.querySelector("#status")!;
document.querySelector("#blocknumber")!.innerHTML =
`Fetching block number next step. For now let's check out which methods are on memory client:
<ul>
${Object.keys(memoryClient).map((key) => `<li>${key}</li>`)}
</ul>`;
status.innerHTML = "Done";
}
runApp();

When we fork a network the blocknumber will be pinned to the block number at the time of the fork. As you mine new blocks you will not get updates from the chain unless you refork it.

It is recomended you also pass in a chain object when forking. This will improve the performance of forking as well as guarantee tevm has all the correct chain information such as which EIPs and hardforks to use. A TevmChain is different from a viem chain in that it extends viem chains with the ethereumjs/common interface.

One can use a viem wallet client or add the wallet actions to the client

Viem client API

The core apis for tevm are the public viem actions api. All public actions are supported.

Let’s use getBlockNumber action from viem to populate the blocknumber div.

async function runApp() {
app.innerHTML = `<div id="status">initializing...</div>
<div id="blocknumber"></div>
`;
const status = app.querySelector("#status")!;
status.innerHTML = "Fetching block number...";
const blockNumber = await memoryClient.getBlockNumber();
document.querySelector("#blocknumber")!.innerHTML =
`ForkBlock: ${blockNumber}`;
status.innerHTML = "Done";
}

Tevm account actions

In addition to the viem api there are also powerful tevm specific actions. Let’s start with the account actions tevmSetAccount and tevmGetAccount

1. Add a div to add some of our account information

app.innerHTML = `<div id="status">initializing...</div>
<div id="blocknumber"></div>
<div>
Address: <span id="address"></span>
</div>
<div>
Nonce: <span id="nonce"></span>
</div>
<div>
Balance: <span id="balance"></span>
</div>
`;

2. Add function that displays account balance

After adding you should see it throw an AccountNotFoundError in the chrome console.

// addresses and abis must be as const for tevm types
const address = `0x${"0420".repeat(10)}` as const;
async function updateAccounts() {
// we are setting throwOnFail to false because we expect this to throw an error from the account not existing
const account = await memoryClient.tevmGetAccount({
address,
throwOnFail: false,
});
if (account.errors) {
// this will error
console.error("Unable to get account", account.errors);
return;
}
console.log(account); // console log the account to get familiar with what properties are on it
document.querySelector("#address")!.innerHTML = address;
document.querySelector("#nonce")!.innerHTML = String(account.nonce);
document.querySelector("#balance")!.innerHTML = String(account.balance);
}
function runApp() {
...
status.innerHTML = 'Updating accounts...'
await updateAccounts()
status.innerHTML = "Done";
}
  • tevmGetAccount will throw if the account is uninitialized (e.g. it has never been touched by the EVM)
  • Tevm has ability to both return errors as typed values as well as throw them using throwOnFail prop. It is recomend to return as values and always handle them for production applications.
  • Tevm errors are strongly typed though at this moment not every error is accounted for. They come with a strongly typed name property as well as a helpful error message.

3. Use tevmSetAccount to initialize the account

Use tevmSetAccount to initialize the account with some eth and fix the “Account not found” error

async function runApp() {
...
status.innerHTML = "Setting account...";
const setAccountResult = await memoryClient.tevmSetAccount({
address,
balance: 420n,
throwOnFail: false,
});
if (setAccountResult.errors) console.error(setAccountResult.errors);
status.innerHTML = "Updating account...";
await updateAccounts();
status.innerHTML = "done";
}

Now we should see the account balance of 420n and a nonce of 0n.

Quick note on prefunded accounts

For convenience the following accounts are prefunded with eth. These are the same accounts anvil and hardhat prefunds.

'0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
'0x70997970C51812dc3A010C7d01b50e0d17dc79C8'
'0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC'
'0x90F79bf6EB2c4f870365E785982E1f101E93b906'
'0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65'
'0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc'
'0x976EA74026E726554dB657fA54763abd0C3a0aa9'
'0x14dC79964da2C08b23698B3D3cc7Ca32193d9955'
'0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f'
'0xa0Ee7A142d267C1f36714E4a8F75612F20a79720'

These can be imported from tevm via the prefundedAccounts export.

import { prefundedAccounts } from "tevm";
console.log(prefundedAccounts[0]);

Anytime you create a transaction it will default to the first prefunded account as the msg.sender unless overridden by an explicit from, caller, or origin prop.

Executing the EVM with tevmCall

Tevm can execute the EVM using viem methods such as memoryClient.call, memoryClient.readContract, memoryClient.estimateGas, etc. It also supports some wallet methods such as eth_sendRawTransaction.

Tevm also has its own powerful method for executing the evm called tevmCall. It’s like a normal ethereum call but with extra superpowers to do things such as

It also happens to be the shared code that supports executing all other call-like methods so it can do everything.

Send eth using tevmCall

import { prefundedAccounts } from "tevm";
...
async function runApp() {
app.innerHTML = `<div id="status">initializing...</div>
<div id="blocknumber"></div>
<div>
Address: <span id="address"></span>
</div>
<div>
Nonce: <span id="nonce"></span>
</div>
<div>
Balance: <span id="balance"></span>
</div>
`;
const status = app.querySelector("#status")!;
status.innerHTML = "Fetching block number...";
const blockNumber = await memoryClient.getBlockNumber();
document.querySelector("#blocknumber")!.innerHTML =
`ForkBlock: ${blockNumber}`;
status.innerHTML = "Setting account...";
const callResult = await memoryClient.tevmCall({
// this is the default `from` address so this line isn't actually necessary
from: prefundedAccounts[0],
to: address,
value: 420n,
throwOnFail: false,
});
// aggregate error is a good way to throw an array of errors
if (callResult.errors) throw callResult.errors;
status.innerHTML = "Updating account...";
await updateAccounts();
status.innerHTML = "done";
}

Fix bug using createTransaction

After we run this code we should see an error in our console. The balance never updated even though there are no errors. This is because we just did a normal call which executes against the EVM but doesn’t actually update any state. This is the default and best used for simply reading the evm. Let’s fix this using createTransaction.

const callResult = await memoryClient.tevmCall({
// this is the default `from` address so this line isn't actually necessary
from: prefundedAccounts[0],
to: address,
value: 420n,
// on-success will only create a transaction if the initial run of it doesn't revert
createTransaction: "on-success",
});
if (callResult.errors) console.error(callResult.errors);

We should still see the balance not getting updated. What gives?

Well we did successfully create a transaction which we can see by checking the tx hash

console.log(callResult.txHash);

If we remove the createTransaction: true the txHash will not be there. However, the transaction has not been mined. It is currently in the mempool. Let’s see it using a low level API getTxPool()

// the tevm means this api is not guaranteed to remain stable
const mempool = await memoryClient.tevm.getTxPool();
console.log(await mempool.getBySenderAddress(createAddress(prefundedAccounts[0])));

Fix second bug using tevmMine

While cheat methods like tevmSetAccount will immediately update the state for the current block. call methods like tevmCall will not update the state until a new block is mined.

Currently tevm only supports manual mining but in future versions it will support other modes including automining, gasmining and intervalmining. To mine a block simply call tevm.mine(). It will sort the mempool based on priority fees and nonces and mine all transactions up until the block gas limit.

First delete the mempool code and then replace it with a memoryClient.tevmMine()

status.innerHTML = "Sending eth to account...";
const callResult = await memoryClient.tevmCall({
// this is the default `from` address so this line isn't actually necessary
from: prefundedAccounts[0],
to: address,
value: 420n,
createTransaction: true,
});
if (callResult.errors) throw callResult.errors;
status.innerHTML = "Mining block";
const mineResult = await memoryClient.tevmMine();
if (mineResult.errors) throw mineResult.errors;
console.log(mineResult.blockHashes);
status.innerHTML = "Updating account...";
await updateAccounts();
status.innerHTML = "done";

Now that we mined a block we should finally see our account balance update.

Deploy a contract with tevmDeploy

All call-like endpoints for tevm use tevmCall under the hood including eth_call, debug_traceCall, eth_sendRawTransaction, and some special tevm methods like tevmContract, and tevmDeploy.

Note we could use tevmCall and the encodeDeployData. Using tevmDeploy is a lot more ergonomic. tevmDeploy has access to all the special cheat properties that a normal tevmCall has.

We also could use tevmSetAccount and manually set the deployedBytecode and any contract storage we want to set. This is a fine way to do it as well and often the most convenient if you don’t need to execute the constructor code. For our simple contract we will use tevmDeploy here.

1. Use tevmDeploy to deploy a contract.

// tevm/contracts has utils for creating `contracts` and `scripts` which we will cover later
// it also offers small library of commonly used contracts
import { SimpleContract } from "tevm/contract";
async function runApp() {
// go ahead and delete the old code
const initialValue = 420n;
const deployResult = await memoryClient.tevmDeploy({
from: prefundedAccounts[0],
abi: SimpleContract.abi,
// make sure to use bytecode rather than deployedBytecode since we are deploying
bytecode: SimpleContract.bytecode,
args: [initialValue],
});
if (deployResult.errors) throw deployResult.errors;
status.innerHTML = `Mining contract deployment tx ${deployResult.txHash} for contract ${deployResult.createdAddress}...`;
// remember to mine!
await memoryClient.tevmMine();
status.innerHTML = `updating ui to reflect newly mined tx ${deployResult.txHash} deploying contract ${deployResult.createdAddress}...`;
// Pass in the contract address to updateAccounts
// we will update this function to display contract info in next step
await updateAccounts(deployResult.createdAddress as Address);
status.innerHTML = "Done";
}

2. Update the html to display contract information

async function runApp() {
app.innerHTML = `<div id="status">initializing...</div>
<div id="blocknumber"></div>
<div>
Address: <span id="address"></span>
</div>
<div>
Nonce: <span id="nonce"></span>
</div>
<div>
Balance: <span id="balance"></span>
</div>
<h1>Counter contract</h1>
<!-- Contract info -->
<table border="1" id="contractInfo">
<thead>
<tr id="contractInfoHeader">
<!-- We will fill this in in js -->
</tr>
</thead>
<tbody>
<tr id="contractInfoRow">
<!-- We will fill this in in js -->
</tr>
</tbody>
</table>
`;
...
}

3. Update updateAccounts to display the contract in html

Now use tevmGetAccount to fill in the table information in updateAccounts. This way we can be assured our contract is deployed correct.

Pass in the contract address as a param

import { createMemoryClient, http, type Address } from "tevm";
...
// const address = `0x${"0420".repeat(10)}` as const;
async function updateAccounts(address: Address) {
const account = await memoryClient.tevmGetAccount({
address,
throwOnFail: false,
});
if (account.errors) throw account.errors
console.log(account); // console log the account to get familiar with what properties are on it
document.querySelector("#address")!.innerHTML = address;
document.querySelector("#nonce")!.innerHTML = String(account.nonce);
document.querySelector("#balance")!.innerHTML = String(account.balance);
// Update contract account info
const contractAccount = await memoryClient.tevmGetAccount({
address: contractAddress,
throwOnFail: false,
returnStorage: true,
});
if (contractAccount.errors) throw contractAccount.errors;
const header = document.querySelector("#contractInfoHeader")!;
const info = document.querySelector("#contractInfoRow")!;
header.innerHTML = `<tr>Address</tr>
<tr>deployedBytecode</tr>
${Object.keys(contractAccount.storage ?? []).map((storageSlot) => `<tr>${storageSlot}</tr>`)}
`;
info.innerHTML = `<tr>${contractAccount.address}</tr>
<tr>${contractAccount.deployedBytecode}</tr>
${Object.values(contractAccount.storage ?? []).map((storageValue) => `<tr>${storageValue}</tr>`)}
`;
}
...

We should see the contract information show up in our html now.

Using the tevmContract action

1. Use tevmContract to write to our contract

tevmContract is has a similar api to readContract from viem and has the following advantages over a normal tevmCall.

  • automatically encodes the call data without needing to manually use encodeFunctionData
  • automatically decodes the return data without needing to manually use decodeFunctionResult
  • automatically decodes any revert messages without needing to manually use decodeErrorResult
  • throws useful warnings such as no contract bytecode existing at the contract address

Let’s use tevm.contract to both read and write to the contract we just deployed. Feel free to add the results of these calls to the dom we will just console log them for now in this tutorial.

This particular contract has two methods. get to get the stored value and set to set the stored value.

For convienience we will also call Contract.withAddress to add the deployed address to the contract instance

async function runApp() {
...
const deployedContract = SimpleContract.withAddress(deployResult.createdAddress as Address);
status.innerHTML = "Querying contract with tevmContract..."
const readResult = await memoryClient.tevmContract({
abi: deployedContract.abi,
functionName: "get",
to: deployedContract.address,
});
if (readResult.errors) throw contractResult.errors;
console.log(readResult.rawData); // returns the raw data returned by evm
console.log(readResult.data); // returns the decoded data. Should be the initial value we set
console.log(readResult.executionGasUsed); // returns the execution gas used (won't include the data cost or base fee)
// console log the entire result to become familiar with what all gets returned
const newValue = 10_000n
status.innerHTML = `Current value ${contractResult.data}. Changing value to ${newValue}`
// just like tevmCall we can write with `createTransaction: true`
// remember the default `from` address is `prefundedAccounts[0]` when not specified!
const writeResult = await memoryClient.tevmContract({
createTransaction: true,
abi: deployedContract.abi,
functionName: "set",
to: deployedContract.address,
args: [newValue],
});
status.innerHTML =`Current value ${contractResult.data}. Changing value to ${newValue}. Mining tx ${writeResult.txHash}`
// remember to mine
const mineResult = await memoryClient.tevmMine();
// feel free to double check the value actually changed by calling tevmContract again!
status.innerHTML = `Value changed in block ${mineResult.blockHashes?.join(',')}. Updating storage in html...`;
// now let's refresh the account information to update storage
await updateAccounts(deployResult.createdAddress as Address);
}

2. Use getTransactionReceipt to get the receipt

Remember after mining new blocks getTransactionReceipt will return the receipt

const receipt = await memoryClient.getTransactionReceipt({
hash: writeResult.txHash as Hex,
});
console.log(receipt);

3. Refactor contract code to use contract action creators

Tevm ships with contract action creators which compose with tevmContract as well as viem methods such as readContract.

These action creators are even more powerful when combined with the tevm compiler which we will cover later in this guide.

Refactor our contract call to use the contract action creator. Note: the returned value of the action creator matches exactly what we had before.

const deployedContract = simpleContract.withAddress(deployResult.address)
const readResult = await memoryClient.tevmContract(
deployedContract.read.get()
);
...
const writeResult = await memoryClient.tevmContract({
createTransaction: true,
...deployedContract.write.set(newValue)
});

Compiling contracts with the Tevm bundler

Tevm not only supplies a runtime EVM but also a buildtime tool for building your contracts within your JavaScript projects. It will compile your contracts for you via simply importing a solidity file.

In future versions whatsabi integration will also be added to be able to pull contracts that are deployed to live networks.

This bundler will give you a lot of great features such as

  • natspec on hover
  • go-to-definition taking you directly to the solidity contract definitions
  • automatically recompiling when you change the contract code
  • support for most bundlers including webpack, vite, esbuild, rollup, and bun
  • typesafe feedback whenever you change your contract code

Gif showing lsp features

Tevm supports all major bundlers including vite, rollup, webpack, rspack, bun and esbuild. If your bundler is not supported open an issue it’s likely a light lift to add support.

1. Configure vite

Configuring vite will allow vite to recognize solidity imports. When it sees solidity it will compile it into the abi and bytecode to make a tevm contract just like we made manually in counterContract.ts

Add the viteExtensionTevm to your vite config under plugins

Terminal window
import { defineConfig } from 'vite'
import { nodePolyfills } from 'vite-plugin-node-polyfills'
import { vitePluginTevm } from 'tevm/bundler/vite-plugin'
// https://vitejs.dev/config/
export default defineConfig({
define: {
global: 'globalThis',
},
plugins: [
vitePluginTevm(),
nodePolyfills({
include: ['stream'],
globals: {
process: true,
Buffer: true,
global: true,
},
}),
],
})

3. Configure the LSP

Configuring a typescript plugin allows any editor such as VSCode or neovim to recognize the correct type of contract imports. This should be added to your tsconfig automatically via restarting your vite serve.

To add it manually, add the @tevm/ts-plugin to the typescript config. It will be automatically added if it doesn’t already exist.

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

Now restart your editor/lsp and typescript will now be able to recognize your contract imports.

Note: If using vscode you will need to set the workspace version to load ts-plugins

4. Add a counter contract

Now that vite can compile solidity we can add a contract.

Note: we must name our contract with a .s.sol extension rather than .sol. Compiling bytecode is expensive and usually unnecessary for contracts that are already deployed thus the compiler will only do it for files marked with .s.sol

If your contract doesn’t have an .s.sol extension you can simply reexport it from a .s.sol file and target that file.

Terminal window
mkdir contracts && touch contracts/Counter.s.sol

Then add the contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Counter {
uint256 public count;
constructor() {
count = 0;
}
function increment() public {
count += 1;
}
}

5. Import the contract and console.log it

import { Counter } from "../contracts/Counter.s.sol";
console.log(Counter);

What is happening is vite is compiling your contract into it’s bytecode and abi and returning a tevm Script object. With this script object we can really easily generate arguments to pass to viem.readContract or tevm.contract

// example
const { data: count } = await memoryClient.tevmContract(
Counter.withAddress(counterAddress).read.count(),
);

You will be able to see the TypeScript the contract is compiled to in the .tevm cache folder

Contracts can be created manually using createScript or createContract

import { createContract } from "tevm/contracts";
const myContract = createContract({
name: "MyErc721",
humanReadableAbi: ["function balanceOf(address): uint256"],
});

Advanced feature: Scripting

At this point we have covered all the major functionality of tevm and will be diving into more advanced features. Consider trying the following if you are up to it:

  1. Deploy the contract using tevm.setAccount this time to any address you prefer
  2. Use tevm.setAccount or the viem method tevm.setStorageAt to modify the storage of your contract without needing to create a transaction
  3. Try out the tevmDumpStorage action. Together with loadStorage this method can be used to hydrate and persist the tevm evm state.

Basic scripting

Like foundry, Tevm offers an extremely powerful solidity scripting environment. Tevms scripts are very tightly integrated into typescript and also include the ability to execute arbitrary typescript within them.

Any solidity contract can be ran as a script. For example, let’s run our counter script. Since we already experimented with browser let’s try using it in vitest

1. Install vitest

npm install vitest --save-dev

Vitest will work with the same tevm plugin we already installed.

2. Import your script and execute it

Create a new test file

touch src/counter.spec.ts
import { createMemoryClient } from "tevm";
import { Counter } from "../contracts/Counter.s.sol";
import { test, expect } from "vitest";
test("scripting", () => {
// let's just throw on fail since we are just playing with scripts not building a production app
const memoryClient = createMemoryClient();
const scriptResult = await memoryClient.tevmContract(Counter.read.count());
expect(scriptResult).toMatchInlineSnapshot();
});

Now run the test to snapshot the script result via updating the test command in package.json

"test": "vitest"

Notice we never had to deploy our script. Tevm scripts will deploy the script for you and then execute them. Tevm scripts will not execute the constructor though as they use tevmSetAccount not tevmDeploy to deploy the contract.

Precompiles

Tevm does not have an enumerated set of cheat codes like foundry but instead just offers a way of executing arbitrary javascript within your scripts. This allows you to do wild stuff theoretically like

  • Read and write to the file system within solidity contract
  • Read and write to the dom within solidity contracts
  • Build a tool that allows users to write servers or indexers in solidity
  • One could implement foundry compatability such that they actually could use foundry cheat codes even in tevm. Foundry scripts in browsers!
  • I’m sure there are even more creative use cases that you can think of

Precompiles require 3 steps to create.

  1. Create an interface in solidity
  2. Implement the interface in TypeScript
  3. Initialize MemoryClient with the precompiles
  4. Either pass in precompiles as arguments to your scripts (my preference), or use their hardcoded addresses.

Let’do create a precompile to read and write to the file system.

1. Create a solidity interface in contracts/Fs.sol

The solidity interface will be used when calling precompiles within solidity and also used to make the JavaScript implementation typesafe.

Terminal window
touch contracts/Fs.sol
// SPDX-License-Identifier: MIT
pragma solidity >0.8.0;
interface Fs {
/**
* @notice Reads the content of a file at the specified path.
* @param path The path of the file to read.
* @return data The content of the file.
*/
function readFile(string calldata path)
external
view
returns (string memory data);
/**
* @notice Writes data to a file at the specified path.
* @param path The path of the file to write to.
* @param data The data to write to the file.
*/
function writeFile(string calldata path, string calldata data)
external
returns (bool success);
}

2. Implement your precompile using createPrecompile

By importing our precompile interface and passing it to createPrecompile typescript will make sure we are implementing every method and returning the correct data type.

The return value of a precompile contains both a value but it also can return logs and gasUsed. We will simply return a value and charge 0 gas.

The tevm compiler makes this very easy and typesafe.

Terminal window
touch src/fsPrecompile.ts
import fs from "node:fs/promises";
import { defineCall, definePrecompile } from "tevm";
import { Fs } from "../contracts/Fs.sol";
const contract = Fs.withAddress("0xf2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2");
export const fsPrecompile = definePrecompile({
contract,
call: defineCall(Fs.abi, {
readFile: async ({ args }) => {
return {
returnValue: await fs.readFile(...args, "utf8"),
executionGasUsed: 0n,
};
},
writeFile: async ({ args }) => {
await fs.writeFile(...args);
return { returnValue: true, executionGasUsed: 0n };
},
}),
});

3. Now call precompiles from your scripts

We can write solidity scripts that execute our JavaScript now.

Terminal window
touch contracts/WriteHelloWorld.s.sol

Using the .s.sol extension tells the tevm compiler it’s a script and thus should compile it’s bytecode.

My preference is to dependency inject the precompile as an argument

// SPDX-License-Identifier: MIT
pragma solidity >0.8.0;
import {Fs} from "./Fs.sol";
contract WriteHelloWorld {
function write(Fs fs) public {
fs.writeFile("test.txt", "Hello world");
}
}

4. Pass precompile to createMemoryClient and call script

import { expect, test } from "vitest";
import { createMemoryClient } from "tevm";
import { existsSync, rmSync } from "node:fs";
import { fsPrecompile } from "./fsPrecompile.js";
import { WriteHelloWorld } from "../contracts/WriteHelloWorld.s.sol";
test("Call precompile from solidity script", async () => {
const client = createMemoryClient({
customPrecompiles: [fsPrecompile.precompile()],
loggingLevel: "trace",
});
await client.tevmContract({
...WriteHelloWorld.write.write(fsPrecompile.contract.address),
throwOnFail: false,
});
expect(existsSync("test.txt")).toBe(true);
rmSync("test.txt");
});

Next steps

What else can tevm do?

We have now implemented all major features of Tevm into a simple application running the EVM. The use cases from here are vast.

There are more features to explore such as

  • diving deeper into the viem actions api
  • the low level apis are open to use such as getTxPool(), getReceiptsManager(), and getVm()
  • After calling getVm() you can explore the vm methods such as vm.buildBlock, stateManager methods such as vm.stateManager.setStateRoot, blockchain methods vm.blockchain.getBlock, and evm methods like vm.evm.runCall. This low level api uses the ethereumjs api
  • Set loggingLevel in memory client to trace or debug
  • Configure the tevm bundler to read foundry remappings
  • Hack the evm using client.tevm.getVm().evm.on to log evm steps or modify the result of them (see ethereumjs generated evm docs for more information on this)
  • Use the statepersister to persist tevm state to local storage
  • Run tevm as an http server

Running tevm as a server

Tevm can run as an http server via the tevm/server subpackage.

import { createServer } from "tevm/server";
import { createMemoryClient } from "tevm";
const memoryClient = createMemoryClient();
const server = createServer({
request: tevm.request,
});
server.listen(8080, () => console.log("listening on 8080"));
  • In addition to createServer which creates a node http server there is also a generic http handler, express middleware, and a next.js server available in the tevm/server package.
  • If you create a server you can talk to it with a normal viem client.
  • If you wish to add the custom tevm actions to a viem client using it’s decorators.
import {tevmActions} from '@tevm/actions'
import {createPublicClient, http} from 'viem'
const client = createPublicClient({transport: 'https://localhost:8080'}).extend(tevmActions())
client.tevmContract(...)
  • If you prefer ethers the @tevm/ethers package provides an ethers provider that uses tevm as it’s in memory backend similar to MemoryClient.
  • Tevm supports advanced tracing apis. Try passing createTrace or createAccessList to a tevmCall or tevmContract.

Subpackages

The Tevm monorepo believes in making all it’s internal subpackages publically available. Thus tevm has over 60 packages available for use that can be explored. Some notable ones

  • @tevm/contracts which was used extensively in this tutorial built on top of abitype and wagmi/viem apis. It along with the tevm bundler works great with wagmi/viem and ethers even without using the rest of tevm.
  • @tevm/solc provides a typesafe wrapper around solc
  • @tevm/ethers has a tevm memory provider as well as a typesafe version of Contract that uses abitype to give it typechain-like typesafety.
  • @tevm/revm compiles revm to wasm as an experiment to try to implement the Evm in wasm.
  • @tevm/actions has the tevm actions api as well as eth json-rpc handlers available as tree-shakable actions.
  • @tevm/state, @tevmtx, @tevm/blockchain have custom tevm implementations of ethereumjs components

Also every subpackage in tevm-bundler and tevm packages is available as a standalone package if you want to minimize how much code gets installed. E.g. tevm/contracts can be installed standalone as @tevm/contracts

Ethereumjs

Tevm is built on top of ethereumjs. Most of Tevm is custom built for tevm except for the Evm but it’s internal api still follows the same interface of ethereumjs.

Custom functionality can be built into tevm by third party developers via decorating any given ethereumjs or tevm component with new functionality or writing from scratch a component that implements the interface.

Star and join discord

Finally if you enjoy tevm consider staring the github and joining the telegram!