Skip to content
LogoLogo

Using with viem and ethers

Action objects are plain data with no Tevm-specific machinery, so they drop straight into other libraries. The one deliberate design decision that makes this work is that every action carries both address and to:

  • Tevm's call API and ethers use to.
  • viem's contract actions use address.

Rather than force a translation layer, the action creators emit both.

viem

Reading

import { ERC20 } from '@tevm/contract'
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
 
const client = createPublicClient({
  chain: mainnet,
  transport: http('https://ethereum-rpc.publicnode.com'),
})
 
const Dai = ERC20.withAddress('0x6B175474E89094C44Da98b954EedeAC495271d0F')
 
const balance = await client.readContract(
  Dai.read.balanceOf('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'),
)
console.log(balance) // bigint

No adapter, no .abi/.functionName/.args destructuring — the object already has the keys readContract expects.

Writing

import { ERC20 } from '@tevm/contract'
import { createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { mainnet } from 'viem/chains'
 
const wallet = createWalletClient({
  account: privateKeyToAccount('0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'),
  chain: mainnet,
  transport: http('https://ethereum-rpc.publicnode.com'),
})
 
const Dai = ERC20.withAddress('0x6B175474E89094C44Da98b954EedeAC495271d0F')
 
const hash = await wallet.writeContract(
  Dai.write.transfer('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 1000n),
)
console.log(hash)

Simulating

const { result, request } = await client.simulateContract({
  ...Dai.write.transfer('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 1000n),
  account: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
})

Multicall

Actions compose naturally into a multicall array:

import { ERC20 } from '@tevm/contract'
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
 
const client = createPublicClient({
  chain: mainnet,
  transport: http('https://ethereum-rpc.publicnode.com'),
})
 
const Dai = ERC20.withAddress('0x6B175474E89094C44Da98b954EedeAC495271d0F')
const Usdc = ERC20.withAddress('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48')
const holder = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' as const
 
const results = await client.multicall({
  contracts: [
    Dai.read.balanceOf(holder),
    Usdc.read.balanceOf(holder),
    Dai.read.totalSupply(),
  ],
})
 
console.log(results.map((r) => r.result))

Events

const logs = await client.getLogs(
  Dai.events.Transfer({
    args: { from: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' },
    fromBlock: 21_000_000n,
    toBlock: 'latest',
  }),
)

Encoding only

If you need raw calldata rather than a client call:

import { ERC20 } from '@tevm/contract'
import { encodeFunctionData } from 'viem'
 
const data = encodeFunctionData(
  ERC20.write.transfer('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 1000n),
)
console.log(data) // '0xa9059cbb...'

wagmi

wagmi's hooks take the same parameter shape as viem's actions, so spreading works there too:

import { ERC20 } from '@tevm/contract'
import { useReadContract } from 'wagmi'
 
const Dai = ERC20.withAddress('0x6B175474E89094C44Da98b954EedeAC495271d0F')
 
export function Balance({ owner }: { owner: `0x${string}` }) {
  const { data, isLoading } = useReadContract(Dai.read.balanceOf(owner))
 
  if (isLoading) return <span>Loading…</span>
  return <span>{data?.toString()}</span>
}

ethers

ethers wants a JSON ABI and an address rather than a per-call action object, so use the contract's abi and address to construct an ethers.Contract once:

import { ERC20 } from '@tevm/contract'
import { JsonRpcProvider, Contract as EthersContract } from 'ethers'
 
const provider = new JsonRpcProvider('https://ethereum-rpc.publicnode.com')
 
const Dai = ERC20.withAddress('0x6B175474E89094C44Da98b954EedeAC495271d0F')
const dai = new EthersContract(Dai.address, Dai.abi, provider)
 
const balance = await dai.balanceOf('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045')
console.log(balance)

humanReadableAbi also works directly as an ethers "human-readable ABI":

const dai = new EthersContract(Dai.address, [...Dai.humanReadableAbi], provider)

Why this works

Nothing in this package knows about viem, wagmi, or ethers. The interop is a consequence of emitting the union of the field names each library expects — abi, functionName, args, address, to, eventName, humanReadableAbi, and optionally code. Any library that reads a subset of those keys and ignores the rest works out of the box.