WebSocket Subscriptions
Real-time Ethereum event streaming with WebSocket transport in eth.zig.
eth.zig includes a WebSocket transport for real-time subscriptions to Ethereum events -- new blocks, pending transactions, and log events.
Connecting
const eth = @import("eth");
var ws = eth.ws_transport.WebSocketTransport.init(allocator, "wss://eth-mainnet.ws.alchemyapi.io/v2/YOUR_KEY");
defer ws.deinit();The WebSocket transport supports both ws:// and wss:// (TLS) URLs. TLS is handled natively in pure Zig.
Subscriptions
Subscribe to real-time events:
const eth = @import("eth");
var ws = eth.ws_transport.WebSocketTransport.init(allocator, "wss://rpc.example.com");
defer ws.deinit();
// Subscribe to new block headers
var sub = try eth.subscription.Subscription.init(allocator, &ws, .newHeads);
defer sub.deinit();
// Process incoming events
while (try sub.next()) |event| {
// Handle new block header
_ = event;
}JSON-RPC over WebSocket
You can also use WebSocket for regular JSON-RPC calls (lower latency than HTTP for frequent requests):
const eth = @import("eth");
var ws = eth.ws_transport.WebSocketTransport.init(allocator, "wss://rpc.example.com");
defer ws.deinit();
var provider = eth.provider.Provider.init(allocator, &ws);
const block_number = try provider.getBlockNumber();
const balance = try provider.getBalance(address);When to Use WebSocket
| Use Case | Transport |
|---|---|
| One-off RPC calls | HTTP |
| High-frequency reads | WebSocket |
| Real-time event streaming | WebSocket |
| Subscription to new blocks | WebSocket |
| Watching for specific events | WebSocket |
URL Format
ws://host:port/path -- unencrypted
wss://host:port/path -- TLS encrypted (recommended)Default ports: 80 for ws://, 443 for wss://.
Watching Logs Per Block (LogWatcher)
The pattern "on each new block, fetch filtered logs and process them" is the core
of most on-chain bots (liquidation keepers, arbitrage, searchers). log_watcher
packages it: a newHeads subscription drives block-scoped eth_getLogs calls,
blocks missed across reconnects are back-filled, and reorgs (parent-hash mismatch
on sequential heads) re-fetch the replaced range.
const eth = @import("eth");
const io = eth.runtime.blockingIo();
var transport = eth.http_transport.HttpTransport.init(allocator, "https://rpc.example.com", io);
defer transport.deinit();
var provider = eth.provider.Provider.init(allocator, &transport);
const client = try eth.ws_client.WsClient.connect(allocator, "wss://rpc.example.com/ws", io, .{});
defer client.deinit();
var watcher = try eth.log_watcher.LogWatcher.init(allocator, &provider, client, .{
.address = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH
.topics = &.{ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" }, // Transfer
}, .{ .start_block_lag = 0, .handle_reorgs = true });
defer watcher.deinit();
while (true) {
const logs = try watcher.pollOnce(); // blocks until the next head
defer eth.log_watcher.freeLogs(allocator, logs);
for (logs) |log| {
// process log.address / log.topics / log.data
}
}Notes:
- On reorgs, logs for the replaced range are re-delivered; key your processing
on
(block_hash, log_index)to deduplicate. start_block_laglets the first poll start N blocks behind the first observed head (useful for warm-up).- A callback-style wrapper
eth.log_watcher.watchLogs(...)is also available.
Pluggable Io
Every network entry point in eth.zig takes a std.Io explicitly, in line with
Zig 0.16's guidance to pass I/O handles rather than reaching for a hidden global.
This includes HttpTransport.init, WsTransport.connect, WsClient.connect,
flashbots.Relay.init, and FallbackProvider.init, as well as functions that
need randomness or sleep (mnemonic.generate, keystore.encrypt,
runtime.milliTimestamp, runtime.sleepMs). Wrappers like Provider,
Wallet, RetryingProvider, and NonceManager inherit the Io of the
transport or provider they wrap, so you only pass it once at construction.
Because the Io is yours to provide, you can drive eth.zig on your own event
loop -- a std.Io.Evented implementation, a custom green-thread scheduler, or
anything that satisfies the std.Io interface. For the common blocking case,
eth.runtime.blockingIo() returns a ready-to-use synchronous Io:
const io = eth.runtime.blockingIo();
var transport = eth.http_transport.HttpTransport.init(allocator, "https://rpc.example.com", io);
defer transport.deinit();
const client = try eth.ws_client.WsClient.connect(allocator, "wss://rpc.example.com/ws", io, .{});
defer client.deinit();In an executable that uses the pub fn main(init: std.process.Init) !void
entry point, pass init.io instead to thread the process's real Io all the
way down.