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 a 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
- Node.js >18.0
- Basic knowledge of JavaScript
- Basic knowledge of Solidity
- Familiarity with viem or a similar library like ethers.js
Creating your Tevm project
-
Create a new project directory.
-
Initialize your project
-
Install the runtime dependencies.
-
Install the buildtime dependencies. TypeScript is the language we’re using. Vite provides us a minimal setup to import TypeScript into our HTML and start a dev server. With Vite we also install a polyfill library. This library injects code into your final build that will allow apis that do not exist in the browser natively to work.
-
Create a TypeScript configuration file.
Tevm has these requirements from the TypeScript configuration:
- Use strict mode
- Support bigint (ES2020 or later)
See the tsconfig docs for more information about these options.
You can use this file.
-
Create the
index.html
file.The HTML file will be the entrypoint to our app.
-
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. -
Create a Vite configuration file.
-
Run your application.
Hit
o
key and then<Enter>
to open uphttp://localhost:5173
in your browserYou should see
Hello Tevm
rendered. -
Add a shortcut script to
package.json
.
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
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.
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
2. Add function that displays account balance
After adding you should see it throw an AccountNotFoundError
in the chrome console.
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
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.
These can be imported from tevm via the prefundedAccounts
export.
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
- create a transaction
- impersonate any account or contract
- arbitrarily set the call depth
- skip all balance checks
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
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
.
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
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()
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()
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.
2. Update the html to display contract information
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
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
2. Use getTransactionReceipt to get the receipt
Remember after mining new blocks getTransactionReceipt will return the 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.
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
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
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.
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.
Then add the contract
5. Import the contract and console.log it
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
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
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:
- Deploy the contract using
tevm.setAccount
this time to any address you prefer - Use
tevm.setAccount
or the viem methodtevm.setStorageAt
to modify the storage of your contract without needing to create a transaction - Try out the
tevmDumpStorage
action. Together withloadStorage
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
Now run the test to snapshot the script result via updating the test
command in package.json
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.
- Create an interface in solidity
- Implement the interface in TypeScript
- Initialize MemoryClient with the precompiles
- 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.
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.
3. Now call precompiles from your scripts
We can write solidity scripts that execute our JavaScript now.
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
4. Pass precompile to createMemoryClient
and call script
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()
, andgetVm()
- After calling
getVm()
you can explore the vm methods such asvm.buildBlock
, stateManager methods such asvm.stateManager.setStateRoot
, blockchain methodsvm.blockchain.getBlock
, and evm methods likevm.evm.runCall
. This low level api uses theethereumjs api
- Set
loggingLevel
in memory client totrace
ordebug
- 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.
- 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 thetevm/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.
- 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
orcreateAccessList
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 ofContract
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!