Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Bundler Quickstart

Set up the Tevm bundler to import Solidity contracts directly into TypeScript/JavaScript.

Overview

Tevm bundler enables:

  • .sol files imported directly in your code
  • Full TypeScript type info for contract methods
  • Go-to-definition and hover docs for Solidity
  • Type-safe contract interaction

Prerequisites

  • Node.js 18+ and npm/yarn/pnpm
  • A supported bundler (Vite, Webpack, Rollup, ESBuild, or Bun)

Step 1: Install Tevm and TypeScript Plugin

npm install tevm
npm install -D @tevm/ts-plugin

Step 2: Configure Your Bundler

The bundler plugin compiles Solidity during your build, so the JS output contains ABIs, bytecode, and TypeScript interfaces.

For Vite (recommended)

// vite.config.ts
import { defineConfig } from 'vite'
import { vitePluginTevm } from 'tevm/bundler/vite-plugin'
import react from '@vitejs/plugin-react'
 
export default defineConfig({
  plugins: [react(), vitePluginTevm()],
})

For Other Bundlers

Webpack
// webpack.config.mjs
import { webpackPluginTevm } from 'tevm/bundler/webpack-plugin'
 
export default {
  plugins: [webpackPluginTevm()],
}
Next.js
// next.config.js
const nextConfig = {
  webpack: (config) => config,
  typescript: {
    // LSP typechecking works in-editor but not during `next build` yet.
    // For build-time typechecking, generate .ts files via `npx evmts-gen`.
    ignoreBuildErrors: true,
  }
}
 
export default nextConfig

:::

Rollup
// rollup.config.js
import { rollupPluginTevm } from 'tevm/bundler/rollup-plugin'
 
export default {
  plugins: [rollupPluginTevm()],
}
Rspack
// rspack.config.mjs
import { rspackPluginTevm } from 'tevm/bundler/rspack-plugin'
 
export default {
  plugins: [rspackPluginTevm()],
}
Bun
// plugins.ts
import { plugin } from 'bun'
import { tevmBunPlugin } from 'tevm/bundler/bun-plugin'
 
plugin(tevmBunPlugin({}))
# bunfig.toml
preload = ["./plugins.ts"]
 
[test]
preload = ["./plugins.ts"]
ESBuild
// build.mjs
import { build } from 'esbuild'
import { esbuildPluginTevm } from 'tevm/bundler/esbuild-plugin'
 
build({
  entryPoints: ['src/index.js'],
  outdir: 'dist',
  bundle: true,
  plugins: [esbuildPluginTevm()],
})

Step 3: Configure TypeScript

The TypeScript plugin enables editor features (completion, type checking, go-to-definition) for Solidity imports. Add to tsconfig.json:

{
  "compilerOptions": {
    "plugins": [
      { "name": "@tevm/ts-plugin" }
    ]
  }
}

Step 4: Configure Your Editor

VS Code and Cursor

Use the workspace TypeScript (the workspace install is what loads @tevm/ts-plugin):

  1. Command Palette (Ctrl/Cmd+Shift+P, or ⌘K/Ctrl+K in Cursor)
  2. "TypeScript: Select TypeScript Version"
  3. "Use Workspace Version"

Vim, Neovim, Other Editors

Works automatically as long as the editor uses the project's workspace TypeScript (required to load @tevm/ts-plugin).

Step 5: Create a Solidity Contract

Name it Counter.s.sol to include bytecode (the .s.sol extension tells Tevm to generate deployable bytecode):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
 
contract Counter {
    uint256 private count = 0;
 
    function increment() public {
        count += 1;
    }
 
    function getCount() public view returns (uint256) {
        return count;
    }
}

Step 6: Import and Use the Contract

// src/App.tsx
import { Counter } from './Counter.s.sol'
import { createMemoryClient } from 'tevm'
import { useState, useEffect } from 'react'
 
function App() {
  const [count, setCount] = useState<bigint>(0n)
  const [client] = useState(() => createMemoryClient())
  const [deployedAddress, setDeployedAddress] = useState<string>('')
 
  useEffect(() => {
    const deploy = async () => {
      const deployed = await client.deployContract(Counter.deploy())
      setDeployedAddress(deployed.address)
      setCount(await deployed.read.getCount())
    }
    deploy()
  }, [client])
 
  const handleIncrement = async () => {
    if (!deployedAddress) return
    const contract = Counter.withAddress(deployedAddress)
    await client.writeContract(contract.write.increment())
    await client.mine({ blocks: 1 })
    setCount(await client.readContract(contract.read.getCount()))
  }
 
  return (
    <div>
      <h1>Tevm Counter Example</h1>
      <p>Count: {count.toString()}</p>
      <button onClick={handleIncrement}>Increment</button>
    </div>
  )
}
 
export default App

Type-Safe Method Calls

// Read methods (view/pure)
const balance = await client.readContract(
  ERC20.read.balanceOf('0x' + '20'.repeat(20))
)
 
// Write methods (state-changing)
await client.writeContract(
  ERC20.write.transfer('0x' + '30'.repeat(20), 1000000n)
)
 
// Events
client.watchEvent(
  ERC20.events.Transfer({
    from: '0x' + '20'.repeat(20),
    to: null // `null` is a wildcard
  }),
  (log) => console.log('Transfer event:', log)
)

What's Happening?

When you import Counter.s.sol, Tevm:

  1. Compiles via solc.
  2. Generates TypeScript with ABI, bytecode, and a Contract instance.
  3. Provides a fully typed interface.

