eth.zig

Transactions

Building, signing, and sending Ethereum transactions with eth.zig.

eth.zig supports all Ethereum transaction types -- Legacy, EIP-2930 (access lists), EIP-1559 (priority fees), EIP-4844 (blob transactions), and EIP-7702 (set-code authorizations).

Sending a Transaction

The Wallet handles the full lifecycle: auto-filling nonce, gas, and chain ID from the provider, then signing and broadcasting.

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

const private_key = try eth.hex.hexToBytesFixed(32, "your_private_key_hex");
var wallet = eth.wallet.Wallet.init(allocator, private_key, &provider);

// Send 1 ETH -- nonce, gas, and chain_id are auto-filled
const tx_hash = try wallet.sendTransaction(.{
    .to = recipient_address,
    .value = eth.units.parseEther(1.0),
});

SendTransactionOpts

All fields in SendTransactionOpts are optional with sensible defaults. Fields like nonce, gas_limit, and fee parameters are auto-filled from the provider when not specified:

FieldTypeDefault
to?[20]u8null
valueu2560
data[]const u8&.{}
gas_limit?u64Auto (estimateGas)
max_fee_per_gas?u256Auto (getGasPrice)
max_priority_fee_per_gas?u256Auto (getMaxPriorityFeePerGas)
nonce?u64Auto (getTransactionCount)

Transaction Types

Legacy (Type 0)

Pre-EIP-1559 transactions with a single gas_price:

const tx = eth.transaction.Transaction{ .legacy = .{
    .nonce = 0,
    .gas_price = 20_000_000_000, // 20 Gwei
    .gas_limit = 21_000,
    .to = recipient,
    .value = eth.units.parseEther(1.0),
    .data = &.{},
    .chain_id = 1,
}};

EIP-1559 (Type 2)

Modern transactions with base fee + priority fee:

const tx = eth.transaction.Transaction{ .eip1559 = .{
    .chain_id = 1,
    .nonce = 0,
    .max_priority_fee_per_gas = 1_000_000_000, // 1 Gwei tip
    .max_fee_per_gas = 30_000_000_000, // 30 Gwei max
    .gas_limit = 21_000,
    .to = recipient,
    .value = eth.units.parseEther(1.0),
    .data = &.{},
    .access_list = &.{},
}};

EIP-4844 (Type 3)

Blob transactions for L2 data availability:

const tx = eth.transaction.Transaction{ .eip4844 = .{
    .chain_id = 1,
    .nonce = 0,
    .max_priority_fee_per_gas = 1_000_000_000,
    .max_fee_per_gas = 30_000_000_000,
    .gas_limit = 21_000,
    .to = recipient,
    .value = 0,
    .data = &.{},
    .access_list = &.{},
    .max_fee_per_blob_gas = 1_000_000_000,
    .blob_versioned_hashes = &.{blob_hash},
}};

EIP-7702 (Type 4)

Set-code transactions (Pectra) let an EOA delegate its code to a contract via one or more signed authorization tuples. Each authorization is signed over keccak256(0x05 || rlp([chain_id, address, nonce])), and the recovered signer is the authority whose code is being set. Note that EIP-7702's to field is not nullable -- type-0x04 transactions cannot create contracts.

Sign an authorization with signer.signAuthorization, then attach it to the transaction. A chain_id of 0 makes the authorization valid on any chain.

const eth = @import("eth");

// The authority (the EOA whose code is being delegated) signs the authorization.
var authority = eth.signer.Signer.init(authority_private_key);
defer authority.deinit();

// Delegate `authority`'s code to `delegate_contract` for chain 1, nonce 0.
const auth = try authority.signAuthorization(
    allocator,
    1, // chain_id (0 = valid on any chain)
    delegate_contract, // [20]u8 delegation target
    0, // authority account nonce
);

const auths = [_]eth.transaction.Authorization{auth};

const tx = eth.transaction.Transaction{ .eip7702 = .{
    .chain_id = 1,
    .nonce = 0,
    .max_priority_fee_per_gas = 1_000_000_000, // 1 Gwei tip
    .max_fee_per_gas = 30_000_000_000, // 30 Gwei max
    .gas_limit = 100_000,
    .to = recipient, // required -- no contract creation
    .value = 0,
    .data = &.{},
    .access_list = &.{},
    .authorization_list = &auths,
}};

// Sign the transaction with the sender's key and broadcast.
var sender = eth.signer.Signer.init(sender_private_key);
defer sender.deinit();

const sighash = try eth.transaction.hashForSigning(allocator, tx);
const sig = try sender.signHash(sighash);
const raw = try eth.transaction.serializeSigned(allocator, tx, sig.r, sig.s, sig.v);
defer allocator.free(raw);

const tx_hash = try provider.sendRawTransaction(raw);

Serialization

Serialize transactions for signing or broadcasting:

// Serialize for signing (produces the sighash payload)
const serialized = try eth.transaction.serializeForSigning(allocator, tx);
defer allocator.free(serialized);

// Hash the serialized transaction
const sighash = eth.keccak.keccak256(serialized);

Unit Conversions

const eth_mod = @import("eth");

const one_ether = eth_mod.units.parseEther(1.0);    // 1000000000000000000
const one_gwei = eth_mod.units.parseGwei(1.0);       // 1000000000

On this page