Skip to main content
This page is a high-level overview. The concrete client and examples are TypeScript-first today; in Zig, treat these as reference for constructing std.json JSON-RPC payloads.

Type System

All JSON-RPC types are auto-generated from the ethereum/execution-apis OpenRPC specification, ensuring they stay in sync with the official Ethereum JSON-RPC API.

Auto-Generation from OpenRPC

Types are auto-generated from the official OpenRPC spec. Source: ethereum/execution-apis OpenRPC specification Format: OpenRPC JSON-RPC API description Total methods: 65 across 3 namespaces (eth, debug, engine)

Type Hierarchy

Namespace Organization

jsonrpc/
├── types/              # Hand-written base types
│   ├── Address.ts/zig
│   ├── Keccak256.ts/zig
│   ├── Quantity.ts/zig
│   ├── BlockTag.ts/zig
│   └── BlockSpec.ts/zig
├── eth/                # Generated: 40 eth_* methods
├── debug/              # Generated: 5 debug_* methods
├── engine/             # Generated: 20 engine_* methods
├── JsonRpc.ts/zig      # Root union of all methods
└── index.ts/root.zig   # Module entry
Important: All files except types/ are auto-generated and should not be edited manually.

Base Types

Core types used throughout the JSON-RPC API:
import type { brand } from '@tevm/voltaire/brand';

// Address - 20-byte Ethereum address
type Address = Uint8Array & { readonly [brand]: "Address" };

// Hash - 32-byte hash
type Hash = Uint8Array & { readonly [brand]: "Hash" };

// Hex - Hex-encoded byte string
type Hex = string & { readonly [brand]: "Hex" };

// Quantity - Hex-encoded unsigned integer
type Quantity = string & { readonly [brand]: "Quantity" };

// BlockTag - Block identifier
type BlockTag = 'latest' | 'earliest' | 'pending' | 'safe' | 'finalized' | Quantity;
These types are hand-written in jsonrpc/types/ and used by generated method definitions.

Branded Primitives Integration

Generated types use Tevm’s branded primitive system:
// Generated eth_getBalance params
interface EthGetBalanceParams {
  address: Address;    // Branded Address from primitives
  block: BlockTag;     // BlockTag type
}

// Generated eth_call params
interface EthCallParams {
  from?: Address;
  to: Address;
  gas?: Quantity;
  gasPrice?: Quantity;
  value?: Quantity;
  data?: Hex;          // Branded Hex from primitives
}
Benefits:
  • Type-safe at compile time
  • Zero runtime overhead (just Uint8Array/string)
  • Can’t mix Address and Hash accidentally
  • IDE autocomplete and refactoring

Method Type Structure

Each generated method includes:
// Example: eth_getBalance
export namespace eth_getBalance {
  // Request parameters
  export interface Params {
    address: Address;
    block: BlockTag;
  }

  // Response result type
  export type Result = Quantity;

  // Full method type
  export type Method = (
    address: Address,
    block: BlockTag
  ) => Promise<Response<Quantity>>;
}

Response Type

All methods return the same Response<T> structure:
type Response<T> =
  | { result: T; error?: never }
  | { result?: never; error: RpcError };

interface RpcError {
  code: number;
  message: string;
  data?: unknown;
}
This enforces error checking before accessing results.

Type Safety Examples

Compile-Time Validation

import * as Address from '@tevm/voltaire/Address';
import { Keccak256 } from '@tevm/voltaire/Keccak256';

// ✅ Correct: Address type
await provider.eth_getBalance(Address('0x...'), 'latest');

// ❌ Error: string not assignable to Address
await provider.eth_getBalance('0x...', 'latest');

// ❌ Error: Keccak256Hash not assignable to Address
await provider.eth_getBalance(Keccak256.hash(data), 'latest');

Type Inference

TypeScript infers return types automatically:
// Inferred: Promise<Response<Quantity>>
const balanceResponse = await provider.eth_getBalance(address, 'latest');

// Inferred: Promise<Response<Block>>
const blockResponse = await provider.eth_getBlockByNumber('latest', true);

// Inferred: Promise<Response<Hex>>
const callResponse = await provider.eth_call(params, 'latest');

Discriminated Unions

After checking for errors, TypeScript narrows types:
const response = await provider.eth_getBalance(address, 'latest');

if (response.error) {
  // response.error: RpcError
  // response.result: undefined
  console.error(response.error.message);
} else {
  // response.result: Quantity
  // response.error: undefined
  const balance = BigInt(response.result);
}

TypeScript and Zig Interop

Types are generated for both TypeScript and Zig:
// TypeScript types
import * as eth from '@tevm/voltaire/jsonrpc/eth';

type BalanceParams = typeof eth.eth_getBalance.Params;
type BalanceResult = typeof eth.eth_getBalance.Result;

const params: BalanceParams = {
  address: Address('0x...'),
  block: 'latest'
};

Version Compatibility

Generated types match the version of ethereum/execution-apis used during generation. Current version: Based on latest execution-apis main branch Update frequency: Regenerate types when new RPC methods or parameters are added to the spec Breaking changes: Rare, but follow Ethereum’s JSON-RPC versioning (e.g., engine_newPayloadV1 → V2 → V3)

Custom Types

While most types are generated, you can extend them for application-specific needs:
import type { Provider } from '@tevm/voltaire/provider';
import * as Address from '@tevm/voltaire/Address';

// Custom wrapper type
interface AccountInfo {
  address: Address.AddressType;
  balance: bigint;
  nonce: number;
  isContract: boolean;
}

// Helper function using generated types
async function getAccountInfo(
  provider: Provider,
  address: Address.AddressType
): Promise<AccountInfo | null> {
  const [balanceRes, nonceRes, codeRes] = await Promise.all([
    provider.eth_getBalance(address, 'latest'),
    provider.eth_getTransactionCount(address, 'latest'),
    provider.eth_getCode(address, 'latest')
  ]);

  if (balanceRes.error || nonceRes.error || codeRes.error) {
    return null;
  }

  return {
    address,
    balance: BigInt(balanceRes.result),
    nonce: Number(nonceRes.result),
    isContract: codeRes.result !== '0x'
  };
}

Specifications