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.
Terminal window mkdir tevm-app && cd tevm-appmkdir src -
Initialize your project
Terminal window npm init --yes -
Install the runtime dependencies.
Terminal window npm install tevm viem -
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.
Terminal window npm install --save-dev typescript vite -
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.
tsconfig.json {"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"]} -
Create the
index.html
file.The HTML file will be the entrypoint to our app.
index.html <!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> -
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.src/main.ts const app = document.querySelector("#app") as Element;app.innerHTML = `<div>Hello Tevm</div>`; -
Create a Vite configuration file.
vite.config.js import { defineConfig } from "vite"// https://vitejs.dev/config/export default defineConfig({}) -
Run your application.
Terminal window npx vite .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
.package.json {"name": "tevm-app","version": "1.0.0","description": "","main": "index.js","scripts": {"test": "echo \"Error: no test specified\" && exit 1","dev": "npx vite ."},"keywords": [],"author": "","license": "ISC","dependencies": {"tevm": "^1.0.0-next.110","viem": "^2.21.2"},"devDependencies": {"typescript": "^5.5.4","vite": "^5.4.3"}}
Create a forked blockchain
With the project created, the next step is to create a fork of a real-world blockchain.
The simplest way to do this is the use a MemoryClient
, a Viem client that uses an in-memory transport.
Instead of sending requests to an RPC provider like Alchemy this client processes requests with tevm in a local EVM instance running in JavaScript.
Memory client has similar features to anvil
:
- Optionally fork an existing network.
- Run special scripts that have advanced functionality.
- Allow you to view and modify the chain state, so you can mint yourself ETH, run traces, modify storage, etc.
- Be Extremely hackable. You can mint yourself ETH, run traces, modify storage, and more.
Replace src/main.ts
with the file below, and see you can get the block number from Redstone.
import { createMemoryClient, http } from "tevm";import { redstone } from "tevm/common";
const app = document.querySelector("#app") as Element;
const memoryClient = createMemoryClient({ common: redstone, fork: { // @warning we may face throttling using the public endpoint // In production apps consider using `loadBalance` and `rateLimit` transports transport: http("https://rpc.redstonechain.com")({}), },});
async function runApp() { app.innerHTML = ` <b>Status:</b> <span id="status">initializing</span>
<details> <summary>memoryClient content</summary> <div id="content"></div> </details>
<b>Forked at block:</b> <span id="blocknumber">???</span> `;
document.querySelector("#content")!.innerHTML = ` <table> <tr> <th>Key</th> <th>Type</th> <th>Value</th> </tr> ${Object.keys(memoryClient) .map(key => ` <tr> <td>${key}</td> <td>${typeof memoryClient[key]}</td> <td>${typeof memoryClient[key] != "function" && memoryClient[key] || ""}</td> </tr>`) .reduce((a,b) => a+b, "") } </table>`;
const status = app.querySelector("#status")!;
status.innerHTML = "Working"; const blockNumber = await memoryClient.getBlockNumber();
document.querySelector("#blocknumber")!.innerHTML = blockNumber;
status.innerHTML = "Done";}
runApp();
Explanation
import { createMemoryClient, http } from "tevm";import { redstone } from "tevm/common";
Import the functions we need.
const app = document.querySelector("#app") as Element;
Use the app
element in index.html
.
const memoryClient = createMemoryClient({ common: redstone, fork: { // @warning we may face throttling using the public endpoint // In production apps consider using `loadBalance` and `rateLimit` transports transport: http("https://rpc.redstonechain.com")({}), },});
Create a MemoryClient
that forks the Redstone network.
We use Redstone because it does not have throttling.
It is recomended you also pass in a Common
chain object when forking.
This improves the performance of fork and guarantees tevm has all the correct chain information such as which EIPs and hardforks to use.
async function runApp() {
This function actually does the work and runs the app.
app.innerHTML = ` <b>Status:</b> <span id="status">initializing</span>
<details> <summary>memoryClient content</summary> <div id="content"></div> </details>
<b>Forked at block:</b> <span id="blocknumber">???</span> `;
This sets the HTML inside the app
element.
document.querySelector("#content")!.innerHTML = `
Specify the content of the content
element.
<table> <tr> <th>Key</th> <th>Type</th> <th>Value</th> </tr> ${Object.keys(memoryClient) .map(key => ` <tr> <td>${key}</td> <td>${typeof memoryClient[key]}</td> <td>${typeof memoryClient[key] != "function" && memoryClient[key] || ""}</td> </tr>`) .reduce((a,b) => a+b, "") }</table>`;
The content is a table of the keys of memoryClient
, and their types, and their values.
The table is created using MapReduce.
const status = app.querySelector("#status")!;
status.innerHTML = "Working";
At this point we start running asynchronous functions and waiting for them to finish, so we change our status to “Working”.
const blockNumber = await memoryClient.getBlockNumber();
Get the current block number at the time of the fork. Note that while the blockchain continues to update, the tevm fork is “frozen” and does not get those updates.
document.querySelector("#blocknumber")!.innerHTML = blockNumber;
status.innerHTML = "Done";}
Update the block number, and change the status to done.
runApp();
Run the async function.
When we fork a blockchain the block number will be pinned to the block number at the time of the fork. Any future changes will not be reflected in tevm unless you create another fork.
Actions
As you can see when you expand memoryClient content actions, many of the Viem actions are available under the same name.
For example, you can modify src/main.ts
to see how they work.
import { createMemoryClient, http } from "tevm";import { redstone } from "tevm/common";
const app = document.querySelector("#app") as Element;
const memoryClient = createMemoryClient({ common: redstone, fork: { // @warning we may face throttling using the public endpoint // In production apps consider using `loadBalance` and `rateLimit` transports transport: http("https://rpc.redstonechain.com")({}), },});
async function runApp() { app.innerHTML = ` <b>Status:</b> <span id="status">initializing</span> <br /> <b>Forked at block:</b> <span id="blocknumber">???</span> <br /> <h2>Output</h2> <div id="outputPanel"></div> `;
const addToOutput = (obj, title) => { output.innerHTML += ` <h4>${title}</h4> <pre>${JSON.stringify(obj, (_, v) => typeof v === 'bigint' ? v.toString() : v, 4)} </pre> ` }
const status = app.querySelector("#status")!;
status.innerHTML = "Working"; const blockNumber = await memoryClient.getBlockNumber();
document.querySelector("#blocknumber")!.innerHTML = blockNumber; const output = document.querySelector("#outputPanel") as Element;
const txn = await memoryClient.getTransaction({ hash: "0x58d3e6c9f7b66ec3cd984219dd48fae465a6e7fc0f51688ef4864045a363b4c2" }); addToOutput(txn, "Transaction")
const block = await memoryClient.getBlock({ blockNumber: txn.blockNumber }); addToOutput(block, "Transaction block");
const address = "0x" + "BAD060A7".padStart(40, "0") addToOutput(address, "Account address")
const balanceT0 = BigInt(await memoryClient.getBalance({address})) await memoryClient.setBalance({ address, value: 1000 }) const balanceT1 = BigInt(await memoryClient.getBalance({address}))
addToOutput({ initialBalance: balanceT0, afterSetBalance: balanceT1 }, "Balances")
status.innerHTML = "Done";}
runApp();
Explanation
<h2>Output</h2> <div id="outputPanel"></div>
We need an output panel.
const addToOutput = (obj, title) => {
A function to write to the output panel.
output.innerHTML += ` <h4>${title}</h4> <pre>${JSON.stringify(obj, (_, v) => typeof v === 'bigint' ? v.toString() : v, 4)} </pre> ` }
The second parameter of JSON.stringify
lets us replace values that we don’t want JSON.stringify
to display.
Here, the function replaces values of type bigint
, which are not part of the JSON standard, with strings, which are.
const output = document.querySelector("#outputPanel") as Element;
const txn = await memoryClient.getTransaction({ hash: "0x58d3e6c9f7b66ec3cd984219dd48fae465a6e7fc0f51688ef4864045a363b4c2" }); addToOutput(txn, "Transaction")
const block = await memoryClient.getBlock({ blockNumber: txn.blockNumber }); addToOutput(block, "Transaction block");
Use Viem’s getTransaction
and gtBlock
.
“typescript const address = “0x” + “BAD060A7”.padStart(40, “0”) addToOutput(address, “Account address”)
Get an address that [isn't in use](https://explorer.redstone.xyz/address/0x00000000000000000000000000000000BAd060A7).
```typescript const balanceT0 = BigInt(await memoryClient.getBalance({address}))
Use getBalance
to get the address’s balance.
await memoryClient.setBalance({ address, value: 1_000_000_000_000_000_000n }) const balanceT1 = BigInt(await memoryClient.getBalance({address}))
Use the test action setBalance
to “give” address
1 ETH.
addToOutput({ initialBalance: balanceT0, afterSetBalance: balanceT1 }, "Balances")
Report the change on the output panel.
Calling tevm
We want to create transactions and see how they affect the local copy (and therefore how they would affect the blockchain). To do this we need to call several functions:
setBalance
to give an address ETH (only locally, of course).tevmContract
to callgreet
, aview
function that gets us the current greeting.tevmContract
again, to send out a transaction to change the greeting.tevmMine
to create a block to include the transaction.tevmContract
a third time to see the greeting has changed.
Optionally, to see what we’ve done, we can use these functions:
getBlock
to examine the block we created.getTransactionReceipt
to examine the transaction.
We will access an instance of Hardhat’s Greeter contract, deployed on Redstone.
Replace src/main.ts
with this file.
import { createMemoryClient, http } from "tevm";import { redstone } from "tevm/common";
const app = document.querySelector("#app") as Element;
const memoryClient = createMemoryClient({ common: redstone, fork: { // @warning we may face throttling using the public endpoint // In production apps consider using `loadBalance` and `rateLimit` transports transport: http("https://rpc.redstonechain.com")({}), },});
const greeterABI = [ { "inputs": [], "name": "greet", "outputs": [ { "internalType": "string", "name": "", "type": "string" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "string", "name": "_greeting", "type": "string" } ], "name": "setGreeting", "outputs": [], "stateMutability": "nonpayable", "type": "function" }] as const
async function runApp() { app.innerHTML = ` <b>Status:</b> <span id="status">initializing</span> <br /> <b>Forked at block:</b> <span id="blocknumber">???</span> <br /> <h2>Output</h2> <div id="outputPanel"></div> `;
const addToOutput = (obj, title) => { output.innerHTML += ` <h4>${title}</h4> <pre>${JSON.stringify(obj, (_, v) => typeof v === 'bigint' ? v.toString() : v, 4)} </pre> ` }
const status = app.querySelector("#status")!;
status.innerHTML = "Working"; const blockNumber = await memoryClient.getBlockNumber();
document.querySelector("#blocknumber")!.innerHTML = blockNumber; const output = document.querySelector("#outputPanel") as Element;
const address = "0x" + "BAD060A7".padStart(40, "0") const setBalanceResult = await memoryClient.setBalance({ address, value: 10n**18n }) addToOutput(setBalanceResult, `setBalance for ${address}`)
const greetingResult1 = await memoryClient.tevmContract({ abi: greeterABI, to: "0x8B7CFA6e4684037f4b4c1F439422fF5B2D0Ab523", functionName: "greet", }) addToOutput(greetingResult1, "first call to greet()")
const setGreetingResult = await memoryClient.tevmContract({ abi: greeterABI, to: "0x8B7CFA6e4684037f4b4c1F439422fF5B2D0Ab523", from: address, functionName: "setGreeting", args: ["Change to this greeting"], createTransaction: "on-success" }) addToOutput(setGreetingResult, "call to setGreeting(string)")
const mineResult = await memoryClient.tevmMine(); addToOutput(mineResult, "mineResult")
const greetingResult2 = await memoryClient.tevmContract({ abi: greeterABI, to: "0x8B7CFA6e4684037f4b4c1F439422fF5B2D0Ab523", functionName: "greet", }) addToOutput(greetingResult2, "second call to greet()")
const blockData = await memoryClient.getBlock({ blockHash: mineResult.blockHashes[0] }) addToOutput(blockData, "Block data")
const txnData = await memoryClient.getTransactionReceipt({ hash: blockData.transactions[0] }) addToOutput(txnData, "Transaction data")
status.innerHTML = "Done";}
runApp();
Explanation
const greeterABI = [ { . . . }] as const
This is the part of the Greeter contract’s ABI we need. On a production system you might want to serve it from a separate file, but this is simpler.
const address = "0x" + "BAD060A7".padStart(40, "0") const setBalanceResult = await memoryClient.setBalance({ address, value: 10n**18n })
Create our source address and provide it with ETH to run transactions.
const greetingResult1 = await memoryClient.tevmContract({ abi: greeterABI, to: "0x8B7CFA6e4684037f4b4c1F439422fF5B2D0Ab523", functionName: "greet", }) addToOutput(greetingResult1, "first call to greet()")
Use tevmContract
to issue a call to a view
function.
Normally, we would need to provide the ABI, the address, and name of the function, and the arguments.
However, greet()
does not take any arguments, so we can either provide an empty list or just omit the parameter.
const setGreetingResult = await memoryClient.tevmContract({
The same tevmContract
function is also used to send transactions.
abi: greeterABI, to: "0x8B7CFA6e4684037f4b4c1F439422fF5B2D0Ab523", functionName: "setGreeting", args: ["Change to this greeting"],
setGreeting
takes one argument, a string
, so we provide that in the args
list.
from: address,
The ability to specify from
lets us figure the results of actions by other users, and to anticipate the actions of our user without a need to ask the wallet extension for a signature.
createTransaction: "on-success" }) addToOutput(setGreetingResult, "call to setGreeting(string)")
createTransaction
lets us specify if tevm should create a transaction.
Here we are modifying the blockchain state, so we need one.
const mineResult = await memoryClient.tevmMine(); addToOutput(mineResult, "mineResult")
Transactions only modify the blockchain state when they are mined into a block that is then added to the blockchain.
The remainder of the code should be self-explanatory.
We call greet()
again to see the new greeting, and then read the block we mined and the transaction inside it.
The transaction data lets us verify that the transaction really did come from address
.
Conclusion
At this point you should be able to use tevmClient
for the basics, to fork a blockchain and then observe the results of user actions before sending a real-life transaction.
This is just the basic use, there are more advanced things you can do with tevm:
- Run Typescript as part a tevm contract call
- Compile contracts
- Deploy contracts
More tutorials are coming soon.