diff --git a/CHANGELOG.md b/CHANGELOG.md index 9511e85..88759fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - EIP-7702 SetCode transactions (type 0x04), shipped in Pectra and live on mainnet (#66). New `transaction.Authorization` tuple `{chain_id, address, nonce, y_parity, r, s}` and `transaction.Eip7702Transaction` (note: `to` is non-nullable -- type-0x04 cannot create contracts), wired into the `Transaction` union and the `serializeForSigning`/`hashForSigning`/`serializeSigned` dispatch. The signing payload is `0x04 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, value, data, access_list, authorization_list])` (signed appends `y_parity, r, s`), each authorization encoded as `[chain_id, address, nonce, y_parity, r, s]`. `signer.signAuthorization(allocator, chain_id, address, nonce)` signs the authorization hash `keccak256(0x05 || rlp([chain_id, address, nonce]))` (MAGIC = 0x05) and fills `y_parity/r/s`; `signer.hashAuthorization` exposes that hash. `rpc_transaction.RpcTransaction` gains an optional `authorization_list`, parsed best-effort from an RPC `authorizationList`. Verified via sign-then-recover round trip (recovered signer equals the authority) plus fixed RLP-shape assertions; no official EIP-7702 signing test vector was found in the EIP or this repo, so correctness rests on the round trip plus shape checks - `fallback_provider` module: multi-RPC failover provider (#70). `FallbackProvider.init(allocator, endpoints, opts)` owns one `HttpTransport`+`Provider` per endpoint and exposes the same read method surface as `Provider` (`getBalance`, `getBlockNumber`, `call`, `getLogs`, `sendRawTransaction`, ...) as a drop-in replacement. On a transport/connection failure it fails over to the next healthy endpoint; after `failover_threshold` consecutive failures an endpoint is marked unhealthy and skipped, and a periodic probe (`recovery_probe_ms` after its last failure) restores it, with lower-index endpoints always preferred so a recovered primary is reclaimed. Crucially, failover fires ONLY on transport errors (`error.ConnectionFailed`/`error.HttpError`/socket errors); an `error.RpcError` is the node's real answer (a revert, unsupported method, rate-limit) and is returned to the caller WITHOUT failover. The failover/recovery state machine is a pure function `selectEndpoint(health, now_ms, opts, tried)` over injectable timestamps, and the failover-vs-RpcError decision is the pure `isFailoverError(err)`, both unit-tested deterministically without network -- `keystore` module: encrypted JSON keystore support (Web3 Secret Storage v3), the on-disk format used by geth/ethers/foundry/MyEtherWallet (#65). `decrypt(allocator, json, password)` parses a v3 document, derives the key via either KDF (`scrypt` or `pbkdf2`-HMAC-SHA256), verifies `MAC = keccak256(derived_key[16..32] ++ ciphertext)` in constant time (`std.crypto.timing_safe.eql`, returning `error.InvalidPassword` on mismatch), and AES-128-CTR decrypts to recover the 32-byte private key. `encrypt(allocator, key, password, opts)` produces a v3 JSON string (defaulting to scrypt N=2^18 + aes-128-ctr, with a v4 UUID and a random salt/IV drawn from the runtime `Io`). All derived keys, the plaintext key copy, and the ciphertext buffer are wiped with `secureZero` on exit. Verified against the canonical Web3 Secret Storage scrypt and pbkdf2 test vectors (both decrypt to `7a28b5ba...fe9d`), proving interop with geth/ethers/foundry +- `keystore` module: encrypted JSON keystore support (Web3 Secret Storage v3), the on-disk format used by geth/ethers/foundry/MyEtherWallet (#65). `decrypt(allocator, json, password)` parses a v3 document, derives the key via either KDF (`scrypt` or `pbkdf2`-HMAC-SHA256), verifies `MAC = keccak256(derived_key[16..32] ++ ciphertext)` in constant time (`std.crypto.timing_safe.eql`, returning `error.InvalidPassword` on mismatch), and AES-128-CTR decrypts to recover the 32-byte private key. `encrypt(allocator, key, password, io, opts)` produces a v3 JSON string (defaulting to scrypt N=2^18 + aes-128-ctr, with a v4 UUID and a random salt/IV drawn from the caller's `Io`). All derived keys, the plaintext key copy, and the ciphertext buffer are wiped with `secureZero` on exit. Verified against the canonical Web3 Secret Storage scrypt and pbkdf2 test vectors (both decrypt to `7a28b5ba...fe9d`), proving interop with geth/ethers/foundry + +### Changed +- **BREAKING:** `std.Io` is now an explicit parameter on every network constructor and on the functions that need randomness or sleep, removing the hidden global `Io` and aligning with Zig 0.16's guidance to pass I/O handles explicitly rather than reaching for a global default. New signatures: `HttpTransport.init(allocator, url, io)`, `WsTransport.connect(allocator, url, io)`, `WsClient.connect(allocator, url, io, opts)`, `flashbots.Relay.init(allocator, url, auth_key, io)`, `FallbackProvider.init(allocator, endpoints, io, opts)`, `MevShareClient.init(allocator, relay_url, stream_url, auth_key, io)` / `initMainnet(allocator, auth_key, io)`, `mnemonic.generate(io, comptime entropy_size)`, `keystore.encrypt(allocator, key, password, io, opts)`, and the runtime helpers `runtime.milliTimestamp(io)` / `runtime.sleepMs(io, ms)`. `Provider`, `Wallet`, `RetryingProvider`, and `NonceManager` inherit the `Io` of the transport/provider they wrap (via `Provider.io()`), so they take no new parameter. `runtime.defaultIo()` is renamed to `runtime.blockingIo()` and is now just a convenient blocking `Io` value to pass explicitly -- pass `eth.runtime.blockingIo()` for the previous default blocking behavior. Because the `Io` is caller-provided, eth.zig can now run on your own event loop. ## [0.6.0] - 2026-06-10 diff --git a/docs/content/docs/batch-calls.mdx b/docs/content/docs/batch-calls.mdx index 6456239..ca2eb75 100644 --- a/docs/content/docs/batch-calls.mdx +++ b/docs/content/docs/batch-calls.mdx @@ -11,7 +11,7 @@ eth.zig supports [JSON-RPC batch requests](https://www.jsonrpc.org/specification const eth = @import("eth"); // Set up provider -var transport = eth.http_transport.HttpTransport.init(allocator, "https://rpc.example.com"); +var transport = eth.http_transport.HttpTransport.init(allocator, "https://rpc.example.com", eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); diff --git a/docs/content/docs/ens.mdx b/docs/content/docs/ens.mdx index e1f9bde..fab34f0 100644 --- a/docs/content/docs/ens.mdx +++ b/docs/content/docs/ens.mdx @@ -12,7 +12,7 @@ Resolve an ENS name to an address: ```zig const eth = @import("eth"); -var transport = eth.http_transport.HttpTransport.init(allocator, "https://eth.llamarpc.com"); +var transport = eth.http_transport.HttpTransport.init(allocator, "https://eth.llamarpc.com", eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); diff --git a/docs/content/docs/examples.mdx b/docs/content/docs/examples.mdx index ab48c85..bd260da 100644 --- a/docs/content/docs/examples.mdx +++ b/docs/content/docs/examples.mdx @@ -45,7 +45,7 @@ const checksum = eth.primitives.addressToChecksum(&addr); ```zig const eth = @import("eth"); -var transport = eth.http_transport.HttpTransport.init(allocator, "https://rpc.example.com"); +var transport = eth.http_transport.HttpTransport.init(allocator, "https://rpc.example.com", eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); @@ -132,7 +132,7 @@ const v2_swap = comptime eth.keccak.selector( "swapExactTokensForTokens(uint256,uint256,address[],address,uint256)", ); -var client = eth.mev_share.MevShareClient.init(allocator, relay_url, stream_url, auth_key); +var client = eth.mev_share.MevShareClient.init(allocator, relay_url, stream_url, auth_key, eth.runtime.blockingIo()); defer client.deinit(); // Subscribe to the SSE stream; the callback fires per pending-tx hint. diff --git a/docs/content/docs/fallback-provider.mdx b/docs/content/docs/fallback-provider.mdx index 2149466..5984f73 100644 --- a/docs/content/docs/fallback-provider.mdx +++ b/docs/content/docs/fallback-provider.mdx @@ -22,7 +22,7 @@ var fb = try eth.fallback_provider.FallbackProvider.init(allocator, &.{ "https://primary.example.com", "https://backup-a.example.com", "https://backup-b.example.com", -}, .{}); +}, eth.runtime.blockingIo(), .{}); defer fb.deinit(); // Same surface as Provider. If the primary's connection fails, this call @@ -108,7 +108,7 @@ pub const FallbackOpts = struct { ``` ```zig -var fb = try eth.fallback_provider.FallbackProvider.init(allocator, endpoints, .{ +var fb = try eth.fallback_provider.FallbackProvider.init(allocator, endpoints, eth.runtime.blockingIo(), .{ .failover_threshold = 5, .recovery_probe_ms = 10_000, }); diff --git a/docs/content/docs/introduction.mdx b/docs/content/docs/introduction.mdx index adf9d82..bd49506 100644 --- a/docs/content/docs/introduction.mdx +++ b/docs/content/docs/introduction.mdx @@ -49,7 +49,7 @@ pub fn main() !void { _ = checksum; // Sign and send a transaction - var transport = eth.http_transport.HttpTransport.init(allocator, "https://rpc.example.com"); + var transport = eth.http_transport.HttpTransport.init(allocator, "https://rpc.example.com", eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); diff --git a/docs/content/docs/keystore.mdx b/docs/content/docs/keystore.mdx index 22f2959..62811ab 100644 --- a/docs/content/docs/keystore.mdx +++ b/docs/content/docs/keystore.mdx @@ -49,7 +49,7 @@ The cipher is always `aes-128-ctr`, the only cipher the v3 spec defines. geth, ethers, or cast. The caller owns the returned slice. ```zig -const json = try eth.keystore.encrypt(allocator, private_key, "my password", .{}); +const json = try eth.keystore.encrypt(allocator, private_key, "my password", eth.runtime.blockingIo(), .{}); defer allocator.free(json); // json is a complete {"version":3,"id":...,"crypto":{...}} document. ``` @@ -61,11 +61,11 @@ write. Those defaults are deliberately expensive; override them through ```zig // Faster scrypt for a test fixture (N = 2^12 = 4096): -const fast = try eth.keystore.encrypt(allocator, key, "pw", .{ .scrypt_log_n = 12 }); +const fast = try eth.keystore.encrypt(allocator, key, "pw", eth.runtime.blockingIo(), .{ .scrypt_log_n = 12 }); defer allocator.free(fast); // pbkdf2-HMAC-SHA256 instead of scrypt: -const pb = try eth.keystore.encrypt(allocator, key, "pw", .{ +const pb = try eth.keystore.encrypt(allocator, key, "pw", eth.runtime.blockingIo(), .{ .kdf = .pbkdf2, .pbkdf2_c = 262144, }); diff --git a/docs/content/docs/mev-share.mdx b/docs/content/docs/mev-share.mdx index 6f5fbd0..84135bf 100644 --- a/docs/content/docs/mev-share.mdx +++ b/docs/content/docs/mev-share.mdx @@ -20,7 +20,8 @@ API are public and need no authentication. const eth = @import("eth"); // auth_key is your Flashbots reputation key (NOT a funded key). -var client = eth.mev_share.MevShareClient.initMainnet(allocator, auth_key); +const io = eth.runtime.blockingIo(); +var client = eth.mev_share.MevShareClient.initMainnet(allocator, auth_key, io); defer client.deinit(); // Or with explicit endpoints (e.g. Sepolia): @@ -29,6 +30,7 @@ var sepolia = eth.mev_share.MevShareClient.init( "https://relay-sepolia.flashbots.net", eth.mev_share.sepolia_stream_url, auth_key, + io, ); defer sepolia.deinit(); ``` diff --git a/docs/content/docs/nonce-manager.mdx b/docs/content/docs/nonce-manager.mdx index a914df8..643e743 100644 --- a/docs/content/docs/nonce-manager.mdx +++ b/docs/content/docs/nonce-manager.mdx @@ -15,7 +15,7 @@ single atomic operation -- no RPC and no lock on the hot path. ```zig const eth = @import("eth"); -var transport = eth.http_transport.HttpTransport.init(allocator, "https://rpc.example.com"); +var transport = eth.http_transport.HttpTransport.init(allocator, "https://rpc.example.com", eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); diff --git a/docs/content/docs/transactions.mdx b/docs/content/docs/transactions.mdx index 96bd0fb..7699bda 100644 --- a/docs/content/docs/transactions.mdx +++ b/docs/content/docs/transactions.mdx @@ -12,7 +12,7 @@ The `Wallet` handles the full lifecycle: auto-filling nonce, gas, and chain ID f ```zig const eth = @import("eth"); -var transport = eth.http_transport.HttpTransport.init(allocator, "https://rpc.example.com"); +var transport = eth.http_transport.HttpTransport.init(allocator, "https://rpc.example.com", eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); diff --git a/docs/content/docs/websockets.mdx b/docs/content/docs/websockets.mdx index bcb8666..8266a62 100644 --- a/docs/content/docs/websockets.mdx +++ b/docs/content/docs/websockets.mdx @@ -82,11 +82,13 @@ on sequential heads) re-fetch the replaced range. ```zig const eth = @import("eth"); -var transport = eth.http_transport.HttpTransport.init(allocator, "https://rpc.example.com"); +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", .{}); +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, .{ @@ -111,3 +113,33 @@ Notes: - `start_block_lag` lets 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`: + +```zig +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. diff --git a/examples/01_derive_address.zig b/examples/01_derive_address.zig index fe7445c..7d8d13a 100644 --- a/examples/01_derive_address.zig +++ b/examples/01_derive_address.zig @@ -7,7 +7,7 @@ const eth = @import("eth"); pub fn main() !void { var buf: [4096]u8 = undefined; - var stdout_impl = std.Io.File.stdout().writerStreaming(eth.runtime.defaultIo(), &buf); + var stdout_impl = std.Io.File.stdout().writerStreaming(eth.runtime.blockingIo(), &buf); const stdout = &stdout_impl.interface; // Hardhat/Anvil account #0 private key (DO NOT use in production) diff --git a/examples/02_check_balance.zig b/examples/02_check_balance.zig index 92f2741..807af32 100644 --- a/examples/02_check_balance.zig +++ b/examples/02_check_balance.zig @@ -7,8 +7,10 @@ const std = @import("std"); const eth = @import("eth"); pub fn main() !void { + const io = eth.runtime.blockingIo(); + var buf: [4096]u8 = undefined; - var stdout_impl = std.Io.File.stdout().writerStreaming(eth.runtime.defaultIo(), &buf); + var stdout_impl = std.Io.File.stdout().writerStreaming(io, &buf); const stdout = &stdout_impl.interface; var gpa: std.heap.DebugAllocator(.{}) = .init; @@ -16,7 +18,7 @@ pub fn main() !void { const allocator = gpa.allocator(); // Connect to local node - var transport = eth.http_transport.HttpTransport.init(allocator, "http://127.0.0.1:8545"); + var transport = eth.http_transport.HttpTransport.init(allocator, "http://127.0.0.1:8545", io); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); diff --git a/examples/03_sign_message.zig b/examples/03_sign_message.zig index 341b29f..07c2048 100644 --- a/examples/03_sign_message.zig +++ b/examples/03_sign_message.zig @@ -7,7 +7,7 @@ const eth = @import("eth"); pub fn main() !void { var buf: [4096]u8 = undefined; - var stdout_impl = std.Io.File.stdout().writerStreaming(eth.runtime.defaultIo(), &buf); + var stdout_impl = std.Io.File.stdout().writerStreaming(eth.runtime.blockingIo(), &buf); const stdout = &stdout_impl.interface; // Hardhat/Anvil account #0 private key diff --git a/examples/04_send_transaction.zig b/examples/04_send_transaction.zig index 74b73b7..793ef0c 100644 --- a/examples/04_send_transaction.zig +++ b/examples/04_send_transaction.zig @@ -7,8 +7,10 @@ const std = @import("std"); const eth = @import("eth"); pub fn main() !void { + const io = eth.runtime.blockingIo(); + var buf: [4096]u8 = undefined; - var stdout_impl = std.Io.File.stdout().writerStreaming(eth.runtime.defaultIo(), &buf); + var stdout_impl = std.Io.File.stdout().writerStreaming(io, &buf); const stdout = &stdout_impl.interface; var gpa: std.heap.DebugAllocator(.{}) = .init; @@ -16,7 +18,7 @@ pub fn main() !void { const allocator = gpa.allocator(); // Connect to local Anvil node - var transport = eth.http_transport.HttpTransport.init(allocator, "http://127.0.0.1:8545"); + var transport = eth.http_transport.HttpTransport.init(allocator, "http://127.0.0.1:8545", io); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); diff --git a/examples/05_read_erc20.zig b/examples/05_read_erc20.zig index 4391305..91d8668 100644 --- a/examples/05_read_erc20.zig +++ b/examples/05_read_erc20.zig @@ -7,8 +7,10 @@ const std = @import("std"); const eth = @import("eth"); pub fn main() !void { + const io = eth.runtime.blockingIo(); + var buf: [4096]u8 = undefined; - var stdout_impl = std.Io.File.stdout().writerStreaming(eth.runtime.defaultIo(), &buf); + var stdout_impl = std.Io.File.stdout().writerStreaming(io, &buf); const stdout = &stdout_impl.interface; var gpa: std.heap.DebugAllocator(.{}) = .init; @@ -16,7 +18,7 @@ pub fn main() !void { const allocator = gpa.allocator(); // Connect to local node - var transport = eth.http_transport.HttpTransport.init(allocator, "http://127.0.0.1:8545"); + var transport = eth.http_transport.HttpTransport.init(allocator, "http://127.0.0.1:8545", io); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); diff --git a/examples/06_hd_wallet.zig b/examples/06_hd_wallet.zig index bcab72f..66ac347 100644 --- a/examples/06_hd_wallet.zig +++ b/examples/06_hd_wallet.zig @@ -8,7 +8,7 @@ const eth = @import("eth"); pub fn main() !void { var buf: [4096]u8 = undefined; - var stdout_impl = std.Io.File.stdout().writerStreaming(eth.runtime.defaultIo(), &buf); + var stdout_impl = std.Io.File.stdout().writerStreaming(eth.runtime.blockingIo(), &buf); const stdout = &stdout_impl.interface; // Standard test mnemonic (DO NOT use in production) diff --git a/examples/07_selectors.zig b/examples/07_selectors.zig index 4e3b2da..2e66d33 100644 --- a/examples/07_selectors.zig +++ b/examples/07_selectors.zig @@ -9,7 +9,7 @@ const eth = @import("eth"); pub fn main() !void { var buf: [4096]u8 = undefined; - var stdout_impl = std.Io.File.stdout().writerStreaming(eth.runtime.defaultIo(), &buf); + var stdout_impl = std.Io.File.stdout().writerStreaming(eth.runtime.blockingIo(), &buf); const stdout = &stdout_impl.interface; // Comptime: evaluated at compile time, zero runtime cost diff --git a/examples/08_mev_share_backrunner.zig b/examples/08_mev_share_backrunner.zig index 280bf8c..f89b030 100644 --- a/examples/08_mev_share_backrunner.zig +++ b/examples/08_mev_share_backrunner.zig @@ -288,9 +288,10 @@ fn parseKey(s: []const u8) ![32]u8 { pub fn main(init: std.process.Init) !void { const allocator = init.gpa; const env = init.environ_map; + const io = init.io; var buf: [4096]u8 = undefined; - var stdout_impl = std.Io.File.stdout().writerStreaming(eth.runtime.defaultIo(), &buf); + var stdout_impl = std.Io.File.stdout().writerStreaming(io, &buf); const stdout = &stdout_impl.interface; // -- Read configuration --------------------------------------------------- @@ -319,12 +320,12 @@ pub fn main(init: std.process.Init) !void { // MevShareClient bundles the authenticated relay (mev_sendBundle) with // the public SSE stream endpoint. - var client = eth.mev_share.MevShareClient.init(allocator, relay_url, stream_url, auth_key); + var client = eth.mev_share.MevShareClient.init(allocator, relay_url, stream_url, auth_key, io); defer client.deinit(); const auth_address = try client.relay.authAddress(); // -- Live mode needs an RPC for nonce / fees / target block -------------- - var transport = eth.http_transport.HttpTransport.init(allocator, rpc_url); + var transport = eth.http_transport.HttpTransport.init(allocator, rpc_url, io); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); diff --git a/src/contract.zig b/src/contract.zig index da54f1a..0476ec2 100644 --- a/src/contract.zig +++ b/src/contract.zig @@ -1,6 +1,7 @@ const std = @import("std"); const provider_mod = @import("provider.zig"); const http_transport_mod = @import("http_transport.zig"); +const runtime = @import("runtime.zig"); const wallet_mod = @import("wallet.zig"); const abi_encode = @import("abi_encode.zig"); const abi_decode = @import("abi_decode.zig"); @@ -110,7 +111,7 @@ pub const Contract = struct { test "Contract.init sets fields correctly" { const contract_addr = @as([20]u8, @splat(0xaa)); - var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = provider_mod.Provider.init(std.testing.allocator, &transport); const contract = Contract.init(std.testing.allocator, contract_addr, &provider); diff --git a/src/erc20.zig b/src/erc20.zig index b02822a..eab25f1 100644 --- a/src/erc20.zig +++ b/src/erc20.zig @@ -7,6 +7,7 @@ const keccak = @import("keccak.zig"); const provider_mod = @import("provider.zig"); const wallet_mod = @import("wallet.zig"); const http_transport_mod = @import("http_transport.zig"); +const runtime = @import("runtime.zig"); const AbiValue = abi_encode.AbiValue; const AbiType = abi_types.AbiType; @@ -220,7 +221,7 @@ test "ERC20 Approval topic matches known value" { test "ERC20.init sets address correctly" { const token_addr = @as([20]u8, @splat(0xaa)); - var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var prov = provider_mod.Provider.init(std.testing.allocator, &transport); const erc20 = ERC20.init(std.testing.allocator, token_addr, &prov); diff --git a/src/erc721.zig b/src/erc721.zig index 3c9d0d7..66e4488 100644 --- a/src/erc721.zig +++ b/src/erc721.zig @@ -7,6 +7,7 @@ const keccak = @import("keccak.zig"); const provider_mod = @import("provider.zig"); const wallet_mod = @import("wallet.zig"); const http_transport_mod = @import("http_transport.zig"); +const runtime = @import("runtime.zig"); const AbiValue = abi_encode.AbiValue; const AbiType = abi_types.AbiType; @@ -261,7 +262,7 @@ test "ERC721 ApprovalForAll topic matches known value" { test "ERC721.init sets address correctly" { const nft_addr = @as([20]u8, @splat(0xbb)); - var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var prov = provider_mod.Provider.init(std.testing.allocator, &transport); const erc721 = ERC721.init(std.testing.allocator, nft_addr, &prov); diff --git a/src/fallback_provider.zig b/src/fallback_provider.zig index 4009d13..a52b4e6 100644 --- a/src/fallback_provider.zig +++ b/src/fallback_provider.zig @@ -162,7 +162,7 @@ pub fn selectEndpoint( /// var fb = try FallbackProvider.init(allocator, &.{ /// "https://primary.example.com", /// "https://backup.example.com", -/// }, .{}); +/// }, eth.runtime.blockingIo(), .{}); /// defer fb.deinit(); /// const block_num = try fb.getBlockNumber(); pub const FallbackProvider = struct { @@ -174,6 +174,9 @@ pub const FallbackProvider = struct { /// Per-endpoint health, parallel to `providers`. health: []EndpointHealth, opts: FallbackOpts, + /// The `std.Io` shared by every endpoint transport; also drives the + /// health-tracking timestamps. + io: std.Io, /// Initialise a FallbackProvider over an ordered list of endpoint URLs. /// The URLs are referenced (not copied); they must outlive the provider, @@ -183,6 +186,7 @@ pub const FallbackProvider = struct { pub fn init( allocator: std.mem.Allocator, endpoints: []const []const u8, + io: std.Io, opts: FallbackOpts, ) !FallbackProvider { if (endpoints.len == 0) return error.NoEndpoints; @@ -196,7 +200,7 @@ pub const FallbackProvider = struct { errdefer allocator.free(health); for (endpoints, 0..) |url, i| { - transports[i] = HttpTransport.init(allocator, url); + transports[i] = HttpTransport.init(allocator, url, io); health[i] = .{}; } // Bind each provider to its transport. Done in a second pass so the @@ -212,6 +216,7 @@ pub const FallbackProvider = struct { .providers = providers, .health = health, .opts = opts, + .io = io, }; } @@ -351,13 +356,13 @@ pub const FallbackProvider = struct { var attempts: usize = 0; while (attempts < self.providers.len) : (attempts += 1) { - const now = runtime.milliTimestamp(); + const now = runtime.milliTimestamp(self.io); const idx = selectEndpoint(self.health, now, self.opts, tried) orelse break; tried[idx] = true; const result = @call(.auto, @field(Provider, method), .{&self.providers[idx]} ++ args); if (result) |ok| { - self.health[idx].recordSuccess(runtime.milliTimestamp()); + self.health[idx].recordSuccess(runtime.milliTimestamp(self.io)); return ok; } else |err| { if (!isFailoverError(err)) { @@ -365,7 +370,7 @@ pub const FallbackProvider = struct { // over, and do NOT count it against endpoint health. return err; } - self.health[idx].recordFailure(runtime.milliTimestamp()); + self.health[idx].recordFailure(runtime.milliTimestamp(self.io)); last_err = err; } } @@ -498,7 +503,7 @@ test "selectEndpoint - reclaims primary once its streak clears" { } test "FallbackProvider - init requires at least one endpoint" { - try testing.expectError(error.NoEndpoints, FallbackProvider.init(testing.allocator, &.{}, .{})); + try testing.expectError(error.NoEndpoints, FallbackProvider.init(testing.allocator, &.{}, runtime.blockingIo(), .{})); } test "FallbackProvider - init/deinit wires one provider per endpoint" { @@ -506,7 +511,7 @@ test "FallbackProvider - init/deinit wires one provider per endpoint" { "http://localhost:8545", "http://localhost:8546", }; - var fb = try FallbackProvider.init(testing.allocator, &endpoints, .{}); + var fb = try FallbackProvider.init(testing.allocator, &endpoints, runtime.blockingIo(), .{}); defer fb.deinit(); try testing.expectEqual(@as(usize, 2), fb.providers.len); diff --git a/src/flashbots.zig b/src/flashbots.zig index 102d074..3ab5544 100644 --- a/src/flashbots.zig +++ b/src/flashbots.zig @@ -243,12 +243,12 @@ pub const Relay = struct { /// Create a new Relay client. /// auth_key is the private key used for Flashbots authentication /// (separate from the transaction signing key). - pub fn init(allocator: std.mem.Allocator, url: []const u8, auth_key: [32]u8) Relay { + pub fn init(allocator: std.mem.Allocator, url: []const u8, auth_key: [32]u8, io: std.Io) Relay { return .{ .allocator = allocator, .url = url, .auth_signer = signer_mod.Signer.init(auth_key), - .client = .{ .allocator = allocator, .io = runtime.defaultIo() }, + .client = .{ .allocator = allocator, .io = io }, .next_id = 1, }; } @@ -1112,7 +1112,7 @@ test "parseCallBundleResult - success with decimal strings" { test "Relay.init sets fields correctly" { const auth_key = try hex_mod.hexToBytesFixed(32, "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"); - var relay = Relay.init(std.testing.allocator, "https://relay.flashbots.net", auth_key); + var relay = Relay.init(std.testing.allocator, "https://relay.flashbots.net", auth_key, runtime.blockingIo()); defer relay.deinit(); try std.testing.expectEqualStrings("https://relay.flashbots.net", relay.url); diff --git a/src/http_transport.zig b/src/http_transport.zig index ee04ba9..3f299ae 100644 --- a/src/http_transport.zig +++ b/src/http_transport.zig @@ -1,5 +1,4 @@ const std = @import("std"); -const runtime = @import("runtime.zig"); /// HTTP JSON-RPC transport layer. /// @@ -8,13 +7,17 @@ const runtime = @import("runtime.zig"); pub const HttpTransport = struct { url: []const u8, allocator: std.mem.Allocator, + /// The `std.Io` this transport runs on. Exposed so wrappers (for example + /// `Provider`) can inherit it as their single source of truth. + io: std.Io, client: std.http.Client, - pub fn init(allocator: std.mem.Allocator, url: []const u8) HttpTransport { + pub fn init(allocator: std.mem.Allocator, url: []const u8, io: std.Io) HttpTransport { return .{ .url = url, .allocator = allocator, - .client = .{ .allocator = allocator, .io = runtime.defaultIo() }, + .io = io, + .client = .{ .allocator = allocator, .io = io }, }; } @@ -185,12 +188,27 @@ test "buildRequestBody - eth_call with object param" { test "init and deinit" { const allocator = std.testing.allocator; - var transport = HttpTransport.init(allocator, "http://localhost:8545"); + const runtime = @import("runtime.zig"); + var transport = HttpTransport.init(allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); try std.testing.expectEqualStrings("http://localhost:8545", transport.url); } +test "init threads caller Io through to the transport" { + const allocator = std.testing.allocator; + const runtime = @import("runtime.zig"); + const io = runtime.blockingIo(); + var transport = HttpTransport.init(allocator, "http://localhost:8545", io); + defer transport.deinit(); + + // The transport exposes the exact Io it was constructed with, and forwards + // it to the underlying http.Client. + try std.testing.expectEqual(io.userdata, transport.io.userdata); + try std.testing.expectEqual(io.vtable, transport.io.vtable); + try std.testing.expectEqual(io.userdata, transport.client.io.userdata); +} + test "buildBatchBody - empty" { const allocator = std.testing.allocator; const body = try HttpTransport.buildBatchBody(allocator, &.{}); diff --git a/src/keystore.zig b/src/keystore.zig index 13fc0bf..9ec98af 100644 --- a/src/keystore.zig +++ b/src/keystore.zig @@ -140,13 +140,14 @@ pub fn encrypt( allocator: std.mem.Allocator, key: [32]u8, password: []const u8, + io: std.Io, opts: EncryptOptions, ) ![]u8 { - // Random salt and IV via the runtime Io (std.crypto.random was removed in 0.16). + // Random salt and IV via the caller's Io (std.crypto.random was removed in 0.16). var salt: [salt_len]u8 = undefined; var iv: [iv_len]u8 = undefined; - runtime.defaultIo().random(&salt); - runtime.defaultIo().random(&iv); + io.random(&salt); + io.random(&iv); var derived_key: [dklen]u8 = undefined; defer secureZero(&derived_key); @@ -182,7 +183,7 @@ pub fn encrypt( // Random UUID (v4) for the keystore id. var uuid_bytes: [16]u8 = undefined; - runtime.defaultIo().random(&uuid_bytes); + io.random(&uuid_bytes); uuid_bytes[6] = (uuid_bytes[6] & 0x0f) | 0x40; // version 4 uuid_bytes[8] = (uuid_bytes[8] & 0x3f) | 0x80; // variant 1 @@ -346,7 +347,7 @@ test "round-trip scrypt" { const allocator = testing.allocator; const key = try hex.hexToBytesFixed(32, "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d"); - const json = try encrypt(allocator, key, "testpassword", test_opts); + const json = try encrypt(allocator, key, "testpassword", runtime.blockingIo(), test_opts); defer allocator.free(json); const recovered = try decrypt(allocator, json, "testpassword"); @@ -357,7 +358,7 @@ test "round-trip pbkdf2" { const allocator = testing.allocator; const key = try hex.hexToBytesFixed(32, "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"); - const json = try encrypt(allocator, key, "hunter2", .{ .kdf = .pbkdf2, .pbkdf2_c = 4096 }); + const json = try encrypt(allocator, key, "hunter2", runtime.blockingIo(), .{ .kdf = .pbkdf2, .pbkdf2_c = 4096 }); defer allocator.free(json); const recovered = try decrypt(allocator, json, "hunter2"); @@ -373,7 +374,7 @@ test "round-trip multiple keys" { }; for (keys) |kh| { const key = try hex.hexToBytesFixed(32, kh); - const json = try encrypt(allocator, key, "pw", test_opts); + const json = try encrypt(allocator, key, "pw", runtime.blockingIo(), test_opts); defer allocator.free(json); const recovered = try decrypt(allocator, json, "pw"); try testing.expectEqualSlices(u8, &key, &recovered); @@ -384,7 +385,7 @@ test "wrong password returns InvalidPassword" { const allocator = testing.allocator; const key = try hex.hexToBytesFixed(32, "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d"); - const json = try encrypt(allocator, key, "correct horse", test_opts); + const json = try encrypt(allocator, key, "correct horse", runtime.blockingIo(), test_opts); defer allocator.free(json); try testing.expectError(KeystoreError.InvalidPassword, decrypt(allocator, json, "wrong horse")); diff --git a/src/mev_share.zig b/src/mev_share.zig index 483bf5b..7c3a3f9 100644 --- a/src/mev_share.zig +++ b/src/mev_share.zig @@ -357,18 +357,19 @@ pub const MevShareClient = struct { relay_url: []const u8, stream_url: []const u8, auth_key: [32]u8, + io: std.Io, ) MevShareClient { return .{ .allocator = allocator, - .relay = flashbots.Relay.init(allocator, relay_url, auth_key), + .relay = flashbots.Relay.init(allocator, relay_url, auth_key, io), .stream_url = stream_url, - .client = .{ .allocator = allocator, .io = runtime.defaultIo() }, + .client = .{ .allocator = allocator, .io = io }, }; } /// Convenience constructor for Ethereum mainnet. - pub fn initMainnet(allocator: std.mem.Allocator, auth_key: [32]u8) MevShareClient { - return init(allocator, mainnet_relay_url, mainnet_stream_url, auth_key); + pub fn initMainnet(allocator: std.mem.Allocator, auth_key: [32]u8, io: std.Io) MevShareClient { + return init(allocator, mainnet_relay_url, mainnet_stream_url, auth_key, io); } pub fn deinit(self: *MevShareClient) void { @@ -1093,7 +1094,7 @@ test "parseEventHistoryResponse - malformed" { test "MevShareClient.init sets endpoints" { const auth_key = try hex_mod.hexToBytesFixed(32, "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"); - var client = MevShareClient.initMainnet(std.testing.allocator, auth_key); + var client = MevShareClient.initMainnet(std.testing.allocator, auth_key, runtime.blockingIo()); defer client.deinit(); try std.testing.expectEqualStrings(mainnet_relay_url, client.relay.url); diff --git a/src/mnemonic.zig b/src/mnemonic.zig index 540d6ed..bce6615 100644 --- a/src/mnemonic.zig +++ b/src/mnemonic.zig @@ -42,10 +42,10 @@ pub const ValidEntropySize = enum(u8) { } }; -/// Generate a random mnemonic phrase. -pub fn generate(comptime entropy_size: ValidEntropySize) [entropy_size.wordCount()][]const u8 { +/// Generate a random mnemonic phrase, drawing entropy from `io`. +pub fn generate(io: std.Io, comptime entropy_size: ValidEntropySize) [entropy_size.wordCount()][]const u8 { var entropy: [@intFromEnum(entropy_size)]u8 = undefined; - runtime.defaultIo().random(&entropy); + io.random(&entropy); return entropyToMnemonic(entropy_size, &entropy); } @@ -294,7 +294,7 @@ test "validate rejects bad checksum" { } test "generate produces a valid mnemonic" { - const words = generate(.@"128"); + const words = generate(runtime.blockingIo(), .@"128"); try std.testing.expectEqual(@as(usize, 12), words.len); try validate(&words); } diff --git a/src/multicall.zig b/src/multicall.zig index 02a3ca2..dbfc6ac 100644 --- a/src/multicall.zig +++ b/src/multicall.zig @@ -1,6 +1,7 @@ const std = @import("std"); const provider_mod = @import("provider.zig"); const http_transport_mod = @import("http_transport.zig"); +const runtime = @import("runtime.zig"); const abi_encode = @import("abi_encode.zig"); const uint256_mod = @import("uint256.zig"); @@ -268,7 +269,7 @@ fn readWord(data: []const u8) usize { test "Multicall.init and deinit" { const allocator = std.testing.allocator; - var transport = http_transport_mod.HttpTransport.init(allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = provider_mod.Provider.init(allocator, &transport); const multicall_addr = @as([20]u8, @splat(0xca)); @@ -282,7 +283,7 @@ test "Multicall.init and deinit" { test "Multicall.addCall adds calls" { const allocator = std.testing.allocator; - var transport = http_transport_mod.HttpTransport.init(allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = provider_mod.Provider.init(allocator, &transport); @@ -306,7 +307,7 @@ test "Multicall.addCall adds calls" { test "Multicall.reset clears calls" { const allocator = std.testing.allocator; - var transport = http_transport_mod.HttpTransport.init(allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = provider_mod.Provider.init(allocator, &transport); @@ -323,7 +324,7 @@ test "Multicall.reset clears calls" { test "Multicall.encodeAggregate3 produces valid ABI encoding" { const allocator = std.testing.allocator; - var transport = http_transport_mod.HttpTransport.init(allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = provider_mod.Provider.init(allocator, &transport); @@ -354,7 +355,7 @@ test "Multicall.encodeAggregate3 produces valid ABI encoding" { test "Multicall.encodeAggregate3 with multiple calls" { const allocator = std.testing.allocator; - var transport = http_transport_mod.HttpTransport.init(allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = provider_mod.Provider.init(allocator, &transport); @@ -384,7 +385,7 @@ test "Multicall.encodeAggregate3 with multiple calls" { test "Multicall.encodeAggregate3 with empty calldata" { const allocator = std.testing.allocator; - var transport = http_transport_mod.HttpTransport.init(allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = provider_mod.Provider.init(allocator, &transport); @@ -486,7 +487,7 @@ test "AGGREGATE3_SELECTOR is correct" { test "Multicall.encodeAggregate3 address encoding" { const allocator = std.testing.allocator; - var transport = http_transport_mod.HttpTransport.init(allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = provider_mod.Provider.init(allocator, &transport); diff --git a/src/nonce_manager.zig b/src/nonce_manager.zig index d105871..4620a6a 100644 --- a/src/nonce_manager.zig +++ b/src/nonce_manager.zig @@ -72,7 +72,7 @@ pub const NonceManager = struct { fn ensureSeeded(self: *NonceManager) !void { if (self.seeded.load(.acquire)) return; - const io = runtime.defaultIo(); + const io = self.provider.io(); self.seed_mutex.lockUncancelable(io); defer self.seed_mutex.unlock(io); @@ -113,7 +113,7 @@ pub const NonceManager = struct { /// expected to land, otherwise an in-flight transaction may collide with /// a re-issued nonce. pub fn resync(self: *NonceManager) !u64 { - const io = runtime.defaultIo(); + const io = self.provider.io(); self.seed_mutex.lockUncancelable(io); defer self.seed_mutex.unlock(io); @@ -178,7 +178,7 @@ fn seededManager(provider: *provider_mod.Provider, base: u64) NonceManager { } test "next() returns increasing values from a seeded base" { - var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = provider_mod.Provider.init(std.testing.allocator, &transport); @@ -190,7 +190,7 @@ test "next() returns increasing values from a seeded base" { } test "peek does not advance" { - var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = provider_mod.Provider.init(std.testing.allocator, &transport); @@ -202,7 +202,7 @@ test "peek does not advance" { } test "peek returns UNSEEDED before seeding" { - var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = provider_mod.Provider.init(std.testing.allocator, &transport); @@ -211,7 +211,7 @@ test "peek returns UNSEEDED before seeding" { } test "onFailure(next-1) makes the next call reuse that nonce" { - var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = provider_mod.Provider.init(std.testing.allocator, &transport); @@ -227,7 +227,7 @@ test "onFailure(next-1) makes the next call reuse that nonce" { } test "onFailure(other) is a no-op" { - var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = provider_mod.Provider.init(std.testing.allocator, &transport); @@ -251,7 +251,7 @@ test "onFailure(other) is a no-op" { } test "onFailure before seeding is a no-op" { - var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = provider_mod.Provider.init(std.testing.allocator, &transport); @@ -260,7 +260,7 @@ test "onFailure before seeding is a no-op" { } test "onFailure(UNSEEDED) does not overflow and is a no-op" { - var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = provider_mod.Provider.init(std.testing.allocator, &transport); @@ -271,7 +271,7 @@ test "onFailure(UNSEEDED) does not overflow and is a no-op" { } test "concurrent next() yields a contiguous, duplicate-free nonce set" { - var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = provider_mod.Provider.init(std.testing.allocator, &transport); diff --git a/src/provider.zig b/src/provider.zig index c51bb8f..af3aaa3 100644 --- a/src/provider.zig +++ b/src/provider.zig @@ -8,6 +8,7 @@ const block_mod = @import("block.zig"); const state_overrides_mod = @import("state_overrides.zig"); const rpc_transaction_mod = @import("rpc_transaction.zig"); const HttpTransport = @import("http_transport.zig").HttpTransport; +const runtime = @import("runtime.zig"); /// Read-only Ethereum JSON-RPC provider. /// @@ -26,6 +27,11 @@ pub const Provider = struct { }; } + /// The `std.Io` this provider runs on, inherited from its transport. + pub fn io(self: *const Provider) std.Io { + return self.transport.io; + } + // ======================================================================== // Chain state // ======================================================================== @@ -1253,7 +1259,7 @@ test "formatLogFilter - with topics" { test "Provider.formatAddressAndBlock" { const allocator = std.testing.allocator; - var transport = HttpTransport.init(allocator, "http://localhost:8545"); + var transport = HttpTransport.init(allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = Provider.init(allocator, &transport); @@ -1269,7 +1275,7 @@ test "Provider.formatAddressAndBlock" { test "Provider.formatCallParams - without from" { const allocator = std.testing.allocator; - var transport = HttpTransport.init(allocator, "http://localhost:8545"); + var transport = HttpTransport.init(allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = Provider.init(allocator, &transport); @@ -1286,7 +1292,7 @@ test "Provider.formatCallParams - without from" { test "Provider.formatCallParams - with from" { const allocator = std.testing.allocator; - var transport = HttpTransport.init(allocator, "http://localhost:8545"); + var transport = HttpTransport.init(allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = Provider.init(allocator, &transport); @@ -1303,7 +1309,7 @@ test "Provider.formatCallParams - with from" { test "Provider.formatCallParamsWithOverrides - balance override" { const allocator = std.testing.allocator; - var transport = HttpTransport.init(allocator, "http://localhost:8545"); + var transport = HttpTransport.init(allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = Provider.init(allocator, &transport); @@ -1328,7 +1334,7 @@ test "Provider.formatCallParamsWithOverrides - balance override" { test "Provider.formatCallParamsWithOverrides - empty overrides emits {}" { const allocator = std.testing.allocator; - var transport = HttpTransport.init(allocator, "http://localhost:8545"); + var transport = HttpTransport.init(allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = Provider.init(allocator, &transport); @@ -1769,7 +1775,7 @@ test "parseLogsResponse - single log" { test "Provider.init" { const allocator = std.testing.allocator; - var transport = HttpTransport.init(allocator, "http://localhost:8545"); + var transport = HttpTransport.init(allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); const provider = Provider.init(allocator, &transport); @@ -1778,7 +1784,7 @@ test "Provider.init" { test "BatchCaller.init and deinit" { const allocator = std.testing.allocator; - var transport = HttpTransport.init(allocator, "http://localhost:8545"); + var transport = HttpTransport.init(allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var prov = Provider.init(allocator, &transport); var batch = BatchCaller.init(allocator, &prov); @@ -1788,7 +1794,7 @@ test "BatchCaller.init and deinit" { test "BatchCaller.addCall accumulates" { const allocator = std.testing.allocator; - var transport = HttpTransport.init(allocator, "http://localhost:8545"); + var transport = HttpTransport.init(allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var prov = Provider.init(allocator, &transport); var batch = BatchCaller.init(allocator, &prov); @@ -1804,7 +1810,7 @@ test "BatchCaller.addCall accumulates" { test "BatchCaller.reset clears" { const allocator = std.testing.allocator; - var transport = HttpTransport.init(allocator, "http://localhost:8545"); + var transport = HttpTransport.init(allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var prov = Provider.init(allocator, &transport); var batch = BatchCaller.init(allocator, &prov); diff --git a/src/retry_provider.zig b/src/retry_provider.zig index f9702b4..8ba4741 100644 --- a/src/retry_provider.zig +++ b/src/retry_provider.zig @@ -47,7 +47,7 @@ pub const RetryingProvider = struct { /// Seeds the PRNG from a cryptographically random value. pub fn init(inner: *Provider, opts: RetryOpts) RetryingProvider { var seed_bytes: [8]u8 = undefined; - runtime.defaultIo().random(&seed_bytes); + inner.io().random(&seed_bytes); const seed = std.mem.readInt(u64, &seed_bytes, .little); return .{ .inner = inner, @@ -222,7 +222,7 @@ const RetryState = struct { const jitter_ms: u64 = @intFromFloat(@as(f64, @floatFromInt(self.backoff_ms)) * self.provider.opts.jitter); const extra = if (jitter_ms > 0) self.provider.prng.random().int(u64) % jitter_ms else 0; - runtime.sleepMs(self.backoff_ms + extra); + runtime.sleepMs(self.provider.inner.io(), self.backoff_ms + extra); const next_backoff: u64 = @intFromFloat( @as(f64, @floatFromInt(self.backoff_ms)) * self.provider.opts.backoff_multiplier, @@ -256,8 +256,11 @@ fn isRetryable(err: anyerror, opts: RetryOpts) bool { // ============================================================================ test "RetryingProvider.init seeds the jitter PRNG" { - var fake_inner: Provider = undefined; - var rp = RetryingProvider.init(&fake_inner, .{}); + const HttpTransport = @import("http_transport.zig").HttpTransport; + var transport = HttpTransport.init(std.testing.allocator, "http://localhost:8545", runtime.blockingIo()); + defer transport.deinit(); + var inner = Provider.init(std.testing.allocator, &transport); + var rp = RetryingProvider.init(&inner, .{}); try std.testing.expectEqual(@as(u32, 3), rp.opts.max_attempts); // The PRNG must be usable. _ = rp.prng.random().int(u64); @@ -295,7 +298,10 @@ test "RetryState - exhausts attempts then stops" { // We can't call into a real Provider here; we test RetryState directly. const opts = RetryOpts{ .max_attempts = 2, .initial_backoff_ms = 0, .jitter = 0 }; const seed: u64 = 0; - var fake_inner: Provider = undefined; + const HttpTransport = @import("http_transport.zig").HttpTransport; + var transport = HttpTransport.init(std.testing.allocator, "http://localhost:8545", runtime.blockingIo()); + defer transport.deinit(); + var fake_inner = Provider.init(std.testing.allocator, &transport); var rp = RetryingProvider{ .inner = &fake_inner, .opts = opts, @@ -312,7 +318,10 @@ test "RetryState - exhausts attempts then stops" { test "RetryState - non-retryable error stops immediately" { const opts = RetryOpts{ .max_attempts = 5, .initial_backoff_ms = 0, .jitter = 0 }; - var fake_inner: Provider = undefined; + const HttpTransport = @import("http_transport.zig").HttpTransport; + var transport = HttpTransport.init(std.testing.allocator, "http://localhost:8545", runtime.blockingIo()); + defer transport.deinit(); + var fake_inner = Provider.init(std.testing.allocator, &transport); var rp = RetryingProvider{ .inner = &fake_inner, .opts = opts, @@ -333,7 +342,10 @@ test "RetryState - backoff increases exponentially" { .jitter = 0, // no sleep .retryable = .connection_errors, }; - var fake_inner: Provider = undefined; + const HttpTransport = @import("http_transport.zig").HttpTransport; + var transport = HttpTransport.init(std.testing.allocator, "http://localhost:8545", runtime.blockingIo()); + defer transport.deinit(); + var fake_inner = Provider.init(std.testing.allocator, &transport); var rp = RetryingProvider{ .inner = &fake_inner, .opts = opts, diff --git a/src/runtime.zig b/src/runtime.zig index 76a3ebc..94380cb 100644 --- a/src/runtime.zig +++ b/src/runtime.zig @@ -1,47 +1,45 @@ const std = @import("std"); -/// Default `std.Io` runtime used by the library. +/// A convenient blocking `std.Io` value callers can pass explicitly. /// -/// Zig 0.16 moved networking, HTTP, sleeping, and clocks behind the `std.Io` -/// interface. To keep the public eth.zig API unchanged (no `Io` parameter on -/// transports), the library constructs a default blocking implementation -/// internally. All operations the library performs through this `Io` are -/// synchronous and run on the calling thread, matching the pre-0.16 behavior -/// of the blocking std.net / std.http APIs. +/// Zig 0.16 moved networking, HTTP, sleeping, clocks, and randomness behind the +/// `std.Io` interface. This library never reaches for an `Io` implicitly: every +/// network entry point and every operation needing time/randomness takes an +/// `io: std.Io` from the caller. `blockingIo()` exists purely as a convenience +/// so callers who do not run their own event loop can write, for example, +/// `HttpTransport.init(allocator, url, eth.runtime.blockingIo())`. /// -/// Callers that need a custom event loop should drive the library from their -/// own threads; the transports remain blocking by design. -pub fn defaultIo() std.Io { - // `global_single_threaded` performs every operation synchronously on the - // calling thread and never allocates, spawns threads, or requires deinit. +/// The returned `Io` performs every operation synchronously on the calling +/// thread and never allocates, spawns threads, or requires deinit. +pub fn blockingIo() std.Io { return std.Io.Threaded.global_single_threaded.io(); } -/// Wall-clock time in milliseconds since the Unix epoch. +/// Wall-clock time in milliseconds since the Unix epoch, read through `io`. /// /// Replacement for `std.time.milliTimestamp()`, which was removed in /// Zig 0.16. Deadline-style APIs in this library (for example /// `WsTransport.readMessageDeadline`) use this time base. -pub fn milliTimestamp() i64 { - return std.Io.Clock.now(.real, defaultIo()).toMilliseconds(); +pub fn milliTimestamp(io: std.Io) i64 { + return std.Io.Clock.now(.real, io).toMilliseconds(); } -/// Blocking sleep for `ms` milliseconds. +/// Blocking sleep for `ms` milliseconds, driven by `io`. /// /// Replacement for `std.Thread.sleep(ms * std.time.ns_per_ms)`, which was /// removed in Zig 0.16. -pub fn sleepMs(ms: u64) void { +pub fn sleepMs(io: std.Io, ms: u64) void { const capped_ms: i64 = @intCast(@min(ms, std.math.maxInt(i64))); - defaultIo().sleep(.fromMilliseconds(capped_ms), .awake) catch {}; + io.sleep(.fromMilliseconds(capped_ms), .awake) catch {}; } test "milliTimestamp returns a plausible value" { - const ms = milliTimestamp(); + const ms = milliTimestamp(blockingIo()); // After 2020-01-01 and before 2100-01-01. try std.testing.expect(ms > 1_577_836_800_000); try std.testing.expect(ms < 4_102_444_800_000); } test "sleepMs zero returns immediately" { - sleepMs(0); + sleepMs(blockingIo(), 0); } diff --git a/src/sse_transport.zig b/src/sse_transport.zig index 210b11f..615bdb6 100644 --- a/src/sse_transport.zig +++ b/src/sse_transport.zig @@ -194,11 +194,12 @@ pub const SseParser = struct { pub fn subscribe( allocator: std.mem.Allocator, url: []const u8, + io: std.Io, extra_headers: []const std.http.Header, parser: *SseParser, callback: *const fn (event: SseEvent) void, ) !void { - var client = std.http.Client{ .allocator = allocator, .io = runtime.defaultIo() }; + var client = std.http.Client{ .allocator = allocator, .io = io }; defer client.deinit(); const uri = try std.Uri.parse(url); @@ -281,6 +282,7 @@ pub const ReconnectOpts = struct { pub fn subscribeWithReconnect( allocator: std.mem.Allocator, url: []const u8, + io: std.Io, extra_headers: []const std.http.Header, opts: ReconnectOpts, callback: *const fn (event: SseEvent) void, @@ -291,7 +293,7 @@ pub fn subscribeWithReconnect( var backoff_ms = opts.initial_backoff_ms; while (true) { - if (subscribe(allocator, url, extra_headers, &parser, callback)) |_| { + if (subscribe(allocator, url, io, extra_headers, &parser, callback)) |_| { // Clean close -- reset backoff. backoff_ms = opts.initial_backoff_ms; } else |_| {} @@ -299,7 +301,7 @@ pub fn subscribeWithReconnect( // Use server-specified retry delay if available, otherwise exponential backoff. const delay = parser.retry_ms orelse backoff_ms; if (opts.on_reconnect) |cb| cb(delay); - runtime.sleepMs(delay); + runtime.sleepMs(io, delay); // Only advance exponential backoff when server hasn't specified retry. if (parser.retry_ms == null) { diff --git a/src/wallet.zig b/src/wallet.zig index e56ad8e..1d0e069 100644 --- a/src/wallet.zig +++ b/src/wallet.zig @@ -166,7 +166,7 @@ pub const Wallet = struct { if (try self.provider.getTransactionReceipt(tx_hash)) |receipt| { return receipt; } - runtime.sleepMs(1_000); // 1 second + runtime.sleepMs(self.provider.io(), 1_000); // 1 second } return null; } @@ -195,7 +195,7 @@ test "Wallet.init sets fields correctly" { const hex = @import("hex.zig"); const private_key = try hex.hexToBytesFixed(32, "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"); - var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = provider_mod.Provider.init(std.testing.allocator, &transport); var wallet = Wallet.init(std.testing.allocator, private_key, &provider); @@ -214,7 +214,7 @@ test "Wallet accepts an optional nonce manager without breaking init" { const hex = @import("hex.zig"); const private_key = try hex.hexToBytesFixed(32, "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"); - var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = provider_mod.Provider.init(std.testing.allocator, &transport); var wallet = Wallet.init(std.testing.allocator, private_key, &provider); @@ -228,7 +228,7 @@ test "Wallet.signTransaction produces valid signed bytes" { const hex = @import("hex.zig"); const private_key = try hex.hexToBytesFixed(32, "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"); - var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = provider_mod.Provider.init(std.testing.allocator, &transport); var wallet = Wallet.init(std.testing.allocator, private_key, &provider); @@ -259,7 +259,7 @@ test "Wallet.signTransaction is deterministic" { const hex = @import("hex.zig"); const private_key = try hex.hexToBytesFixed(32, "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"); - var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545"); + var transport = http_transport_mod.HttpTransport.init(std.testing.allocator, "http://localhost:8545", runtime.blockingIo()); defer transport.deinit(); var provider = provider_mod.Provider.init(std.testing.allocator, &transport); var wallet = Wallet.init(std.testing.allocator, private_key, &provider); diff --git a/src/ws_client.zig b/src/ws_client.zig index 27df28b..b56ec38 100644 --- a/src/ws_client.zig +++ b/src/ws_client.zig @@ -94,6 +94,9 @@ pub const Subscription = struct { pub const WsClient = struct { allocator: std.mem.Allocator, + /// The `std.Io` this client runs on. Used for every connect/reconnect, + /// the keepalive timestamps, and the ping-nonce randomness. + io: std.Io, /// Owned copy of the URL used for reconnects. url: []u8, transport: ?WsTransport, @@ -105,7 +108,7 @@ pub const WsClient = struct { next_id: u64, opts: Opts, state: State, - /// `runtime.milliTimestamp()` of the last received frame. + /// `runtime.milliTimestamp(io)` of the last received frame. last_activity_ms: i64, /// Sentinel for an in-flight ping. pending_pong: bool, @@ -117,17 +120,18 @@ pub const WsClient = struct { /// Open a resilient WebSocket client. /// `connect()` itself is NOT retried; if the very first connect fails, /// the call returns `error.ConnectionFailed`. - pub fn connect(allocator: std.mem.Allocator, url: []const u8, opts: Opts) !*WsClient { + pub fn connect(allocator: std.mem.Allocator, url: []const u8, io: std.Io, opts: Opts) !*WsClient { const self = try allocator.create(WsClient); errdefer allocator.destroy(self); const url_copy = try allocator.dupe(u8, url); errdefer allocator.free(url_copy); - const transport = WsTransport.connect(allocator, url) catch return error.ConnectionFailed; + const transport = WsTransport.connect(allocator, url, io) catch return error.ConnectionFailed; self.* = .{ .allocator = allocator, + .io = io, .url = url_copy, .transport = transport, .subs = .empty, @@ -136,10 +140,10 @@ pub const WsClient = struct { .next_id = 1, .opts = opts, .state = .connected, - .last_activity_ms = runtime.milliTimestamp(), + .last_activity_ms = runtime.milliTimestamp(io), .pending_pong = false, .ping_sent_ms = 0, - .rng = std.Random.DefaultPrng.init(@bitCast(runtime.milliTimestamp())), + .rng = std.Random.DefaultPrng.init(@bitCast(runtime.milliTimestamp(io))), }; return self; } @@ -379,7 +383,7 @@ pub const WsClient = struct { const t = &self.transport.?; const frames_before = t.frames_received; - const now = runtime.milliTimestamp(); + const now = runtime.milliTimestamp(self.io); // Pong-timeout check. if (self.pending_pong and now - self.ping_sent_ms >= @as(i64, @intCast(self.opts.pong_timeout_ms))) { @@ -391,7 +395,7 @@ pub const WsClient = struct { const elapsed_idle = now - self.last_activity_ms; if (elapsed_idle >= @as(i64, @intCast(self.opts.ping_interval_ms))) { var nonce: [8]u8 = undefined; - runtime.defaultIo().random(&nonce); + self.io.random(&nonce); t.sendPing(&nonce) catch return error.WriteError; self.ping_sent_ms = now; self.pending_pong = true; @@ -408,7 +412,7 @@ pub const WsClient = struct { const maybe_frame = t.readMessageDeadline(deadline_ms) catch return error.ReadError; if (maybe_frame) |frame| { - self.last_activity_ms = runtime.milliTimestamp(); + self.last_activity_ms = runtime.milliTimestamp(self.io); self.pending_pong = false; return frame; } @@ -416,7 +420,7 @@ pub const WsClient = struct { // we were waiting for a data frame, we are still alive. Otherwise // loop, which will either send a ping or trip pong-timeout. if (t.frames_received != frames_before) { - self.last_activity_ms = runtime.milliTimestamp(); + self.last_activity_ms = runtime.milliTimestamp(self.io); self.pending_pong = false; } } @@ -473,15 +477,15 @@ pub const WsClient = struct { const delay = applyJitter(base, self.opts.jitter_pct, self.rng.random()); if (self.opts.on_reconnect) |cb| cb(attempt, delay); - runtime.sleepMs(delay); + runtime.sleepMs(self.io, delay); - const maybe_t = WsTransport.connect(self.allocator, self.url); + const maybe_t = WsTransport.connect(self.allocator, self.url, self.io); if (maybe_t) |t_val| { var t = t_val; if (self.resubscribeAll(&t)) |_| { self.transport = t; self.state = .connected; - self.last_activity_ms = runtime.milliTimestamp(); + self.last_activity_ms = runtime.milliTimestamp(self.io); return; } else |_| { t.close(); @@ -703,6 +707,7 @@ test "Opts defaults" { fn testStubClient(allocator: std.mem.Allocator) WsClient { return .{ .allocator = allocator, + .io = runtime.blockingIo(), .url = &.{}, .transport = null, .subs = .empty, diff --git a/src/ws_transport.zig b/src/ws_transport.zig index 7d86310..f1cfa83 100644 --- a/src/ws_transport.zig +++ b/src/ws_transport.zig @@ -293,7 +293,7 @@ pub fn validateHandshakeResponse(response: []const u8, expected_accept: []const /// for the TLS client's reader/writer interfaces. /// /// Usage: -/// var transport = try WsTransport.connect(allocator, "ws://localhost:8545"); +/// var transport = try WsTransport.connect(allocator, "ws://localhost:8545", io); /// defer transport.close(); /// const response = try transport.request("eth_blockNumber", "[]"); /// defer allocator.free(response); @@ -363,9 +363,8 @@ pub const WsTransport = struct { /// /// Parses the URL, opens a TCP connection, optionally wraps it in TLS, /// and performs the WebSocket upgrade handshake. - pub fn connect(allocator: std.mem.Allocator, url: []const u8) TransportError!WsTransport { + pub fn connect(allocator: std.mem.Allocator, url: []const u8, io: std.Io) TransportError!WsTransport { const parsed = parseUrl(url) catch return error.ConnectionFailed; - const io = runtime.defaultIo(); // Open TCP connection. Try the host as an IP literal first, then // fall back to DNS resolution. @@ -721,7 +720,7 @@ pub const WsTransport = struct { } if (deadline_ms) |dl| { - const now = runtime.milliTimestamp(); + const now = runtime.milliTimestamp(self.io); if (now >= dl) return false; const wait_i64 = dl - now; const wait_ms: i32 = if (wait_i64 > std.math.maxInt(i32)) @@ -809,13 +808,14 @@ pub const ReconnectOpts = struct { pub fn connectWithReconnect( allocator: std.mem.Allocator, url: []const u8, + io: std.Io, opts: ReconnectOpts, callback: *const fn (transport: *WsTransport) anyerror!void, ) void { var backoff_ms = opts.initial_backoff_ms; while (true) { - if (WsTransport.connect(allocator, url)) |transport_val| { + if (WsTransport.connect(allocator, url, io)) |transport_val| { var transport = transport_val; defer transport.close(); if (callback(&transport)) |_| { @@ -825,7 +825,7 @@ pub fn connectWithReconnect( } else |_| {} if (opts.on_reconnect) |cb| cb(backoff_ms); - runtime.sleepMs(backoff_ms); + runtime.sleepMs(io, backoff_ms); // Guard against overflow before clamping. backoff_ms = if (backoff_ms > opts.max_backoff_ms / 2) diff --git a/tests/integration_tests.zig b/tests/integration_tests.zig index 1c4af94..378898f 100644 --- a/tests/integration_tests.zig +++ b/tests/integration_tests.zig @@ -27,7 +27,7 @@ const ANVIL_CHAIN_ID: u64 = 31337; /// Check if Anvil is reachable by opening a TCP connection to 127.0.0.1:8545. /// This avoids going through the HTTP client which can crash on connection refused. fn isAnvilAvailable() bool { - const io = eth.runtime.defaultIo(); + const io = eth.runtime.blockingIo(); const addr = std.Io.net.IpAddress.parse(ANVIL_HOST, ANVIL_PORT) catch return false; const stream = addr.connect(io, .{ .mode = .stream }) catch return false; stream.close(io); @@ -42,7 +42,7 @@ test "getChainId returns 31337 (Anvil default)" { if (!isAnvilAvailable()) return; const allocator = std.testing.allocator; - var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL); + var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL, eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); @@ -54,7 +54,7 @@ test "getBlockNumber returns a value" { if (!isAnvilAvailable()) return; const allocator = std.testing.allocator; - var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL); + var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL, eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); @@ -71,7 +71,7 @@ test "getBalance of funded account" { if (!isAnvilAvailable()) return; const allocator = std.testing.allocator; - var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL); + var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL, eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); @@ -88,7 +88,7 @@ test "getTransactionCount of account" { if (!isAnvilAvailable()) return; const allocator = std.testing.allocator; - var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL); + var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL, eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); @@ -104,7 +104,7 @@ test "getCode of EOA returns empty" { if (!isAnvilAvailable()) return; const allocator = std.testing.allocator; - var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL); + var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL, eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); @@ -124,7 +124,7 @@ test "getGasPrice returns non-zero" { if (!isAnvilAvailable()) return; const allocator = std.testing.allocator; - var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL); + var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL, eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); @@ -136,7 +136,7 @@ test "getMaxPriorityFee returns a value" { if (!isAnvilAvailable()) return; const allocator = std.testing.allocator; - var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL); + var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL, eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); @@ -152,7 +152,7 @@ test "getBlock for block 0 returns genesis" { if (!isAnvilAvailable()) return; const allocator = std.testing.allocator; - var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL); + var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL, eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); @@ -171,7 +171,7 @@ test "getBlock for non-existent block returns null" { if (!isAnvilAvailable()) return; const allocator = std.testing.allocator; - var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL); + var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL, eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); @@ -188,7 +188,7 @@ test "Wallet.address derives correct address from private key" { if (!isAnvilAvailable()) return; const allocator = std.testing.allocator; - var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL); + var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL, eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); @@ -208,7 +208,7 @@ test "send ETH transfer and verify receipt" { if (!isAnvilAvailable()) return; const allocator = std.testing.allocator; - var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL); + var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL, eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); @@ -258,7 +258,7 @@ test "estimateGas for simple ETH transfer" { if (!isAnvilAvailable()) return; const allocator = std.testing.allocator; - var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL); + var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL, eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); @@ -275,7 +275,7 @@ test "sendTransactionAndWait returns receipt directly" { if (!isAnvilAvailable()) return; const allocator = std.testing.allocator; - var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL); + var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL, eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); @@ -298,7 +298,7 @@ test "getTransactionReceipt for unknown hash returns null" { if (!isAnvilAvailable()) return; const allocator = std.testing.allocator; - var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL); + var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL, eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); @@ -316,7 +316,7 @@ test "provider next_id increments across calls" { if (!isAnvilAvailable()) return; const allocator = std.testing.allocator; - var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL); + var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL, eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); @@ -338,7 +338,7 @@ test "callWithOverrides applies code + balance override" { if (!isAnvilAvailable()) return; const allocator = std.testing.allocator; - var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL); + var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL, eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); @@ -371,7 +371,7 @@ test "callWithOverrides applies stateDiff (storage slot)" { if (!isAnvilAvailable()) return; const allocator = std.testing.allocator; - var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL); + var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL, eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); @@ -402,7 +402,7 @@ test "callWithOverrides without an override matches plain call" { if (!isAnvilAvailable()) return; const allocator = std.testing.allocator; - var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL); + var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL, eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); @@ -425,7 +425,7 @@ const ANVIL_WS_URL = "ws://127.0.0.1:8545"; /// Trigger a block on Anvil so newHeads subscriptions emit a notification. fn anvilMineOne(allocator: std.mem.Allocator) !void { - var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL); + var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL, eth.runtime.blockingIo()); defer transport.deinit(); const response = try transport.request("evm_mine", "[]", 1); allocator.free(response); @@ -438,7 +438,7 @@ test "WsClient subscribe newHeads receives a fresh block" { // Disable keepalive in tests so the test's run time is bounded by // mining + dispatch, not by the default 30s ping interval. const opts = eth.ws_client.Opts{ .ping_interval_ms = 0 }; - const client = try eth.ws_client.WsClient.connect(allocator, ANVIL_WS_URL, opts); + const client = try eth.ws_client.WsClient.connect(allocator, ANVIL_WS_URL, eth.runtime.blockingIo(), opts); defer client.deinit(); const sub = try client.subscribe(.{ .new_heads = {} }); @@ -458,7 +458,7 @@ test "WsClient multiplexes two subscriptions on one connection" { const allocator = std.testing.allocator; const opts = eth.ws_client.Opts{ .ping_interval_ms = 0 }; - const client = try eth.ws_client.WsClient.connect(allocator, ANVIL_WS_URL, opts); + const client = try eth.ws_client.WsClient.connect(allocator, ANVIL_WS_URL, eth.runtime.blockingIo(), opts); defer client.deinit(); const sub_a = try client.subscribe(.{ .new_heads = {} }); @@ -488,7 +488,7 @@ test "WsClient unsubscribe frees handle and removes from registry" { const allocator = std.testing.allocator; const opts = eth.ws_client.Opts{ .ping_interval_ms = 0 }; - const client = try eth.ws_client.WsClient.connect(allocator, ANVIL_WS_URL, opts); + const client = try eth.ws_client.WsClient.connect(allocator, ANVIL_WS_URL, eth.runtime.blockingIo(), opts); defer client.deinit(); const sub = try client.subscribe(.{ .new_heads = {} }); @@ -504,14 +504,14 @@ test "WsClient subscribe pending full streams an RpcTransaction" { // Subscribe on a fresh WsClient before sending the tx so we don't miss // the notification. const opts = eth.ws_client.Opts{ .ping_interval_ms = 0 }; - const client = try eth.ws_client.WsClient.connect(allocator, ANVIL_WS_URL, opts); + const client = try eth.ws_client.WsClient.connect(allocator, ANVIL_WS_URL, eth.runtime.blockingIo(), opts); defer client.deinit(); const sub = try client.subscribe(.{ .new_pending_transactions = .{ .full = true } }); // Send a transaction via the HTTP wallet so the WsClient is purely a // reader. Account #0 -> account #1, 0.001 ETH. - var http = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL); + var http = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL, eth.runtime.blockingIo()); defer http.deinit(); var provider = eth.provider.Provider.init(allocator, &http); const private_key = try eth.hex.hexToBytesFixed(32, ACCOUNT_0_KEY_HEX); @@ -571,11 +571,11 @@ test "LogWatcher pollOnce tracks mined blocks" { if (!isAnvilAvailable()) return; const allocator = std.testing.allocator; - var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL); + var transport = eth.http_transport.HttpTransport.init(allocator, ANVIL_URL, eth.runtime.blockingIo()); defer transport.deinit(); var provider = eth.provider.Provider.init(allocator, &transport); - const client = try eth.ws_client.WsClient.connect(allocator, ANVIL_WS_URL, .{ .ping_interval_ms = 0 }); + const client = try eth.ws_client.WsClient.connect(allocator, ANVIL_WS_URL, eth.runtime.blockingIo(), .{ .ping_interval_ms = 0 }); defer client.deinit(); var watcher = try eth.log_watcher.LogWatcher.init(allocator, &provider, client, .{}, .{});