Skip to content

Commit 09e39d7

Browse files
authored
Add ERC-20/721 modules, JSON ABI parser, examples, and fix Zig 0.15.2 compat (#1)
New modules: - src/erc20.zig: typed ERC-20 wrapper with comptime selectors - src/erc721.zig: typed ERC-721 wrapper with comptime selectors - src/abi_json.zig: Solidity JSON ABI artifact parser Examples directory (examples/): - 7 example programs demonstrating key library features - Standalone build.zig with path dependency on parent module Zig 0.15.2 API migration across 12 source files: - ArrayList.init(allocator) -> .empty + pass allocator to each method - Pointer.Size enum: .One/.Slice -> .one/.slice - pbkdf2 namespace flattened to direct function call - Added @setEvalBranchQuota for comptime keccak hashing
1 parent 20942c8 commit 09e39d7

26 files changed

Lines changed: 1971 additions & 358 deletions

examples/01_derive_address.zig

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Example 01: Derive an Ethereum address from a private key
2+
//
3+
// Pure compute -- no RPC connection needed.
4+
5+
const std = @import("std");
6+
const eth = @import("eth");
7+
8+
pub fn main() !void {
9+
var buf: [4096]u8 = undefined;
10+
var stdout_impl = std.fs.File.stdout().writer(&buf);
11+
const stdout = &stdout_impl.interface;
12+
13+
// Hardhat/Anvil account #0 private key (DO NOT use in production)
14+
const private_key = try eth.hex.hexToBytesFixed(32, "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80");
15+
16+
// Create a signer from the private key
17+
const signer = eth.signer.Signer.init(private_key);
18+
19+
// Derive the Ethereum address
20+
const addr = try signer.address();
21+
const checksum = eth.primitives.addressToChecksum(&addr);
22+
23+
try stdout.print("Private key: 0xac0974...f2ff80\n", .{});
24+
try stdout.print("Address: {s}\n", .{checksum});
25+
// Expected: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
26+
try stdout.flush();
27+
}

examples/02_check_balance.zig

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Example 02: Check an account balance via JSON-RPC
2+
//
3+
// Requires a running Ethereum node (e.g., Anvil at localhost:8545).
4+
// Start Anvil with: anvil
5+
6+
const std = @import("std");
7+
const eth = @import("eth");
8+
9+
pub fn main() !void {
10+
var buf: [4096]u8 = undefined;
11+
var stdout_impl = std.fs.File.stdout().writer(&buf);
12+
const stdout = &stdout_impl.interface;
13+
14+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
15+
defer _ = gpa.deinit();
16+
const allocator = gpa.allocator();
17+
18+
// Connect to local node
19+
var transport = eth.http_transport.HttpTransport.init(allocator, "http://127.0.0.1:8545");
20+
defer transport.deinit();
21+
var provider = eth.provider.Provider.init(allocator, &transport);
22+
23+
// Anvil account #0
24+
const addr = try eth.primitives.addressFromHex("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
25+
26+
const balance = provider.getBalance(addr) catch |err| {
27+
try stdout.print("Failed to connect to RPC: {}\n", .{err});
28+
try stdout.print("\nTo run this example, start Anvil:\n anvil\n", .{});
29+
try stdout.flush();
30+
return;
31+
};
32+
33+
const ether = eth.units.formatEther(balance);
34+
const checksum = eth.primitives.addressToChecksum(&addr);
35+
try stdout.print("Address: {s}\n", .{checksum});
36+
try stdout.print("Balance: {d:.4} ETH\n", .{ether});
37+
try stdout.flush();
38+
}

examples/03_sign_message.zig

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Example 03: Sign a message with EIP-191 personal message prefix
2+
//
3+
// Pure compute -- no RPC connection needed.
4+
5+
const std = @import("std");
6+
const eth = @import("eth");
7+
8+
pub fn main() !void {
9+
var buf: [4096]u8 = undefined;
10+
var stdout_impl = std.fs.File.stdout().writer(&buf);
11+
const stdout = &stdout_impl.interface;
12+
13+
// Hardhat/Anvil account #0 private key
14+
const private_key = try eth.hex.hexToBytesFixed(32, "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80");
15+
16+
const signer = eth.signer.Signer.init(private_key);
17+
const addr = try signer.address();
18+
const checksum = eth.primitives.addressToChecksum(&addr);
19+
20+
// Sign a personal message (EIP-191)
21+
const message = "Hello, Ethereum!";
22+
const sig = try signer.signMessage(message);
23+
24+
try stdout.print("Signer: {s}\n", .{checksum});
25+
try stdout.print("Message: \"{s}\"\n", .{message});
26+
try stdout.print("v: {d}\n", .{sig.v});
27+
try stdout.print("r: ", .{});
28+
for (sig.r) |b| try stdout.print("{x:0>2}", .{b});
29+
try stdout.print("\ns: ", .{});
30+
for (sig.s) |b| try stdout.print("{x:0>2}", .{b});
31+
try stdout.print("\n", .{});
32+
try stdout.flush();
33+
}

examples/04_send_transaction.zig

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Example 04: Send a transaction using the Wallet
2+
//
3+
// Requires Anvil running at localhost:8545.
4+
// Start Anvil with: anvil
5+
6+
const std = @import("std");
7+
const eth = @import("eth");
8+
9+
pub fn main() !void {
10+
var buf: [4096]u8 = undefined;
11+
var stdout_impl = std.fs.File.stdout().writer(&buf);
12+
const stdout = &stdout_impl.interface;
13+
14+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
15+
defer _ = gpa.deinit();
16+
const allocator = gpa.allocator();
17+
18+
// Connect to local Anvil node
19+
var transport = eth.http_transport.HttpTransport.init(allocator, "http://127.0.0.1:8545");
20+
defer transport.deinit();
21+
var provider = eth.provider.Provider.init(allocator, &transport);
22+
23+
// Test connection first
24+
_ = provider.getChainId() catch |err| {
25+
try stdout.print("Failed to connect to RPC: {}\n", .{err});
26+
try stdout.print("\nTo run this example, start Anvil:\n anvil\n", .{});
27+
try stdout.flush();
28+
return;
29+
};
30+
31+
// Anvil account #0
32+
const private_key = try eth.hex.hexToBytesFixed(32, "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80");
33+
34+
var wallet = eth.wallet.Wallet.init(allocator, private_key, &provider);
35+
const sender = try wallet.address();
36+
const sender_checksum = eth.primitives.addressToChecksum(&sender);
37+
38+
// Send 0.1 ETH to account #1
39+
const recipient = try eth.primitives.addressFromHex("0x70997970C51812dc3A010C7d01b50e0d17dc79C8");
40+
const recipient_checksum = eth.primitives.addressToChecksum(&recipient);
41+
const value = eth.units.parseEther(0.1);
42+
43+
try stdout.print("Sending 0.1 ETH\n", .{});
44+
try stdout.print(" From: {s}\n", .{sender_checksum});
45+
try stdout.print(" To: {s}\n", .{recipient_checksum});
46+
47+
const tx_hash = try wallet.sendTransaction(.{
48+
.to = recipient,
49+
.value = value,
50+
});
51+
52+
try stdout.print("Tx hash: 0x", .{});
53+
for (tx_hash) |b| try stdout.print("{x:0>2}", .{b});
54+
try stdout.print("\n", .{});
55+
try stdout.flush();
56+
}

examples/05_read_erc20.zig

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Example 05: Read ERC-20 token data using the ERC20 convenience module
2+
//
3+
// Requires Anvil running at localhost:8545 with a deployed ERC-20 token.
4+
// This example demonstrates the API -- it will fail without a deployed contract.
5+
6+
const std = @import("std");
7+
const eth = @import("eth");
8+
9+
pub fn main() !void {
10+
var buf: [4096]u8 = undefined;
11+
var stdout_impl = std.fs.File.stdout().writer(&buf);
12+
const stdout = &stdout_impl.interface;
13+
14+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
15+
defer _ = gpa.deinit();
16+
const allocator = gpa.allocator();
17+
18+
// Connect to local node
19+
var transport = eth.http_transport.HttpTransport.init(allocator, "http://127.0.0.1:8545");
20+
defer transport.deinit();
21+
var provider = eth.provider.Provider.init(allocator, &transport);
22+
23+
// Test connection
24+
_ = provider.getChainId() catch |err| {
25+
try stdout.print("Failed to connect to RPC: {}\n", .{err});
26+
try stdout.print("\nTo run this example, start Anvil:\n anvil\n", .{});
27+
try stdout.flush();
28+
return;
29+
};
30+
31+
// Show the ERC-20 module API
32+
try stdout.print("ERC-20 Module API:\n\n", .{});
33+
try stdout.print("Comptime selectors (zero runtime cost):\n", .{});
34+
try stdout.print(" transfer: 0x", .{});
35+
for (eth.erc20.selectors.transfer) |b| try stdout.print("{x:0>2}", .{b});
36+
try stdout.print("\n balanceOf: 0x", .{});
37+
for (eth.erc20.selectors.balanceOf) |b| try stdout.print("{x:0>2}", .{b});
38+
try stdout.print("\n approve: 0x", .{});
39+
for (eth.erc20.selectors.approve) |b| try stdout.print("{x:0>2}", .{b});
40+
try stdout.print("\n totalSupply: 0x", .{});
41+
for (eth.erc20.selectors.totalSupply) |b| try stdout.print("{x:0>2}", .{b});
42+
try stdout.print("\n\n", .{});
43+
44+
// Show how you would use it with a real token contract:
45+
try stdout.print("Usage:\n", .{});
46+
try stdout.print(" var token = eth.erc20.ERC20.init(allocator, token_addr, &provider);\n", .{});
47+
try stdout.print(" const balance = try token.balanceOf(holder_addr);\n", .{});
48+
try stdout.print(" const name = try token.name();\n", .{});
49+
try stdout.print(" defer allocator.free(name);\n", .{});
50+
try stdout.flush();
51+
}

examples/06_hd_wallet.zig

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Example 06: HD wallet derivation from mnemonic
2+
//
3+
// Pure compute -- no RPC connection needed.
4+
// Derives multiple Ethereum addresses from a BIP-39 mnemonic.
5+
6+
const std = @import("std");
7+
const eth = @import("eth");
8+
9+
pub fn main() !void {
10+
var buf: [4096]u8 = undefined;
11+
var stdout_impl = std.fs.File.stdout().writer(&buf);
12+
const stdout = &stdout_impl.interface;
13+
14+
// Standard test mnemonic (DO NOT use in production)
15+
const words = [_][]const u8{
16+
"abandon", "abandon", "abandon", "abandon",
17+
"abandon", "abandon", "abandon", "abandon",
18+
"abandon", "abandon", "abandon", "about",
19+
};
20+
21+
// Convert mnemonic to seed
22+
const seed = try eth.mnemonic.toSeed(&words, "");
23+
24+
try stdout.print("Mnemonic: abandon abandon ... about\n\n", .{});
25+
try stdout.print("Derived accounts (BIP-44: m/44'/60'/0'/0/i):\n", .{});
26+
27+
// Derive first 5 accounts
28+
for (0..5) |i| {
29+
const key = try eth.hd_wallet.deriveEthAccount(seed, @intCast(i));
30+
const addr = key.toAddress();
31+
const checksum = eth.primitives.addressToChecksum(&addr);
32+
try stdout.print(" [{d}] {s}\n", .{ i, checksum });
33+
}
34+
try stdout.flush();
35+
}

examples/07_comptime_selectors.zig

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Example 07: Comptime function selectors and event topics
2+
//
3+
// Pure compute -- no RPC connection needed.
4+
// Showcases eth.zig's comptime-first approach: all selectors and topics
5+
// are computed at compile time with zero runtime cost.
6+
7+
const std = @import("std");
8+
const eth = @import("eth");
9+
10+
pub fn main() !void {
11+
var buf: [4096]u8 = undefined;
12+
var stdout_impl = std.fs.File.stdout().writer(&buf);
13+
const stdout = &stdout_impl.interface;
14+
15+
// Use runtime selectors in examples (comptime selectors are used inside the library
16+
// where the eval branch quota is pre-configured).
17+
const transfer_sel = eth.keccak.selector("transfer(address,uint256)");
18+
const approve_sel = eth.keccak.selector("approve(address,uint256)");
19+
const balance_sel = eth.keccak.selector("balanceOf(address)");
20+
21+
const transfer_topic = eth.keccak.hash("Transfer(address,address,uint256)");
22+
const approval_topic = eth.keccak.hash("Approval(address,address,uint256)");
23+
24+
try stdout.print("Function Selectors:\n", .{});
25+
try stdout.print(" transfer(address,uint256): 0x", .{});
26+
for (transfer_sel) |b| try stdout.print("{x:0>2}", .{b});
27+
try stdout.print("\n approve(address,uint256): 0x", .{});
28+
for (approve_sel) |b| try stdout.print("{x:0>2}", .{b});
29+
try stdout.print("\n balanceOf(address): 0x", .{});
30+
for (balance_sel) |b| try stdout.print("{x:0>2}", .{b});
31+
32+
try stdout.print("\n\nEvent Topics:\n", .{});
33+
try stdout.print(" Transfer(address,address,uint256):\n 0x", .{});
34+
for (transfer_topic) |b| try stdout.print("{x:0>2}", .{b});
35+
try stdout.print("\n Approval(address,address,uint256):\n 0x", .{});
36+
for (approval_topic) |b| try stdout.print("{x:0>2}", .{b});
37+
38+
try stdout.print("\n\nInside the library, these are computed at compile time:\n", .{});
39+
try stdout.print(" const sel = eth.abi_comptime.comptimeSelector(\"transfer(address,uint256)\");\n", .{});
40+
try stdout.print(" // sel == [4]u8{{ 0xa9, 0x05, 0x9c, 0xbb }} -- zero runtime cost\n", .{});
41+
try stdout.flush();
42+
}

examples/build.zig

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const std = @import("std");
2+
3+
pub fn build(b: *std.Build) void {
4+
const target = b.standardTargetOptions(.{});
5+
const optimize = b.standardOptimizeOption(.{});
6+
7+
const eth_dep = b.dependency("eth", .{
8+
.target = target,
9+
.optimize = optimize,
10+
});
11+
const eth_module = eth_dep.module("eth");
12+
13+
const examples = .{
14+
.{ "01_derive_address", "01_derive_address.zig" },
15+
.{ "02_check_balance", "02_check_balance.zig" },
16+
.{ "03_sign_message", "03_sign_message.zig" },
17+
.{ "04_send_transaction", "04_send_transaction.zig" },
18+
.{ "05_read_erc20", "05_read_erc20.zig" },
19+
.{ "06_hd_wallet", "06_hd_wallet.zig" },
20+
.{ "07_comptime_selectors", "07_comptime_selectors.zig" },
21+
};
22+
23+
inline for (examples) |example| {
24+
const exe = b.addExecutable(.{
25+
.name = example[0],
26+
.root_module = b.createModule(.{
27+
.root_source_file = b.path(example[1]),
28+
.target = target,
29+
.optimize = optimize,
30+
.imports = &.{
31+
.{ .name = "eth", .module = eth_module },
32+
},
33+
}),
34+
});
35+
b.installArtifact(exe);
36+
}
37+
}

examples/build.zig.zon

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.{
2+
.name = .eth_zig_examples,
3+
.version = "0.1.0",
4+
.fingerprint = 0xd73bf5facd267e8e,
5+
.minimum_zig_version = "0.15.2",
6+
.dependencies = .{
7+
.eth = .{
8+
.path = "..",
9+
},
10+
},
11+
.paths = .{
12+
"build.zig",
13+
"build.zig.zon",
14+
},
15+
}

0 commit comments

Comments
 (0)