TypeScript-first: Examples reference an EIP-1193 provider. Zig currently does not include one; build JSON-RPC requests with std.json and send them via std.http.Client. Use primitives.AbiEncoding for calldata and result decoding.
JSONRPCProvider
EIP-1193 compliant Ethereum JSON-RPC provider interface with branded primitive types and standard event emitter pattern. All types auto-generated from the official OpenRPC specification.
New to JSONRPCProvider? Start with Getting Started for installation and your first requests.
Overview
JSONRPCProvider implements the EIP-1193 provider interface for interacting with Ethereum nodes via JSON-RPC:
- EIP-1193 compliant - Standard
request(args) method
- Branded primitives - Type-safe params using Address, Hash, Hex, Quantity
- Throws on error - Standard error handling with exceptions
- EventEmitter pattern - Standard on/removeListener for events
- Auto-generated types - Generated from ethereum/execution-apis OpenRPC spec
Provider Interface
// Zig: Build JSON and POST with std.http.Client
const std = @import("std");
pub fn requestExample(allocator: std.mem.Allocator) !void {
var s = std.json.Stringify.init(allocator);
defer s.deinit();
try s.beginObject();
try s.field("jsonrpc", "2.0");
try s.field("id", 1);
try s.field("method", "eth_blockNumber");
try s.field("params", &[_]u8{});
try s.endObject();
const body = s.buf.*; // POST to node URL
_ = body;
}
Request Arguments
Create requests using request builders:
// Zig: Use the same structure: {"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}
// Parse result with std.json.parse and handle errors with try/catch.
Key Features
Architecture
OpenRPC Specification (ethereum/execution-apis)
↓
JSON-RPC Type Definitions
(eth, debug, engine)
↓
Request Builders
(Rpc.Eth.*, Rpc.Debug.*, Rpc.Engine.*)
↓
EIP-1193 Provider Interface
(request(args) method)
↓
Transport Layer
(HTTP, WebSocket, IPC, Custom)
API Methods
Method Namespaces
Core Concepts
Advanced Topics
Types
Provider
Request Builders
Error Handling
Common Types
// See requestExample above for the Zig pattern
// Zig: Build the same payloads by name and params with std.json
// Examples:
// {"method":"eth_getBalance","params":["0x...","latest"]}
// {"method":"eth_call","params":[{"to":"0x...","data":"0x..."},"latest"]}
// Handle JSON-RPC errors in Zig by parsing the response
const std = @import("std");
pub fn handleRpcResponse(resp_body: []const u8) !void {
var parser = std.json.Parser.init(std.heap.page_allocator, .{});
defer parser.deinit();
const tree = try parser.parse(resp_body);
defer tree.deinit();
const root = tree.root;
if (root.object.get("error")) |err_node| {
const code = err_node.object.get("code").?.integer;
const message = err_node.object.get("message").?.string;
return error.RpcError; // attach code/message in your error type
}
// else handle result
}
EIP-1193 Error Structure:// Represent EIP-1193 errors in Zig
pub const ProviderRpcError = struct {
code: i64,
message: []const u8,
data: ?[]const u8 = null,
};
// Block tags
type BlockTag = 'latest' | 'earliest' | 'pending' | 'safe' | 'finalized' | Quantity;
// Branded primitives
import type { brand } from '@tevm/voltaire/brand';
type Address = Uint8Array & { readonly [brand]: "Address" };
type Hash = Uint8Array & { readonly [brand]: "Hash" };
type Hex = string & { readonly [brand]: "Hex" };
type Quantity = string & { readonly [brand]: "Quantity" }; // Hex-encoded numbers
// Request types
interface CallParams {
from?: Address;
to: Address;
gas?: Quantity;
gasPrice?: Quantity;
value?: Quantity;
data?: Hex;
}
interface LogFilter {
fromBlock?: BlockTag;
toBlock?: BlockTag;
address?: Address | Address[];
topics?: (Hash | Hash[] | null)[];
}
Usage Patterns
Check Balance and Nonce
import * as Rpc from '@tevm/voltaire/jsonrpc';
import * as Address from '@tevm/voltaire/Address';
const address = Address('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0');
// Create requests using builders
const balanceReq = Rpc.Eth.GetBalanceRequest(address, 'latest');
const nonceReq = Rpc.Eth.GetTransactionCountRequest(address, 'latest');
try {
const [balance, nonce] = await Promise.all([
provider.request(balanceReq),
provider.request(nonceReq)
]);
console.log('Balance:', balance);
console.log('Nonce:', nonce);
} catch (error) {
console.error('RPC error:', error.message);
}
Query Contract State
import * as Rpc from '@tevm/voltaire/jsonrpc';
import * as Address from '@tevm/voltaire/Address';
import * as Hex from '@tevm/voltaire/Hex';
const contractAddress = Address('0x...');
try {
// Check if contract exists
const code = await provider.request(
Rpc.Eth.GetCodeRequest(contractAddress, 'latest')
);
if (code !== '0x') {
// Contract exists, call totalSupply()
const result = await provider.request(
Rpc.Eth.CallRequest({
to: contractAddress,
data: Hex('0x18160ddd') // totalSupply() selector
}, 'latest')
);
console.log('Total supply:', result);
}
} catch (error) {
console.error('Query failed:', error.message);
}
Monitor Contract Events
import * as Address from '@tevm/voltaire/Address';
import * as Hash from '@tevm/voltaire/Hash';
const contractAddress = Address('0x...');
const transferSignature = Hash(
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
);
// Subscribe using EIP-1193 event emitter
provider.on('message', (event) => {
if (event.type === 'eth_subscription') {
const log = event.data.result;
console.log('Transfer event:');
console.log(' Block:', log.blockNumber);
console.log(' Transaction:', log.transactionHash);
console.log(' From:', log.topics[1]);
console.log(' To:', log.topics[2]);
}
});
// Or use chainChanged, accountsChanged events
provider.on('chainChanged', (chainId) => {
console.log('Chain changed to:', chainId);
});
Wait for Transaction Confirmation
import * as Rpc from '@tevm/voltaire/jsonrpc';
import * as Hash from '@tevm/voltaire/Hash';
async function waitForConfirmation(
txHash: Hash,
confirmations: number = 3
): Promise<void> {
try {
const receipt = await provider.request(
Rpc.Eth.GetTransactionReceiptRequest(txHash)
);
if (!receipt) {
throw new Error('Transaction not found');
}
const targetBlock = BigInt(receipt.blockNumber) + BigInt(confirmations);
// Poll for block number
while (true) {
const currentBlock = await provider.request(
Rpc.Eth.BlockNumberRequest()
);
if (BigInt(currentBlock) >= targetBlock) {
console.log(`Transaction confirmed with ${confirmations} confirmations`);
break;
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
} catch (error) {
console.error('Confirmation failed:', error.message);
throw error;
}
}
Tree-Shaking
Import only what you need for optimal bundle size:
// Import Provider interface (no implementations)
import type { Provider } from '@tevm/voltaire/provider';
// Import specific primitives used
import * as Address from '@tevm/voltaire/Address';
import * as Hash from '@tevm/voltaire/Hash';
// Only these primitives included in bundle
// Unused primitives (Bytecode, Transaction, etc.) excluded
Importing primitives individually enables tree-shaking. Unused methods and types are excluded from your bundle.
Key Features Compared to Other Libraries
| Feature | Standard EIP-1193 | Voltaire JSONRPCProvider |
|---|
| Method calls | request({ method, params }) | request(Rpc.Eth.MethodRequest()) |
| Parameters | Plain strings/objects | Branded primitive types |
| Events | on(event, listener) | on(event, listener) |
| Errors | Throws exceptions | Throws exceptions |
| Type safety | Basic inference | Full inference with brands |
| Request builders | Manual object creation | Type-safe builder functions |
See Comparison for detailed differences and migration guides.
Primitives
- Address - 20-byte Ethereum addresses with EIP-55 checksumming
- Keccak256 - 32-byte keccak256 hashes
- Hex - Hex-encoded byte strings
- Transaction - Transaction types and encoding
Cryptography
Guides
Specifications