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
- 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
1. Create a new project directory:
2. Initialize your project
3. Install tevm
and viem
as runtime dependencies
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
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.
2. Create an html file
The HTML file will be the entrypoint to our app. Add it to index.html
3. Add a typescript file
You will see it is importing a src/main.ts
file in a script tag. Go ahead and add that too
Now double check that you can run your application
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
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
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.
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
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.
Viem client API
Because the tevm client is a viem client it has access to most of the public viem actions api as well as test actions Wallet actions are not yet supported but will receive support in a later version of tevm. Don’t worry though, you can still create transactions using the custom tevmActions.
We have already used a viem action with memoryClient.getBlockNumber()
Refer to viem docs for more details about how these actions work.
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 updates account balance and add it to runApp() {}
After adding you should see it throw an Account not found
error
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
. Refer to viem docs for instructions on using the viem api.
Tevm also has it’s 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 if succesful
- 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 getVm()
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()
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.
Advanced calls with tevmContract
and tevmDeploy
Not verified
The tevm quick start guide thus far has only been verified up to the Mining blocks
step.
Any further steps may have bugs and typos. A full documentationr revamp is underway and should be completed by end of May.
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
, tevmDeploy
and tevmScript
. We will talk about tevmScript
later.
Note we could use tevmCall
and the encodeDeployData
utility provided by viem but using tevmDeploy
is a lot more ergonomic. tevmDeploy
has access to all the special cheat properties (TODO link to BaseCallParams docs) 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. We will use tevmDeploy here.
1. Let’s use tevmDeploy
to deploy a contract.
TODO we need to add simpleContract to tevm/contracts
still! If you are following along this tutorial you can get it in meantime by installing @tevm/test-utils
and importing it from there.
The biggest gotchya to be aware of here is if you specify an abi without using as const
or import it from a json it will hurt abitype’s ability to infer typescript types which will lose you typesafety and editor autocompletion.
2. Use tevmContract
to call our contract
tevmContract
is has a similar api to readContract from viem and has the followign advantages over a normal tevmCall
.
TODO add links to all these
- 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.
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. Install @tevm/bundler
2. 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
3. Add a simple 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
4. 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
5. Configure the LSP
You may notice that TypeScript started giving you red underlines even though the application works. This is because though vite is able to compile contracts we haven’t told typescript to do the same.
Add the tevm/bundler/ts-plugin
to the typescript config (note I think you might have to install @tevm/ts-plugin because a bug here but will be testing later)
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
Note: I haven’t tested the LSP in many months while focusing on building MemoryClient so it may have regressions. We will be giving the bundler and lsp some more love by end of may.
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
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 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.
3. Now call precompiles from your scripts
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@tevm/opstack
has an experimental opstack devnet that comes predeployed with all optimism protocol contracts and utilities
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!