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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to eth.zig will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- `abigen` module: comptime contract bindings (#68). `eth.bind(@embedFile("erc20.json"))` (alias for `eth.abigen.Bind`) parses a Solidity JSON ABI **at compile time** and returns a fully typed contract struct -- zero runtime ABI parsing, with selectors and event topic0s precomputed at comptime. The ABI is read by a purpose-built comptime JSON tokenizer (std.json needs a runtime allocator and does not run at comptime in 0.16), and generated structs are reified with `@Struct` while typed reads/decodes dispatch on a comptime function name so the argument-tuple and return types are fully resolved per call site. Public surface: `Self.at(address)`; `call(self, provider, comptime name, args) !Ret` (ABI-encodes `selector ++ encode(args)`, runs `eth_call`, decodes into the mapped return type); `selectorOf(name)`/`ArgsOf(name)`/`ReturnOf(name)`; and for events `decodeEvent(name, log) !EventStruct` (indexed params from `log.topics[1..]`, non-indexed from `log.data`, validating `topics[0]`), `topicOf(name)`, and `EventOf(name)`. ABI integers map to the smallest fitting Zig integer (`uint8` -> `u8`, `uint112` -> `u112`), matching zabi's `AbiParameterToPrimative`; `address` -> `[20]u8`, `bool` -> `bool`, `bytesN` -> `[N]u8`, `bytes`/`string` -> `[]const u8`. Read functions and events this release; state-changing writes (taking a `*Wallet`) are a documented follow-up. Tuple/array params and overloaded-function redeclarations are parsed but not yet addressable, and naming an unsupported entry is a clear `@compileError`. Asserts the comptime-derived selectors against known values (`balanceOf(address)` == 0x70a08231, `transfer(address,uint256)` == 0xa9059cbb) and the ERC-20 `Transfer` topic0, and exercises calldata encoding and a full `Transfer` log decode without a network. Builds and tests green on both Zig 0.16 and 0.17-dev

## [0.7.0] - 2026-06-11

### Added
Expand Down
104 changes: 104 additions & 0 deletions docs/content/docs/abigen.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
title: Contract Bindings (abigen)
description: Generate a fully typed contract struct from a JSON ABI at compile time -- zero runtime ABI parsing.
---

`eth.bind(@embedFile("weth.json"))` parses a Solidity JSON ABI **at compile time**
and returns a fully typed contract struct: typed read calls per function with
selectors precomputed, typed event decoders with topics precomputed, and zero
runtime ABI parsing.

This is the unique "why Zig" capability. Rust needs a proc-macro plus a codegen
step to reach the same developer experience; Zig does it in-language with
`comptime`.

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

const Erc20 = eth.bind(@embedFile("erc20.json"));
```

## Reading a contract

Function names are passed as **comptime strings**, so the compiler resolves the
matching ABI entry at each call site and the argument tuple and return types are
fully typed. There is no runtime ABI lookup in the hot path.

```zig
var token = Erc20.at(token_address); // [20]u8 -> contract handle

// balanceOf(address) -> uint256
// args: .{ holder } typed as .{ [20]u8 }
// return: u256
const balance = try token.call(&provider, "balanceOf", .{ holder });

// decimals() -> uint8 maps to the smallest fitting Zig integer: u8
const decimals = try token.call(&provider, "decimals", .{});

// name() -> string is heap-allocated on provider.allocator; the caller frees it
const name = try token.call(&provider, "name", .{});
defer provider.allocator.free(name);
```

The 4-byte selector for any function is precomputed at compile time:

```zig
const sel = Erc20.selectorOf("balanceOf");
// sel == [4]u8{ 0x70, 0xa0, 0x82, 0x31 }
```

`ArgsOf(name)` and `ReturnOf(name)` expose the typed argument-tuple and return
types if you want them directly (e.g. for building calldata yourself).

## Decoding events

Each event gets a typed decoder and a precomputed topic0:

```zig
// topic0 == keccak256("Transfer(address,address,uint256)")
const topic = Erc20.topicOf("Transfer");

// Decode a log into a typed struct:
// indexed params come from log.topics[1..], non-indexed from log.data
const transfer = try Erc20.decodeEvent("Transfer", log);
// transfer.from: [20]u8
// transfer.to: [20]u8
// transfer.value: u256
```

`decodeEvent` validates `log.topics[0]` against the event's signature hash and
returns `error.TopicMismatch` if it does not match, so you can fan a stream of
logs through several decoders and let the wrong ones fall through.

## ABI to Zig type mapping

| ABI type | Zig type | Notes |
| ------------------ | --------------- | ------------------------------------------- |
| `uintN` (8..256) | `uN` | Smallest fitting unsigned (`uint8` -> `u8`) |
| `intN` (8..256) | `iN` | Smallest fitting signed (`int128` -> `i128`)|
| `uint` / `int` | `u256` / `i256` | Bare alias for the 256-bit form |
| `address` | `[20]u8` | Raw 20-byte address |
| `bool` | `bool` | |
| `bytesN` (1..32) | `[N]u8` | Fixed-size byte array |
| `bytes` | `[]const u8` | Dynamic; decoded values are heap-allocated |
| `string` | `[]const u8` | Same encoding as `bytes` |

Integers map to the exact Solidity width: `decimals()` is `u8`, a Uniswap pair's
`getReserves()` returns `u112` fields. The mapping is lossless and the
encode/decode bridge widens to `u256`/`i256` at the boundary.

## Scope and limitations

This release covers **read functions and event decoders**. Honest scope cuts:

- **Writes are deferred.** State-changing calls (which would take a `*Wallet`)
are a planned follow-up.
- **Tuples and arrays.** Functions or events whose parameters contain a `tuple`,
fixed array, or dynamic array still parse and still get a correct selector or
topic, but naming one in `call`/`decodeEvent` is a compile-time error for now.
- **Overloaded functions.** Solidity allows two functions sharing a name; the
first declaration wins. Use the runtime
[`abi_json`](/modules) parser if you need every overload.

Unknown or unsupported names fail the build with a clear `@compileError`, so
typos and unsupported ABI shapes are caught at compile time, never at runtime.
1 change: 1 addition & 0 deletions docs/content/docs/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"examples",
"transactions",
"contracts",
"abigen",
"tokens",
"hd-wallets",
"keystore",
Expand Down
Loading
Loading