Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,25 @@ const name = try token.name();
defer allocator.free(name);
```

### Simulate eth_call with state overrides

Lets searchers answer "what if?" questions without forking a node: what if this token balance were larger, what if this storage slot held a different value, what if this address ran alternative bytecode? Maps to the geth-style third argument to `eth_call`.

```zig
const eth = @import("eth");

var overrides = eth.state_overrides.StateOverrides.init(allocator);
defer overrides.deinit();

// Pretend this account is rich, has nonce 0, and the pool has a hand-tuned reserve.
try overrides.setBalance(searcher_addr, 1_000_000 * std.math.pow(u256, 10, 18));
try overrides.setNonce(searcher_addr, 0);
try overrides.setStorageAt(pool_addr, reserves_slot, new_reserves_word);

const result = try provider.callWithOverrides(target_contract, calldata, &overrides);
defer allocator.free(result);
```

### Function selectors and event topics

```zig
Expand Down
110 changes: 110 additions & 0 deletions src/provider.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const uint256_mod = @import("uint256.zig");
const primitives = @import("primitives.zig");
const receipt_mod = @import("receipt.zig");
const block_mod = @import("block.zig");
const state_overrides_mod = @import("state_overrides.zig");
const HttpTransport = @import("http_transport.zig").HttpTransport;

/// Read-only Ethereum JSON-RPC provider.
Expand Down Expand Up @@ -158,6 +159,33 @@ pub const Provider = struct {
return parseHexBytes(self.allocator, result_str);
}

/// Executes a message call (eth_call) against the latest block with
/// state overrides applied. Lets simulators answer "what if?" questions
/// (modified balances, code, storage) without forking a node.
///
/// Maps to the third parameter of the geth-style eth_call:
/// `eth_call(transaction, blockTag, stateOverrideObject)`.
/// Most production providers (Alchemy, Infura, QuickNode, Anvil)
/// accept this third argument.
///
/// Caller owns the returned memory.
pub fn callWithOverrides(
self: *Provider,
to: [20]u8,
data: []const u8,
overrides: *const state_overrides_mod.StateOverrides,
) ![]u8 {
const params = try self.formatCallParamsWithOverrides(to, data, null, overrides);
defer self.allocator.free(params);

const raw = try self.rpcCall(json_rpc.Method.eth_call, params);
defer self.allocator.free(raw);

const result_str = try extractResultString(self.allocator, raw);
defer self.allocator.free(result_str);
return parseHexBytes(self.allocator, result_str);
}

/// Estimates the gas needed to execute the given transaction.
pub fn estimateGas(self: *Provider, to: [20]u8, data: []const u8, from: ?[20]u8) !u64 {
const params = try self.formatCallParams(to, data, from);
Expand Down Expand Up @@ -305,6 +333,44 @@ pub const Provider = struct {

return buf.toOwnedSlice(self.allocator);
}

/// Like `formatCallParams`, but emits the third state-override
/// argument so eth_call can be invoked with simulated state.
pub fn formatCallParamsWithOverrides(
self: *Provider,
to: [20]u8,
data: []const u8,
from: ?[20]u8,
overrides: *const state_overrides_mod.StateOverrides,
) ![]u8 {
const to_hex = primitives.addressToHex(&to);
const data_hex = try hex_mod.bytesToHex(self.allocator, data);
defer self.allocator.free(data_hex);

const overrides_json = try overrides.serializeJson(self.allocator);
defer self.allocator.free(overrides_json);

var buf: std.ArrayList(u8) = .empty;
errdefer buf.deinit(self.allocator);
try buf.appendSlice(self.allocator, "[{");

if (from) |f| {
const from_hex = primitives.addressToHex(&f);
try buf.appendSlice(self.allocator, "\"from\":\"");
try buf.appendSlice(self.allocator, &from_hex);
try buf.appendSlice(self.allocator, "\",");
}

try buf.appendSlice(self.allocator, "\"to\":\"");
try buf.appendSlice(self.allocator, &to_hex);
try buf.appendSlice(self.allocator, "\",\"data\":\"");
try buf.appendSlice(self.allocator, data_hex);
try buf.appendSlice(self.allocator, "\"},\"latest\",");
try buf.appendSlice(self.allocator, overrides_json);
try buf.append(self.allocator, ']');

return buf.toOwnedSlice(self.allocator);
}
};

// ============================================================================
Expand Down Expand Up @@ -1110,6 +1176,50 @@ test "Provider.formatCallParams - with from" {
try std.testing.expect(std.mem.indexOf(u8, params, "\"to\":\"0xdead000000000000000000000000000000000000\"") != null);
}

test "Provider.formatCallParamsWithOverrides - balance override" {
const allocator = std.testing.allocator;
var transport = HttpTransport.init(allocator, "http://localhost:8545");
defer transport.deinit();

var provider = Provider.init(allocator, &transport);
const to = try primitives.addressFromHex("0xdead000000000000000000000000000000000000");
const data = &[_]u8{ 0x12, 0x34 };

var overrides = state_overrides_mod.StateOverrides.init(allocator);
defer overrides.deinit();
const target = try primitives.addressFromHex("0xcafe000000000000000000000000000000000000");
try overrides.setBalance(target, 0xdeadbeef);

const params = try provider.formatCallParamsWithOverrides(to, data, null, &overrides);
defer allocator.free(params);

// Shape: [{...},"latest",{...overrides...}]
try std.testing.expect(std.mem.indexOf(u8, params, "\"to\":\"0xdead000000000000000000000000000000000000\"") != null);
try std.testing.expect(std.mem.indexOf(u8, params, "\"data\":\"0x1234\"") != null);
try std.testing.expect(std.mem.indexOf(u8, params, ",\"latest\",{") != null);
try std.testing.expect(std.mem.indexOf(u8, params, "\"0xcafe000000000000000000000000000000000000\":{\"balance\":\"0xdeadbeef\"}") != null);
try std.testing.expect(std.mem.endsWith(u8, params, "}}]"));
}

test "Provider.formatCallParamsWithOverrides - empty overrides emits {}" {
const allocator = std.testing.allocator;
var transport = HttpTransport.init(allocator, "http://localhost:8545");
defer transport.deinit();

var provider = Provider.init(allocator, &transport);
const to = try primitives.addressFromHex("0xdead000000000000000000000000000000000000");
const data = &[_]u8{};

var overrides = state_overrides_mod.StateOverrides.init(allocator);
defer overrides.deinit();

const params = try provider.formatCallParamsWithOverrides(to, data, null, &overrides);
defer allocator.free(params);

// Even with no overrides, the third positional argument is present.
try std.testing.expect(std.mem.endsWith(u8, params, "\"latest\",{}]"));
}

test "parseTransactionReceipt - successful receipt" {
const allocator = std.testing.allocator;
const raw =
Expand Down
2 changes: 2 additions & 0 deletions src/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub const ws_transport = @import("ws_transport.zig");
pub const sse_transport = @import("sse_transport.zig");
pub const subscription = @import("subscription.zig");
pub const ws_client = @import("ws_client.zig");
pub const state_overrides = @import("state_overrides.zig");
pub const provider = @import("provider.zig");
pub const retry_provider = @import("retry_provider.zig");
pub const RetryingProvider = retry_provider.RetryingProvider;
Expand Down Expand Up @@ -111,6 +112,7 @@ test {
_ = @import("sse_transport.zig");
_ = @import("subscription.zig");
_ = @import("ws_client.zig");
_ = @import("state_overrides.zig");
_ = @import("provider.zig");
_ = @import("retry_provider.zig");
// Layer 7: Client
Expand Down
Loading
Loading