Skip to content

Instantly share code, notes, and snippets.

@roninjin10
Created June 5, 2025 10:41
Show Gist options
  • Select an option

  • Save roninjin10/e633add2dc30d7d63e7a699de4ba61ad to your computer and use it in GitHub Desktop.

Select an option

Save roninjin10/e633add2dc30d7d63e7a699de4ba61ad to your computer and use it in GitHub Desktop.
The most concise introduction to viem and tevm I can give

Viem is a lightweight, EIP-1193–compliant client for sending JSON-RPC requests—so it can plug into any EIP-1193–compatible server (Anvil, Hardhat, Alchemy, Geth, Reth, Tevm, etc.). Tevm is an EIP-1193–compatible server that can plug into Viem, Ethers, Express.js, Bun, or any JSON-RPC handler.

Viem Concepts

  1. Transport Defines how you send JSON-RPC requests. For example:

    import { http } from "viem";
    
    // Create a new HTTP transport and send a simple request:
    const result = await http("https://mainnet.infura.io/v3/YOUR_PROJECT_ID")({}).request({
      method: "eth_blockNumber",
    });
    console.log(result); // e.g. { jsonrpc: "2.0", id: 1, result: "0x10d4f" }

    Here, http() returns a transport‐factory function; you pass any options (e.g., headers) in the first call, then call .request(...) with your JSON-RPC payload.

  2. Utils Utility functions that don’t require talking to a node. For example, to compute Keccak-256:

    import { keccak256 } from "viem";
    
    const data = new TextEncoder().encode("hello world");
    const hash = keccak256(data);
    console.log(hash); // "0x47173285a8d7342…"

    You can use any Viem utility (e.g., encodeFunctionArgs, decodeEventLog) without a live transport.

  3. Clients Opinionated, type-safe structs built around transports. A basic client is effectively the same as http(), but you can inject chain metadata:

    import { createPublicClient, http } from "viem";
    import { optimism } from "viem/chains";
    
    // Create a typed client for the Optimism network:
    const client = createPublicClient({
      chain: optimism,
      transport: http("https://optimism-mainnet.infura.io/v3/YOUR_PROJECT_ID")({}),
    });
    
    // This is equivalent to: http(...)({}).request(...)
    const blockNumberRx = await client.request({ method: "eth_blockNumber" });
    console.log(blockNumberRx); // e.g. { jsonrpc: "2.0", id: 1, result: "0xABCDE" }

    By passing chain: optimism, Viem will handle any chain‐specific quirks (e.g., formatting of special transactions).

  4. Actions Opinionated, type-safe wrappers around raw JSON-RPC methods. For example, the built‐in getBlockNumber action:

    import { getBlockNumber } from "viem/actions";
    // `params` is undefined because getBlockNumber doesn’t take any arguments.
    const latest = await getBlockNumber(client, undefined);
    console.log(latest); // e.g. 11217453

    Under the hood, getBlockNumber(client, undefined) simply invokes client.request({ method: "eth_blockNumber" }) and parses the response.

  5. Convenience Clients Higher‐level clients that bundle actions so you can call client.someAction(params) directly:

    import { createPublicClient, createWalletClient, createMemoryClient } from "viem";
    
    // PublicClient example:
    const publicClient = createPublicClient({
      chain: optimism,
      transport: http("https://optimism-mainnet.infura.io/v3/YOUR_PROJECT_ID")({}),
    });
    const blockNum = await publicClient.getBlockNumber();
    console.log(blockNum);
    
    // (Similarly, you could do):
    // const memoryClient = createMemoryClient({ chain: optimism, transport: http(...)({}) });
    // memoryClient.getBlockNumber();
    
    // WalletClient example (when you want to sign transactions):
    const walletClient = createWalletClient({
      chain: optimism,
      transport: http("https://optimism-mainnet.infura.io/v3/YOUR_PROJECT_ID")({}),
      account: "0xYourAddress",
      privateKey: "0xYourPrivateKey",
    });
    const balance = await walletClient.getBalance();
    console.log(balance);