The Counter object is a Tevm Contract with:

  • abi
  • address (if set via withAddress())
  • bytecode — creation bytecode (undefined for .sol)
  • deployedBytecode — runtime bytecode (undefined for .sol)
  • deploy() — deployment data with constructor args
  • read — type-safe read methods
  • write — type-safe write methods
  • events — type-safe event filters
  • withAddress() — new instance with an address

Advanced Configuration

Foundry Integration

By default Tevm uses Node.js resolution. For Foundry projects or custom remappings, create tevm.config.json:

{
  "foundryProject": true,
  "libs": ["lib", "node_modules"],
  "remappings": {
    "@openzeppelin/": "node_modules/@openzeppelin/"
  },
  "cacheDir": ".tevm"
}

"foundryProject": true will:

  • Auto-read remappings from foundry.toml/remappings.txt
  • Include Foundry library paths (lib/ by default)
  • Allow Foundry-style import paths
  • Merge with manual remappings/libs

Manual config without Foundry:

{
  "foundryProject": false,
  "libs": [
    "./contracts",
    "node_modules"
  ],
  "remappings": {
    "@openzeppelin/": "node_modules/@openzeppelin/",
    "ds-test/": "lib/forge-std/lib/ds-test/src/",
    "solmate/": "node_modules/solmate/src/"
  }
}

Useful for mixed Foundry/JS projects, Forge libraries (OpenZeppelin, Solmate), or complex Solidity import paths.

See the Foundry example for a complete project.

Using Third-Party Contracts

NPM Packages

// Use .s.sol when bytecode is needed
import { ERC20 } from '@openzeppelin/contracts/token/ERC20/ERC20.s.sol'
 
const myToken = await client.deployContract(
  ERC20.deploy("MyToken", "MTK")
)

Coming Soon Features

Network Imports with Macros

Upcoming: import contracts from any EVM network via build-time macros, resolved during build for better performance and type safety:

// contract-macros.js
import { createMemoryClient } from 'tevm'
import { http } from 'viem'
import { mainnet, optimism } from 'viem/chains'
import { loadContract } from 'tevm'
import { SourcifyABILoader, EtherscanABILoader } from 'tevm/whatsabi'
 
// Use fixed block heights for reproducible builds
const mainnetClient = createMemoryClient({
  fork: {
    transport: http('https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY'),
    blockNumber: 19000000n
  }
})
 
const optimismClient = createMemoryClient({
  fork: {
    transport: http('https://mainnet.optimism.io'),
    blockNumber: 116000000n
  }
})
 
const mainnetLoaders = [
  new SourcifyABILoader(),
  new EtherscanABILoader({ apiKey: 'YOUR_ETHERSCAN_KEY' })
]
 
const optimismLoaders = [
  new SourcifyABILoader(),
  new EtherscanABILoader({
    apiKey: 'YOUR_ETHERSCAN_KEY',
    baseUrl: 'https://api-optimistic.etherscan.io/api'
  })
]
 
export async function wethContract() {
  return loadContract(mainnetClient, {
    address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
    followProxies: true,
    loaders: mainnetLoaders
  })
}
 
export async function usdcContract() {
  return loadContract(optimismClient, {
    address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85',
    followProxies: true,
    loaders: optimismLoaders
  })
}

Import with the with {type: 'macro'} attribute:

import { wethContract } from './contract-macros.js' with { type: 'macro' }
import { usdcContract } from './contract-macros.js' with { type: 'macro' }
 
const wethBalance = await client.readContract({
  ...wethContract.read.balanceOf('0x123...'),
  address: wethContract.address
})
 
console.log(`USDC decimals: ${await client.readContract({
  ...usdcContract.read.decimals(),
  address: usdcContract.address
})}`)
console.log(`Human readable ABI: ${usdcContract.humanReadableAbi}`)
console.log(`Implementation address: ${usdcContract.proxyDetails?.[0]?.implementation || 'Not a proxy'}`)

Enable macros in tevm.config.json (required for security):

{
  "macros": true,
  "foundryProject": true,
  "libs": ["lib", "node_modules"]
}

ABIs resolve at build time with full type safety. See Contract Loader.

Inline Solidity with Template Literals

For prototyping, define contracts inline:

import { sol } from 'tevm'
 
const { Counter } = sol`
  // SPDX-License-Identifier: MIT
  pragma solidity ^0.8.0;
 
  contract Counter {
    uint256 private count = 0;
 
    function increment() public {
      count += 1;
    }
 
    function getCount() public view returns (uint256) {
      return count;
    }
  }
`
 
const deployed = await client.deployContract(Counter.deploy())
await deployed.write.increment()
const count = await deployed.read.getCount()

Same type-safety and features as file-based imports.

Documentation Resources

Troubleshooting

  • Red underlines in imported Solidity — use the workspace TypeScript version.
  • No bytecode available — use .s.sol for deployable contracts.
  • Deployment errors — pass required constructor args to .deploy().
  • Compilation errors — check Solidity code and version compatibility.
  • TypeScript errors — verify the TS plugin is configured.
  • Red underlines in editor — confirm workspace TS with plugin loaded.
  • Import resolution failures (Foundry-style imports like @openzeppelin/contracts/...):
    • Create tevm.config.json with "foundryProject": true
    • Check foundry.toml remappings
    • Add explicit remappings in tevm.config.json
  • Test runners — Vitest/Jest work out-of-the-box once the bundler plugin is configured.

Next Steps