eth.zig

Examples

Runnable code examples for eth.zig -- address derivation, signing, ERC-20, HD wallets, and more.

The examples/ directory in the repo contains self-contained programs for each major feature.

ExampleDescriptionRequires RPC
01_derive_addressDerive address from private keyNo
02_check_balanceQuery ETH balance via JSON-RPCYes
03_sign_messageEIP-191 personal message signingNo
04_send_transactionSend ETH with WalletYes (Anvil)
05_read_erc20ERC-20 module API showcaseYes
06_hd_walletBIP-44 HD wallet derivationNo
07_comptime_selectorsComptime function selectorsNo
08_mev_share_backrunnerMEV-Share backrunner bot (SSE stream + bundle)No (dry-run)

Run any example:

cd examples && zig build && ./zig-out/bin/01_derive_address

Derive an Address

const eth = @import("eth");

// WARNING: This is the default Anvil/Hardhat test key. Never use it for real funds.
const private_key = try eth.hex.hexToBytesFixed(
    32,
    "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
);
const signer = eth.signer.Signer.init(private_key);
const addr = try signer.address();
const checksum = eth.primitives.addressToChecksum(&addr);
// "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"

Sign and Send a Transaction

const eth = @import("eth");

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);

var wallet = eth.wallet.Wallet.init(allocator, private_key, &provider);
const tx_hash = try wallet.sendTransaction(.{
    .to = recipient_address,
    .value = eth.units.parseEther(1.0),
});

Read an ERC-20 Token

const eth = @import("eth");

// Comptime selectors -- zero runtime cost
const balance_sel = eth.erc20.selectors.balanceOf;

// Or use the typed wrapper
var token = eth.erc20.ERC20.init(allocator, token_addr, &provider);
const balance = try token.balanceOf(holder_addr);
const name = try token.name();
defer allocator.free(name);

Function Selectors

const eth = @import("eth");

// Computed at compile time -- zero runtime cost
const transfer_sel = comptime eth.keccak.selector("transfer(address,uint256)");
// transfer_sel == [4]u8{ 0xa9, 0x05, 0x9c, 0xbb }

// Same function works at runtime too
const runtime_sel = eth.keccak.selector(some_signature);

const transfer_topic = comptime eth.keccak.hash("Transfer(address,address,uint256)");
// transfer_topic == keccak256("Transfer(address,address,uint256)")

HD Wallet from Mnemonic

const eth = @import("eth");

const words = [_][]const u8{
    "abandon", "abandon", "abandon", "abandon",
    "abandon", "abandon", "abandon", "abandon",
    "abandon", "abandon", "abandon", "about",
};
const seed = try eth.mnemonic.toSeed(&words, "");
const key = try eth.hd_wallet.deriveEthAccount(seed, 0);
const addr = key.toAddress();

Multicall3 Batch Reads

const eth = @import("eth");

var mc = eth.multicall.Multicall3.init(allocator, &provider);
try mc.addCall(token_addr, eth.erc20.selectors.balanceOf, .{holder_addr});
try mc.addCall(token_addr, eth.erc20.selectors.totalSupply, .{});
const results = try mc.execute();

MEV-Share Backrunner

08_mev_share_backrunner is a teaching example that ties together comptime selectors, the MEV-Share SSE stream, EIP-1559 signing, and bundle composition. It subscribes to the pending-transaction event stream, matches hints against a comptime-computed table of Uniswap swap selectors, and builds a backrun bundle (the user's tx hash followed by a signed backrun tx). It defaults to a safe DRY_RUN mode that prints the mev_sendBundle params it would submit, and points at Sepolia by default so the subscription flow can be exercised with no mainnet funds.

const eth = @import("eth");

// Comptime: the swap selector is hashed by the compiler.
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, eth.runtime.blockingIo());
defer client.deinit();

// Subscribe to the SSE stream; the callback fires per pending-tx hint.
try client.on(&onEvent);

// Inside onEvent: match the revealed selector, then compose the bundle.
const body = [_]eth.flashbots.MevBundleBody{
    .{ .hash = user_tx_hash }, // the user's pending swap
    .{ .tx = .{ .data = signed_backrun_tx } }, // our backrun
};
_ = try client.relay.mevSendBundle(.{
    .body = &body,
    .inclusion = .{ .block = target_block, .max_block = target_block + 2 },
});

Run it (dry-run, against the Sepolia matchmaker):

cd examples && zig build && DRY_RUN=1 ./zig-out/bin/08_mev_share_backrunner

On this page