Skip to content
LogoLogo

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

RequirementVersion
Node.js^24.0.0 (the package also runs in browsers and Bun)
TypeScript>=5.0, with "strict": true
Module formatESM 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) // 100n

The 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