Getting started
Install
@tevm/contract is published to npm as @tevm/contract.
The 1.0 line is currently a release candidate.
@tevm/utils and @tevm/errors are installed automatically as dependencies — you do not need
to add them yourself, though importing types from @tevm/utils (Address, Hex, Abi) is
supported and common.
Requirements
| Requirement | Version |
|---|---|
| Node.js | ^24.0.0 (the package also runs in browsers and Bun) |
| TypeScript | >=5.0, with "strict": true |
| Module format | ESM and CJS builds are both published |
Your first contract
import { createContract } from '@tevm/contract'
export const Counter = createContract({
name: 'Counter',
humanReadableAbi: [
'function get() view returns (uint256)',
'function set(uint256 newValue)',
'event ValueSet(uint256 newValue)',
],
})That is a complete, valid contract definition. Counter.abi is the parsed JSON ABI,
Counter.humanReadableAbi is the array you passed in, and Counter.read, Counter.write, and
Counter.events are populated from the ABI.
Attach an address
A contract defined without an address is a shape. Attach an address to get a contract bound to a deployment:
import { createContract } from '@tevm/contract'
const Counter = createContract({
name: 'Counter',
humanReadableAbi: ['function get() view returns (uint256)', 'function set(uint256 newValue)'],
})
const deployed = Counter.withAddress('0x1234567890123456789012345678901234567890')
console.log(deployed.address)
// '0x1234567890123456789012345678901234567890'
console.log(Counter.address)
// undefined — `withAddress` returned a new contract, it did not mutate `Counter`Once an address is attached, every action creator includes address and to:
console.log(deployed.read.get())
// {
// abi: [...],
// humanReadableAbi: ['function get() view returns (uint256)'],
// functionName: 'get',
// address: '0x1234567890123456789012345678901234567890',
// to: '0x1234567890123456789012345678901234567890',
// }Executing an action
@tevm/contract builds actions; something else runs them. A complete round-trip using
SimpleContract — one of the prebuilt contracts this package
ships with real, compiled bytecode — and a Tevm in-memory node:
import { SimpleContract } from '@tevm/contract'
import { createMemoryClient, PREFUNDED_ACCOUNTS } from 'tevm'
const client = createMemoryClient()
await client.tevmReady()
// Deploy. `SimpleContract.deploy(initialValue)` returns { abi, bytecode, args }.
const deployResult = await client.tevmDeploy({
from: PREFUNDED_ACCOUNTS[0].address,
...SimpleContract.deploy(42n),
addToMempool: true,
})
await client.tevmMine({ blockCount: 1 })
if (!deployResult.createdAddress) throw new Error('deployment failed')
const Counter = SimpleContract.withAddress(deployResult.createdAddress)
// Read — no transaction, no mining.
const { data } = await client.tevmContract(Counter.read.get())
console.log(data) // 42n
// Write — spread the action and add the transaction options.
await client.tevmContract({
from: PREFUNDED_ACCOUNTS[0].address,
...Counter.write.set(100n),
addToMempool: true,
})
await client.tevmMine({ blockCount: 1 })
const { data: updated } = await client.tevmContract(Counter.read.get())
console.log(updated) // 100nThe important thing here is the division of labour: Counter.read.get() and
Counter.write.set(100n) are plain objects built with no client at all, and client.tevmContract
is the thing that knows how to execute them. The same objects work with viem — see
Using with viem and ethers.
Where to go next
- Creating a contract — human-readable vs JSON ABIs, bytecode
- Reading state and Writing state
- Events and filters
- Bundler-generated contracts — importing
.solfiles directly - API reference