Tevm Concepts

  1. JSON-RPC Procedures All Tevm RPC methods live under tevm/actions. Internally, Tevm has:

    • An EVM implementation in Zig that executes opcodes.
    • A state manager and immutable blockchain structure.
    • (Optionally) Anvil compatibility to mirror Hardhat/Anvil behavior.

    You can create a fully in-memory EIP-1193 node like this:

    import { createTevmNode } from "tevm/node";
    import { eip1193RequestFn } from "tevm/eip1193";
    
    // Create a new Tevm node with default configuration:
    const node = createTevmNode().extend(eip1193RequestFn());
    
    // Now you can call any JSON-RPC method on `node.request(...)`:
    const blockNumber = await node.request({ method: "eth_blockNumber" });
    console.log(blockNumber); // e.g. "0x0" (hex-encoded)
  2. Node Configuration When instantiating a Tevm node, you can pass custom settings:

    import { createTevmNode } from "tevm/node";
    import { eip1193RequestFn } from "tevm/eip1193";
    import { precompiles, EIPInfo } from "tevm/config";
    
    const node = createTevmNode({
      chainId: 420, // custom chainId
      eips: EIPInfo.latest,
      precompiles: [...precompiles.default],
    }).extend(eip1193RequestFn());
  3. Communication Methods

    • Direct JSON-RPC

      const blockNumber = await node.request({ method: "eth_blockNumber" });
    • HTTP Server

      import { createServer } from "tevm/server";
      import { createMemoryClient } from "tevm";
      
      // Wrap Tevm in an HTTP server (listens on port 8545):
      const server = createServer(createMemoryClient());
      server.listen(8545, () => {
        console.log("listening on port 8545");
      
        // From anywhere (e.g., another process), you can do:
        import { http } from "viem";
        http("http://localhost:8545")({}).request({
          method: "eth_blockNumber",
        })
        .then((response) => {
          console.log(response);
        });
      });

      Output in console:

      listening on port 8545
      response {
        jsonrpc: "2.0",
        method: "eth_blockNumber",
        result: "0x0",
        id: 1
      }
    • In-Memory Client (Viem/Ethers)

      • Using Viem

        import { createMemoryClient, createTevmTransport } from "tevm";
        import { createPublicClient } from "viem";
        
        // Use Tevm’s custom transport instead of `http()`:
        const tevmTransport = createTevmTransport({
          chain: { chainId: 1337, name: "Local Tevm" },
          node: createTevmNode().extend(eip1193RequestFn()),
        });
        
        const client = createPublicClient({
          transport: tevmTransport,
          chain: { chainId: 1337, name: "Local Tevm" },
        });
        
        // Now `client.getBlockNumber()` calls go directly to the in‐memory Tevm instance:
        const bn = await client.getBlockNumber();
        console.log(bn); // e.g. 0
      • Using Ethers.js BrowserProvider

        import { createTevmNode } from "tevm/node";
        import { eip1193RequestFn } from "tevm/eip1193";
        import { ethers } from "ethers";
        
        const node = createTevmNode().extend(eip1193RequestFn());
        // Wrap Tevm’s EIP-1193 interface in an Ethers.js provider:
        const provider = new ethers.BrowserProvider(node);
        
        // Now you can use any Ethers.js methods:
        const blockNumber = await provider.getBlockNumber();
        console.log(blockNumber); // e.g. 0
  4. Convenience Clients Tevm also ships its own in-memory convenience client:

    import { createMemoryClient } from "tevm";
    import { eip1193RequestFn } from "tevm/eip1193";
    
    // `createMemoryClient()` under the hood spins up a Tevm node + actions:
    const client = createMemoryClient({
      chain: { chainId: 1337, name: "Local Tevm" },
      eips: [/* … */],
      precompiles: [/* … */],
    });
    
    // Now you can directly call:
    const bn = await client.getBlockNumber();
    console.log(bn); // e.g. 0
    
    // And because we’ve wired up “Anvil” methods, you can set code on an address:
    await client.setCode({
      address: "0xAbC1230000000000000000000000000000000000",
      code: "0x6001600155", // simple runtime bytecode
    });
    const code = await client.getCode({
      address: "0xAbC1230000000000000000000000000000000000",
    });
    console.log(code); // "0x6001600155"
  5. Anvil Compatibility Tevm supports Anvil-style methods (e.g., setCode, impersonateAccount, evm_mine, etc.). For example:

    // Using the same `client` from above:
    // 1. Set bytecode at address
    await client.setCode({
      address: "0xAbC1230000000000000000000000000000000000",
      code: "0x6001600155",
    });
    // 2. Mine a new block
    await client.mine();
    // 3. Check balance, code, etc.
    const code = await client.getCode({ address: "0xAbC1230000000000000000000000000000000000" });
    console.log(code); // "0x6001600155"
  6. Server Wrapping You can also expose Tevm over HTTP by combining createServer with Viem’s http() call:

    import { createServer } from "tevm/server";
    import { createMemoryClient } from "tevm";
    import { http } from "viem";
    
    const server = createServer(createMemoryClient());
    server.listen(8545, () => {
      console.log("listening on port 8545");
    
      http("http://localhost:8545")({}).request({
        method: "eth_blockNumber",
      })
      .then((response) => {
        console.log(response);
      });
    });

    Running bun run example.ts produces:

    ➜  bun run example.ts
    listening on port 8545
    response {
      jsonrpc: "2.0",
      method: "eth_blockNumber",
      result: "0x0",
      id: 1
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment