
Alloy
- 10 installs
- Updated April 23, 2026
- melonask/alloy-skills
Helps with ai & agent building tasks.
About
alloy is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- alloy
- AI & Agent Building
- AI-coding skill
Alloy by the numbers
- 10 all-time installs (skills.sh)
- Ranked #11,959 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/melonask/alloy-skills --skill alloyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 10 |
|---|---|
| Last updated | April 23, 2026 |
| Repository | melonask/alloy-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Alloy — Rust Ethereum & EVM Development Library
Alloy is the next-generation Rust library for interacting with Ethereum and all EVM-compatible blockchains. It is maintained by the Reth team at Paradigm and is the successor to ethers-rs. Alloy provides a modular, type-safe, and performant toolkit for every layer of blockchain interaction: providers, signers, contracts, transactions, and network primitives.
Crate Architecture
| Crate | Purpose |
|---|---|
alloy | Meta-crate that re-exports everything — start here for simple projects |
alloy-provider | RPC providers (HTTP, WS, IPC) with layered middleware |
alloy-signer-* | Wallet/signer implementations (local, Ledger, Trezor, AWS, GCP, YubiKey) |
alloy-network | Network abstractions (Ethereum, custom chains) |
alloy-primitives | Core types: Address, U256, Bytes, FixedBytes, B256 |
alloy-sol-types | Solidity type system: ABI encode/decode, the sol! macro |
alloy-contract | High-level contract interaction: deploy, call, events |
alloy-transport | Transport layer: HTTP, WS, IPC connections |
alloy-consensus | Transaction types and consensus logic |
alloy-json-rpc | JSON-RPC type definitions |
alloy-node-bindings | Local node management: Anvil, Geth, Reth |
alloy-chains | Chain definitions and ID mappings |
Quick Start: Minimal Dependency Setup
Add the meta-crate for the fastest setup — it re-exports the most commonly needed items:
# Cargo.toml
[dependencies]
alloy = { version = "2.0", features = ["full"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
eyre = "0.6"For finer-grained control, use individual crates:
[dependencies]
alloy-primitives = "2.0"
alloy-sol-types = "2.0"
alloy-provider = { version = "2.0", features = ["reqwest"] }
alloy-signer-local = "2.0"
alloy-consensus = "2.0"
alloy-contract = "2.0"
alloy-network = { version = "2.0", features = ["ethereum"] }
alloy-transport = { version = "2.0", features = ["http", "ws"] }Quick Reference: Which Guide Do I Need?
The user's task determines which reference file to read:
- Signing messages & creating digital signatures -> Read
references/signatures-wallets.md - Sending transactions (ETH & ERC-20 tokens) -> Read
references/transactions-payments.md - Verifying incoming payments & monitoring transfers -> Read
references/payment-verification.md - Setting up providers (HTTP/WS/IPC) & middleware layers -> Read
references/providers-networking.md - Deploying & calling smart contracts, sol! macro -> Read
references/contracts-abi.md - Subscribing to events, blocks, logs -> Read
references/subscriptions-events.md - Address, U256, Bytes, hashing utilities -> Read
references/primitives-types.md - Anvil, Geth, Reth local node integration -> Read
references/node-bindings.md - Cargo setup, feature flags, ethers-rs migration -> Read
references/cargo-setup.md
Core Patterns at a Glance
1. Create a Provider and Send a Transaction
This is the most common starting pattern. Every blockchain interaction flows through a provider.
use alloy::network::EthereumWallet;
use alloy::primitives::address;
use alloy::providers::{Provider, ProviderBuilder};
use alloy::signers::local::PrivateKeySigner;
#[tokio::main]
async fn main() -> eyre::Result<()> {
// Set up signer from private key
let signer: PrivateKeySigner = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
.parse()?;
let wallet = EthereumWallet::from(signer.clone());
// Build provider with wallet (HTTP by default). Recommended fillers (gas, nonce, chain ID)
// are already enabled by ProviderBuilder::new().
let rpc_url = "https://eth.llamarpc.com".parse()?;
let provider = ProviderBuilder::new()
.wallet(wallet)
.connect_http(rpc_url);
// Get balance
let address = signer.address();
let balance = provider.get_balance(address).await?;
println!("Balance: {}", balance);
Ok(())
}2. Transfer Native ETH
use alloy::primitives::{address, U256, Bytes};
use alloy::providers::{Provider, ProviderBuilder};
use alloy::rpc::types::TransactionRequest;
use alloy::network::TransactionBuilder;
use alloy::signers::local::PrivateKeySigner;
use alloy::network::EthereumWallet;
let signer: PrivateKeySigner = "0x...".parse()?;
let wallet = EthereumWallet::from(signer.clone());
let provider = ProviderBuilder::new()
.wallet(wallet)
.connect_http("https://eth.llamarpc.com".parse()?);
// Build and send the transaction
let tx = TransactionRequest::default()
.to(address!("0xRecipientAddress"))
.value(U256::from(1_000_000_000_000_000_000u128)) // 1 ETH (18 decimals)
.with_input(Bytes::new());
let pending = provider.send_transaction(tx).await?;
// Wait for confirmation and fetch receipt
let receipt = pending.get_receipt().await?;
println!("Status: {:?}", receipt.status());3. Sign a Message (EIP-191 Personal Sign)
use alloy::signers::{Signer, local::PrivateKeySigner};
let signer: PrivateKeySigner = "0x...".parse()?;
let message = "Hello, Ethereum!";
// Sign with EIP-191 prefix (personal_sign)
let signature = signer.sign_message(message.as_bytes()).await?;
println!("Signature: 0x{}", signature);
// Verify
let recovered = signature.recover_address_from_msg(message)?;
assert_eq!(recovered, signer.address());4. ERC-20 Token Transfer
use alloy::primitives::{address, U256};
use alloy::sol;
// Define the ERC-20 interface using sol! macro
sol! {
#[sol(rpc)]
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
function decimals() external view returns (uint8);
function approve(address spender, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
}
}
let token_address = address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC on mainnet
let token = IERC20::new(token_address, &provider);
// Check balance
let balance = token.balanceOf(signer.address()).call().await?;
println!("USDC balance: {}", balance);
// Check decimals
let decimals = token.decimals().call().await?;
println!("USDC decimals: {}", decimals);
// Transfer 100 USDC
let tx_hash = token.transfer(address!("0xRecipient"), U256::from(100_000_000u128))
.send()
.await?
.watch()
.await?;5. Deploy a Contract
sol! {
#[sol(abi)]
contract MyContract {
constructor(uint256 initial_value);
function value() external view returns (uint256);
}
}
// Get the compiled bytecode (from forge build or solc)
let bytecode = hex::decode("6080604052...")?;
let deploy_tx = MyContract::deploy_builder(&provider, U256::from(42))
.deploy()
.await?;
let contract_address = deploy_tx.address();
println!("Deployed to: {}", contract_address);
// Interact
let contract = MyContract::new(contract_address, &provider);
let value = contract.value().call().await?;Key Concepts
Provider Layer Stack
Providers are built in layers. Each layer adds functionality (middleware pattern):
Your code
|
SignerLayer (adds wallet/signer for signing)
|
GasFiller (auto-estimates gas price)
|
NonceFiller (manages nonce sequence)
|
ChainIdFiller (auto-fills chain ID)
|
Transport (HTTP / WS / IPC)
|
RPC NodeUse ProviderBuilder::new() — it comes with all recommended fillers (gas, nonce, chain ID) pre-configured. The wallet layer is added on top so it signs after all fillers have populated their fields.
Supported Transaction Types
| Type | EIP | Key Feature |
|---|---|---|
| Legacy | - | Gas price only |
| EIP-1559 | 1559 | Base fee + priority fee, gas tip market |
| EIP-4844 | 4844 | Blob transactions (data availability) |
| EIP-7702 | 7702 | Account abstraction via delegation |
| EIP-7594 | 7594 | PeerDAS (peer data availability sampling) |
| Private | - | Private transaction pools (mev-share, flashbots) |
Signer Types
| Signer | Crate | Use Case |
|---|---|---|
PrivateKeySigner | alloy-signer-local | Local private key, testing, scripts |
MnemonicSigner | alloy-signer-local | HD wallet from BIP-39 mnemonic phrase |
LedgerSigner | alloy-signer-ledger | Ledger hardware wallet |
TrezorSigner | alloy-signer-trezor | Trezor hardware wallet |
AwsSigner | alloy-signer-aws | AWS KMS signing |
GcpSigner | alloy-signer-gcp | Google Cloud KMS signing |
YubiSigner | alloy-signer-yubihsm | YubiKey HSM signing |
All signers implement the Signer trait, so they are interchangeable — swap a PrivateKeySigner for a LedgerSigner without changing any other code.
The sol! Macro
The sol! macro generates type-safe Rust bindings from Solidity source. This is the preferred way to define contract interfaces:
sol! {
#[sol(rpc)] // generates both ABI bindings AND an RPC contract struct
interface IERC20 {
function balanceOf(address who) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
}
}Key attributes:
#[sol(rpc)]— generates an RPC-ready contract struct#[sol(abi)]— generates only ABI types (no RPC, for deploy)#[derive(Debug, PartialEq)]— standard Rust derives on generated types
Common Pitfalls
1. Missing fillers — ProviderBuilder::new() already includes recommended fillers by default in Alloy 2.0. Use .disable_recommended_fillers() if you need to opt out and manage gas, nonce, and chain ID manually.
2. Decimals confusion — ERC-20 tokens use different decimals (USDC = 6, WETH = 18). Always check with decimals() call or use parse_units / format_units utilities.
3. Chain ID mismatches — The ChainIdFiller handles this automatically. If you skip it, transactions will be rejected. Never hardcode chain IDs.
4. Blocking in async context — All provider methods are async. Hardware signers may block briefly. Use tokio::task::spawn_blocking if needed.
5. Integer overflow — Always use U256 for on-chain values, not native Rust integers. The U256::from() constructor accepts u64 or u128.
Reference Files
For detailed information on any topic, read the appropriate reference file:
references/signatures-wallets.md— All signer types, EIP-191/EIP-712 signing, message verification, keystore creation, permit signatures, hardware wallet setupreferences/transactions-payments.md— EIP-1559/legacy/4844/7702 transactions, ETH transfers, ERC-20 transfers, permit2 flows, gas estimation, private transactions, transaction lifecyclereferences/payment-verification.md— Verifying incoming payments, monitoring transaction receipts, parsing transfer events, filtering logs, building payment listeners, reconciliation patternsreferences/providers-networking.md— HTTP/WS/IPC providers, provider builder, layers (retry, fallback, logging, delay), batched RPC, embedded consensus, authenticated connections, dynamic providersreferences/contracts-abi.md— sol! macro deep dive, contract deployment from artifact/bytecode, static vs dynamic ABI, JSON ABI loading, revert decoding, multi-call batching, library linkingreferences/subscriptions-events.md— Block subscriptions, log filtering, pending transaction streams, event multiplexer, poll-based log watching, ENS resolutionreferences/primitives-types.md— Address, B256, Bytes, FixedBytes, U256/U128, Uint/Bint, parse_units/format_units, keccak256, ecrecover, conversion between typesreferences/node-bindings.md— Anvil (launch, fork, set storage), Geth integration, Reth local instances, foundry test helpersreferences/cargo-setup.md— Feature flags table, minimal vs full crate selection, ethers-rs migration guide, type conversion reference, Cargo.toml patterns per use case
Known Issues in Alloy Skill (Fixed)
This document records all errors found during real-world testing of the Alloy skill with alloy version 2.0.1 (April 2026), along with the fixes applied.
---
Issue 1: EthereumWallet::from(signer) consumes the signer
File: SKILL.md, references/transactions-payments.md, references/providers-networking.md
Problem: The code uses EthereumWallet::from(signer) which moves signer, but then tries to use signer afterward (e.g., signer.address()). This fails with error[E0382]: borrow of moved value: signer.
Example from SKILL.md (line 82-94):
let signer: PrivateKeySigner = "0x...".parse()?;
let wallet = EthereumWallet::from(signer); // ERROR: signer moved
// ...
let address = signer.address(); // ERROR: borrow after moveFix: Clone the signer before passing to EthereumWallet::from():
let wallet = EthereumWallet::from(signer.clone());
let address = signer.address(); // OK: signer still availableVerified with real test project: test-projects/test01-basic-provider/src/main.rs
---
Issue 2: .with_recommended_fillers() method does not exist
File: SKILL.md line 281, references/cargo-setup.md line 170
Problem: The skill mentions .with_recommended_fillers() as something to always call. In Alloy 2.0.1, ProviderBuilder::new() already includes recommended fillers by default. Calling .with_recommended_fillers() causes:
error[E0599]: no method named `with_recommended_fillers` found for struct `ProviderBuilder<...>`Fix: Remove all references to .with_recommended_fillers(). The builder already has fillers enabled by default. If you need to opt-out, use .disable_recommended_fillers().
Verified with real test project: test-projects/test07-providers/src/main.rs
---
Issue 3: Transfer::decode_log(&log, true) signature is wrong
File: references/payment-verification.md, references/subscriptions-events.md
Problem: The skill uses Transfer::decode_log(&log, true) but the correct API in alloy 2.0.1 uses decode_log(log: &Log) with only one argument. Additionally, SolEvent trait must be in scope.
Example from payment-verification.md:
let transfer = Transfer::decode_log(&log, true)?; // ERROR: two args instead of oneFix: Use Transfer::decode_log(&log) (single argument) and import alloy::sol_types::SolEvent:
use alloy::sol_types::SolEvent;
let transfer = Transfer::decode_log(&log)?; // OKFor decoding event data from a LogData object, use decode_log_data:
let data = Transfer::decode_log_data(&log.data())?;Verified with real test project: test-projects/test08-payment-verification/src/main.rs
---
Issue 4: alloy_node_bindings import path is wrong in node-bindings.md
File: references/node-bindings.md
Problem: The skill uses use alloy_node_bindings::Anvil; but with the alloy meta-crate and node-bindings feature enabled, the correct path is use alloy::node_bindings::Anvil;.
Example from node-bindings.md:
use alloy_node_bindings::Anvil; // ERROR: unresolved importFix: Use the re-export from the alloy meta-crate:
use alloy::node_bindings::Anvil; // OKAlso, anvil.endpoint_url() returns a Url directly, so no .parse()? is needed.
Verified with real test project: test-projects/test09-node-bindings/src/main.rs
---
Issue 5: keccak256(b"...") returns [u8; 32], not B256 directly
File: references/primitives-types.md, references/payment-verification.md
Problem: The skill uses B256::from_slice(&alloy::primitives::keccak256(...)) which is unnecessarily verbose. keccak256() already returns [u8; 32] which can be directly converted to B256.
Fix: Use B256::from(keccak256(b"...")) or simply rely on the macro types.
Actually, the key issue is that B256::from_slice(&keccak256(...)) works but is unidiomatic. The bigger issue is that in many places the code tries to pass a slice to B256::from_slice but keccak256 returns an owned array.
Verified with real test project: test-projects/test08-payment-verification/src/main.rs
---
Issue 6: provider.wallet() method doesn't exist on AnvilInstance
File: references/node-bindings.md line 33, 165, 191
Problem: The skill mentions .wallet(anvil.wallet().unwrap()) but AnvilInstance does not have a .wallet() method.
Fix: Construct the wallet manually from a private key:
use alloy::signers::local::PrivateKeySigner;
use alloy::network::EthereumWallet;
let signer: PrivateKeySigner = anvil.keys()[0].parse()?;
let wallet = EthereumWallet::from(signer);
let provider = ProviderBuilder::new()
.wallet(wallet)
.connect_http(anvil.endpoint_url());Verified with real test project: test-projects/test09-node-bindings/src/main.rs
---
Issue 7: ok_or_eyre is not a standard method
File: references/payment-verification.md
Problem: The code uses .ok_or_eyre("...") but this method requires a specific trait import that might not be obvious.
Fix: Use .ok_or(eyre::eyre!("...")) or ensure use eyre::Context; is imported. The Context trait from eyre provides ok_or_eyre().
use eyre::Context;
let receipt = provider.get_transaction_receipt(tx_hash).await?
.ok_or_eyre("Transaction not found")?;Actually, in our tests, .ok_or(eyre::eyre!("...")) worked.
---
Issue 8: with_input::<Bytes>(vec![].into()) syntax
File: SKILL.md line 122
Problem: The code uses .with_input::<Bytes>(vec![].into()) — this actually compiles fine but is an unusual way to specify no input. It may cause confusion.
Fix: The pattern is functional but a simpler approach is .with_input(Bytes::new()) or omit the call entirely when there is no input data.
---
Issue 9: DynProvider does not exist in alloy 2.0.1
File: references/providers-networking.md line 224-232
Problem: The skill uses DynProvider but this type does not exist in alloy 2.0.1.
Fix: There is currently no simple dynamic provider type in alloy 2.0. Use Box<dyn Provider> or a concrete provider type.
---
Issue 10: BlockNumber::Latest vs BlockNumberOrTag::Latest
File: references/payment-verification.md, references/subscriptions-events.md
Problem: The skill uses BlockNumber::Latest in places where BlockNumberOrTag::Latest is expected.
Fix: Use alloy::rpc::types::BlockNumberOrTag::Latest.
---
Issue 11: Nonce Race Condition with Concurrent Settlement Workers
File: references/providers-networking.md, references/transactions-payments.md
Problem: When using ProviderBuilder::new().wallet(wallet), the NonceFiller is included by default. The NonceFiller queries eth_getTransactionCount from the RPC node to determine the nonce. If you run settlement workers with .concurrency(N) where N > 1 (e.g., apalis with 4 concurrent workers), multiple threads may query the nonce simultaneously before the first transaction hits the mempool. They will all receive the same nonce (e.g., 42), causing 3 of 4 transactions to fail with "nonce too low" or "replacement transaction underpriced".
Scenario: Facilitator settling x402 payment transactions on-chain with 4 concurrent workers.
Fix Option 1 — Restrict concurrency to 1:
// For settlement/ledger workers, use concurrency(1)
.configure_worker(|worker| {
worker.concurrency(1)
})Fix Option 2 — Implement a local Mutex nonce manager:
use std::sync::Mutex;
use alloy::providers::{Provider, ProviderBuilder};
use alloy::signers::local::PrivateKeySigner;
use alloy::network::EthereumWallet;
struct NonceManager {
next_nonce: Mutex<u64>,
}
impl NonceManager {
fn new(start: u64) -> Self {
Self { next_nonce: Mutex::new(start) }
}
fn next(&self) -> u64 {
*self.next_nonce.lock().unwrap()
}
fn increment(&self) -> u64 {
*self.next_nonce.lock().unwrap()
}
}
// When building the provider, disable the default NonceFiller and use your own:
// ProviderBuilder::new()
// .wallet(wallet)
// .disable_recommended_fillers() // Remove default NonceFiller
// .connect_http(rpc_url);
// Then manually set nonce on each transaction:
// let tx = tx_request.clone().with_nonce(manager.next());
// manager.increment();Fix Option 3 — Redis-backed atomic nonce (for distributed systems): For multi-instance deployments, use Redis INCR to atomically increment the nonce across processes.
---
Summary of Verified Working Features
The following patterns from the skill were verified to work correctly with alloy 2.0.1:
1. ✅ ProviderBuilder::new().connect_http(url) — basic provider creation 2. ✅ ProviderBuilder::new().connect(url).await — async connection 3. ✅ PrivateKeySigner parsing and signing messages 4. ✅ EthereumWallet::from(signer) (with clone) 5. ✅ sol! { #[sol(rpc)] interface IERC20 { ... } } — contract interface generation 6. ✅ IERC20::new(address, &provider) — contract instance creation 7. ✅ token.balanceOf(addr).call().await? — read-only calls 8. ✅ token.decimals().call().await? — calling view functions 9. ✅ provider.get_balance(address).await? — balance queries 10. ✅ provider.get_block_number().await? — block number queries 11. ✅ address!("0x...") macro 12. ✅ U256::from(1_000_000u64) and arithmetic operations 13. ✅ keccak256(b"...") hashing 14. ✅ parse_units("1.5", 18)? and format_units(raw, 18)? 15. ✅ Address::ZERO, Address::from_word(), etc. 16. ✅ alloy::node_bindings::Anvil — programmatic node spawning 17. ✅ anvil.addresses(), anvil.endpoint_url(), anvil.chain_id() 18. ✅ provider.get_transaction_receipt(hash).await? 19. ✅ Filter::new().address(token).event_signature(topic) 20. ✅ provider.get_logs(&filter).await? 21. ✅ Signature verification: signature.recover_address_from_msg(message)? 22. ✅ alloy::transports::TransportError for error handling
---
Tested on: April 23, 2026 Rust version: 1.95.0 Alloy version: 2.0.1
alloy-skills
A comprehensive LLM skill for building blockchain solutions with Alloy — the next-generation Rust library for Ethereum and EVM-compatible chains.
Overview
This skill enables an LLM to accurately develop, debug, and reason about Rust applications that interact with EVM blockchains using the Alloy library. It covers the full stack: from signing a message to deploying contracts and verifying payments on-chain.
Installation
npx skills add melonask/alloy-skillsWhat This Skill Covers
| Category | Capabilities |
|---|---|
| Digital Signatures | EIP-191, EIP-712, EIP-2612 permits — all signer types (private key, mnemonic, Ledger, Trezor, AWS KMS, GCP KMS, YubiKey), keystore management |
| Payments | Native ETH transfers, ERC-20 token transfers, approve/transferFrom, permit2 gasless flows, gas estimation strategies, urgent/private transactions |
| Payment Verification | Receipt checking, real-time event log filtering, Transfer event monitoring, payment listener services, block confirmations, reconciliation |
| Smart Contracts | sol! macro for type-safe Solidity bindings, contract deployment (bytecode, artifact, Foundry), static vs dynamic ABI, revert decoding, multicall batching, library linking |
| Networking | HTTP / WebSocket / IPC providers, provider builder with layers (retry, fallback, logging, delay), batched RPC, multi-chain, authenticated connections |
| Subscriptions | Real-time block, log, and pending transaction streams, event multiplexing, poll-based fallback, ENS resolution |
| Primitives | Address, U256, B256, Bytes, parse_units/format_units, keccak256, ecrecover, type conversions |
| Local Nodes | Anvil (launch, fork, storage override, impersonation), Geth, Reth — testing patterns |
| Migration | Complete ethers-rs to Alloy migration guide with type mapping and code examples |
File Structure
alloy/
├── SKILL.md # Main skill — quick start, core patterns, reference index
└── references/
├── signatures-wallets.md # Signers, EIP-191/712, keystore, hardware wallets
├── transactions-payments.md # All tx types, ETH/ERC-20 transfers, gas control
├── payment-verification.md # Receipt verification, event monitoring, reconciliation
├── providers-networking.md # HTTP/WS/IPC, layers, batched RPC, multi-chain
├── contracts-abi.md # sol! macro, deployment, ABI encoding, multicall
├── subscriptions-events.md # Block/log/tx subscriptions, ENS, production patterns
├── primitives-types.md # Address, U256, hashing, conversions
├── node-bindings.md # Anvil, Geth, Reth, testing patterns
└── cargo-setup.md # Dependencies, feature flags, ethers-rs migrationQuick Example
use alloy::primitives::{address, U256};
use alloy::providers::{Provider, ProviderBuilder};
use alloy::signers::local::PrivateKeySigner;
use alloy::sol;
// Define ERC-20 interface
sol! {
#[sol(rpc)]
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
}
#[tokio::main]
async fn main() -> eyre::Result<()> {
let signer: PrivateKeySigner = "0x...".parse()?;
let wallet = alloy::network::EthereumWallet::from(signer.clone());
let provider = ProviderBuilder::new()
.wallet(wallet)
.connect_http("https://eth.llamarpc.com".parse()?);
let token = IERC20::new(
address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), // USDC
&provider,
);
let balance = token.balanceOf(signer.address()).call().await?;
println!("USDC balance: {}", balance);
Ok(())
}Requirements
[dependencies]
alloy = { version = "2.0", features = ["full"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
eyre = "0.6"License
Created for LLM-assisted blockchain development.
Cargo Setup & Migration
This guide covers dependency management, feature flags, crate selection patterns, and migrating from ethers-rs to Alloy.
Quick Dependency Patterns
Simple Script (Meta-crate)
For small scripts and quick prototypes where you want everything in one dependency:
[dependencies]
alloy = { version = "2.0", features = ["full"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
eyre = "0.6"Provider + Signer (Most Common)
For applications that send transactions:
[dependencies]
alloy-primitives = "2.0"
alloy-provider = { version = "2.0", features = ["reqwest"] }
alloy-signer-local = "2.0"
alloy-sol-types = "2.0"
alloy-network = { version = "2.0", features = ["ethereum"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
eyre = "0.6"Contract Interaction Only (No Signing)
For read-only applications (blockchain explorers, indexers, dashboards):
[dependencies]
alloy-primitives = "2.0"
alloy-provider = { version = "2.0", features = ["reqwest"] }
alloy-sol-types = "2.0"
alloy-network = { version = "2.0", features = ["ethereum"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }WebSocket Subscriptions
For real-time applications (bots, monitors, event listeners):
[dependencies]
alloy-primitives = "2.0"
alloy-provider = { version = "2.0", features = ["ws"] }
alloy-sol-types = "2.0"
alloy-network = { version = "2.0", features = ["ethereum"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
futures-util = "0.3"Hardware Wallet
For applications using Ledger or Trezor:
[dependencies]
alloy-primitives = "2.0"
alloy-provider = { version = "2.0", features = ["reqwest"] }
alloy-signer-ledger = "2.0"
# or: alloy-signer-trezor = "2.0"
alloy-sol-types = "2.0"
alloy-network = { version = "2.0", features = ["ethereum"] }Full Suite (All Features)
For complex applications that need everything:
[dependencies]
alloy = { version = "2.0", features = [
"full",
"signer-local",
"signer-ledger",
"signer-trezor",
"provider-http",
"provider-ws",
"provider-ipc",
"contract",
"network",
"node-bindings",
] }
tokio = { version = "1", features = ["full"] }Feature Flags Reference
alloy (meta-crate) Features
| Feature | Description |
|---|---|
full | Enables all common features |
consensus | Transaction types and consensus |
contract | High-level contract interaction |
network | Network abstractions |
providers | Provider implementations |
signers | All signer implementations |
transports | HTTP, WS, IPC transports |
sol-types | Solidity type bindings |
primitives | Core primitive types |
alloy-provider Features
| Feature | Description |
|---|---|
reqwest | HTTP transport via reqwest |
hyper | HTTP transport via hyper |
ws | WebSocket transport |
ipc | IPC transport |
alloy-signer Features
| Feature | Description |
|---|---|
local | Private key and mnemonic signers |
ledger | Ledger hardware wallet |
trezor | Trezor hardware wallet |
aws | AWS KMS |
gcp | Google Cloud KMS |
yubihsm | YubiKey HSM |
ethers-rs to Alloy Migration
High-Level Mapping
| ethers-rs | Alloy | Notes |
|---|---|---|
ethers::providers::Provider | alloy::providers::Provider | Different builder pattern |
ethers::signers::LocalWallet | alloy::signers::local::PrivateKeySigner | Different name |
ethers::signers::MnemonicBuilder | alloy::signers::local::MnemonicsBuilder | Similar API |
ethers::contract::Contract | alloy::contract::ContractInstance | sol! macro preferred |
ethers::abi::HumanReadableParser | sol! macro | Compile-time vs runtime |
ethers::utils::parse_units | alloy::primitives::utils::parse_units | Same API |
ethers::utils::format_units | alloy::primitives::utils::format_units | Same API |
ethers::utils::keccak256 | alloy::primitives::keccak256 | Same function |
ethers::types::Address | alloy::primitives::Address | Same type |
ethers::types::U256 | alloy::primitives::U256 | Same type |
ethers::types::Bytes | alloy::primitives::Bytes | Same type |
ethers::types::H256 | alloy::primitives::B256 | Renamed |
ethers::types::I256 | alloy::primitives::I256 | Same type |
Provider Migration
// ethers-rs
let provider = Provider::<Http>::try_from("https://rpc.example.com")?;
let wallet = "0x...".parse::<LocalWallet>()?.with_chain_id(1u64);
let client = SignerMiddleware::new(provider, wallet);
// Alloy
let signer: PrivateKeySigner = "0x...".parse()?;
let provider = ProviderBuilder::new()
// Replaces manually adding middleware
.wallet(wallet)
.connect_http("https://rpc.example.com".parse()?);Key differences:
- Alloy uses
ProviderBuilderwith a fluent API instead of middleware wrapping - Fillers (gas, nonce, chain ID) are included by default in
ProviderBuilder::new(). Use.disable_recommended_fillers()to opt out. - No need for
SignerMiddleware— the signer is added directly to the builder
Contract Migration
// ethers-rs
let abi = serde_json::from_str::<Abi>(&abi_str)?;
let contract = Contract::new(address, abi, client);
let result: U256 = contract.method("balanceOf", address)?.call().await?;
let tx = contract.method("transfer", (to, amount))?.send().await?;
// Alloy (preferred: sol! macro)
sol! {
#[sol(rpc)]
interface IERC20 {
function balanceOf(address) external view returns (uint256);
function transfer(address, uint256) external returns (bool);
}
}
let contract = IERC20::new(address, &provider);
let result = contract.balanceOf(address).call().await?;
let tx = contract.transfer(to, amount).send().await?;Signing Migration
// ethers-rs
let wallet: LocalWallet = "0x...".parse()?;
let sig = wallet.sign_message("hello").await?;
// Alloy
let signer: PrivateKeySigner = "0x...".parse()?;
let sig = signer.sign_message(b"hello").await?;Import Path Changes
// ethers-rs
use ethers::{types::*, providers::*, signers::*, utils::*};
// Alloy
use alloy::primitives::{Address, U256, B256, Bytes};
use alloy::providers::{Provider, ProviderBuilder};
use alloy::signers::{Signer, local::PrivateKeySigner};
use alloy::sol;Things That Changed Significantly
1. Contract interaction: Use the sol! macro instead of runtime ABI parsing. This gives compile-time type safety.
2. Middleware: Replaced by layers. Instead of wrapping providers in middleware, use .layer() in the builder.
3. Event decoding: Uses generated types from sol! instead of AbiDecode.
4. Transaction request: The builder API is slightly different. Use provider.send_transaction().to().value().finish() instead of constructing a TransactionRequest manually.
Contracts & ABI
This guide covers the sol! macro, contract deployment, contract interaction, ABI encoding/decoding, revert handling, and multi-call patterns.
The sol! Macro
The sol! macro is Alloy's primary interface for defining Solidity types in Rust. It generates type-safe bindings for function calls, events, errors, structs, and enums.
Basic Contract Interface
use alloy::sol;
sol! {
#[sol(rpc)]
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
}Attributes Explained
| Attribute | Effect |
|---|---|
#[sol(rpc)] | Generates both ABI types and an RPC-ready contract struct with new() |
#[sol(abi)] | Generates only ABI types (no RPC struct — used for deployment) |
#[sol(all_derives)] | Adds Debug, Clone, PartialEq, Eq, Hash derives to all types |
#[derive(Eip712Hash)] | Enables EIP-712 typed data signing for the struct |
Function Call Patterns
let token = IERC20::new(token_address, &provider);
// Read-only call (no gas, no on-chain state change)
let balance = token.balanceOf(my_address).call().await?;
// State-changing call (costs gas, changes state)
let pending_tx = token.transfer(recipient, amount)
.send() // Broadcast transaction
.await?;
// Wait for confirmation
let receipt = pending_tx.get_receipt().await?;
// Or watch (sends and waits in one step)
let receipt = token.transfer(recipient, amount)
.send()
.await?
.watch()
.await?;Events
// Emitting events is on-chain (contracts do this).
// In Rust, you decode events from logs:
let logs = provider.get_logs(&filter).await?;
for log in logs {
let transfer = IERC20::Transfer::decode_log(&log, true)?;
println!("Transfer: {} -> {} = {}", transfer.from, transfer.to, transfer.value);
}Contract Deployment
Deploy from sol! Definition
sol! {
#[sol(abi)]
contract SimpleStorage {
constructor(uint256 initial_value);
function value() external view returns (uint256);
function set_value(uint256 new_value) external;
}
}
// Compile your contract with `forge build` or `solc`
// The bytecode is in `out/SimpleStorage.sol/SimpleStorage.json`
// Load bytecode (you can embed it or load from file)
let bytecode_bytes = hex::decode("6080604052348015600f57600080fd5b5060405160...")?;
let bytecode = Bytes::from(bytecode_bytes);
// Deploy
let deploy_result = SimpleStorage::deploy_builder(&provider, U256::from(42))
.deploy()
.await?;
let contract_address = deploy_result.address();
let receipt = deploy_result.get_receipt().await?;
// Interact with deployed contract
let contract = SimpleStorage::new(contract_address, &provider);
let value = contract.value().call().await?;
assert_eq!(value, U256::from(42));Deploy from Artifact JSON (Foundry)
use std::fs;
// Load Foundry artifact
let artifact_json = fs::read_to_string("out/MyContract.sol/MyContract.json")?;
let artifact: serde_json::Value = serde_json::from_str(&artifact_json)?;
let bytecode_hex = artifact["bytecode"]["object"].as_str().unwrap();
let bytecode = hex::decode(bytecode_hex.replace("0x", ""))?;Deploy with Constructor Arguments
sol! {
#[sol(abi)]
contract Token {
constructor(string name, string symbol, uint8 decimals_);
function mint(address to, uint256 amount) external;
}
}
let deploy_result = Token::deploy_builder(
&provider,
"MyToken".to_string(),
"MTK".to_string(),
18u8,
)
.deploy()
.await?;Loading ABI from JSON
For interacting with existing contracts where you have the ABI JSON:
use alloy::contract::ContractInstance;
use alloy::json_abi::JsonAbi;
// Load ABI
let abi_json = r#"[
{"type":"function","name":"balanceOf","inputs":[{"name":"account","type":"address"}],"outputs":[{"name":"","type":"uint256"}],"stateMutability":"view"},
{"type":"function","name":"transfer","inputs":[{"name":"to","type":"address"},{"name":"amount","type":"uint256"}],"outputs":[{"name":"","type":"bool"}],"stateMutability":"nonpayable"}
]"#;
let abi: JsonAbi = serde_json::from_str(abi_json)?;Static vs Dynamic ABI
Alloy offers two ABI systems:
Static ABI (sol! macro — Preferred)
Type-safe at compile time. The compiler catches mismatches between your Solidity interface and how you use it in Rust.
sol! {
#[sol(rpc)]
interface MyContract {
function getValue() external view returns (uint256);
}
}
// Compile-time type safety
let result = contract.getValue().call().await?; // Returns U256 directlyDynamic ABI (runtime resolution)
Useful when the ABI is not known at compile time, or when building generic tools like explorers or multisig frontends.
use alloy::dyn_abi::{DynSolValue, DynSolType};
use alloy::json_abi::{JsonAbi, Function};
// Parse ABI at runtime
let abi: JsonAbi = serde_json::from_str(&abi_json)?;
// Call a function by name
let function: &Function = abi.function("balanceOf").unwrap();
// Encode parameters
let params = vec![
DynSolValue::Address(my_address),
];
let calldata = function.abi_encode_input(¶ms)?;
// Make the call
let result_bytes = provider.call().to(contract_address).calldata(calldata.into()).await?;
// Decode output
let outputs = function.abi_decode_output(&result_bytes, false)?;Revert Decoding
When a transaction reverts, Alloy can decode the revert reason:
sol! {
// Define custom errors from the contract
error InsufficientBalance(uint256 available, uint256 required);
error Unauthorized();
error AlreadyExists();
}
// After a reverted transaction
let receipt = pending_tx.get_receipt().await?;
if !receipt.status() {
// The revert reason is available through the provider
// You can decode it if you know the ABI
let result = contract.my_function().call().await;
match result {
Err(e) => {
// Check for known revert reasons
// The error message will include the revert data
println!("Revert reason: {}", e);
}
Ok(_) => {}
}
}Complex Types in sol!
Structs
sol! {
struct Order {
address maker;
address tokenIn;
address tokenOut;
uint128 amountIn;
uint128 amountOut;
uint256 deadline;
}
}Enums
sol! {
enum Status {
Pending,
Active,
Closed,
}
}Custom Errors
sol! {
error InsufficientFunds(uint256 required, uint256 available);
error NotOwner(address caller, address owner);
}Events with All Data
sol! {
event Swap(
address indexed sender,
uint256 amount0In,
uint256 amount1In,
uint256 amount0Out,
uint256 amount1Out,
address indexed to
);
}Multicall Batching
Bundle multiple contract calls into a single on-chain transaction for gas savings:
sol! {
#[sol(rpc)]
interface Multicall3 {
function aggregate3(Call3[] calldata calls)
external payable returns (Result[] memory returnData);
struct Call3 {
address target;
bool allowFailure;
bytes callData;
}
struct Result {
bool success;
bytes returnData;
}
}
}
// Multicall3 is deployed at the same address on every chain:
// 0xcA11bde05977b3631167028862bE2a173976CA11
let multicall = Multicall3::new(
address!("0xcA11bde05977b3631167028862bE2a173976CA11"),
&provider,
);
// Build individual calls
let call1 = token_a.balanceOf.encode_call((my_address,));
let call2 = token_b.balanceOf.encode_call((my_address,));
let calls = vec![
Call3 {
target: token_a_address,
allowFailure: false,
callData: call1,
},
Call3 {
target: token_b_address,
allowFailure: false,
callData: call2,
},
];
let results = multicall.aggregate3(calls).call().await?;
// Decode individual results
let balance_a = U256::from_be_slice(&results[0].returnData);
let balance_b = U256::from_be_slice(&results[1].returnData);Library Linking
When deploying contracts that use libraries, link the library addresses into the bytecode:
use alloy::contract::ContractInstance;
// 1. Deploy the library first
let lib_receipt = MyLibrary::deploy_builder(&provider).deploy().await?;
let lib_address = lib_receipt.address();
// 2. Link the library into the main contract bytecode
let linked_bytecode = main_bytecode.replace(
"__$LIBRARY_PLACEHOLDER$__",
&format!("{:x}", lib_address),
);
// 3. Deploy the main contract with linked bytecode
let main_receipt = MainContract::deploy_builder(&provider)
.from(linked_bytecode)
.deploy()
.await?;Interacting with Contract Instances
Once you have a contract instance, all defined functions are available as methods:
let token = IERC20::new(token_address, &provider);
// View functions (free, no gas)
let balance = token.balanceOf(addr).call().await?;
let decimals = token.decimals().call().await?;
let total_supply = token.totalSupply().call().await?;
// Write functions (cost gas)
let tx = token.approve(spender, amount).send().await?;
let receipt = tx.watch().await?;
// With explicit gas and value
let tx = token.somePayableFunction()
.gas(100_000)
.value(U256::from(1_ether))
.send()
.await?;Node Bindings
This guide covers integrating Alloy with local Ethereum nodes: Anvil (Foundry), Geth, and Reth.
Anvil (Foundry)
Anvil is Foundry's local Ethereum node, perfect for development and testing.
[dependencies]
alloy = { version = "2.0", features = ["node-bindings"] }Launch Anvil Programmatically
use alloy::node_bindings::Anvil;
use alloy::providers::{Provider, ProviderBuilder};
use alloy::signers::local::PrivateKeySigner;
use alloy::network::EthereumWallet;
// Launch with defaults (random accounts, port 8545)
let anvil = Anvil::new().spawn();
println!("RPC URL: {}", anvil.endpoint_url());
println!("Chain ID: {}", anvil.chain_id());
// Access default test accounts (10 accounts with 10000 ETH each)
let accounts = anvil.addresses();
println!("Account 0: {}", accounts[0]);
println!("Private key 0: {}", anvil.keys()[0]);
// Create a wallet from the first Anvil key
let signer: PrivateKeySigner = anvil.keys()[0].parse().unwrap();
let wallet = EthereumWallet::from(signer);
// Use the provider
let provider = ProviderBuilder::new()
.wallet(wallet)
.connect_http(anvil.endpoint_url());Fork Mainnet with Anvil
Test against real mainnet state without spending real ETH:
let anvil = Anvil::new()
.fork("https://eth.llamarpc.com")
.fork_block_number(19_000_000) // Pin to specific block
.spawn();
// Now you have mainnet state at block 19M
// All balances, contracts, and storage are available
let vitalik_balance = provider.get_balance(
address!("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
).await?;Set Storage (Manipulate Contract State)
Override storage slots for testing edge cases:
use alloy::primitives::{address, B256};
// Set a storage slot on a contract
anvil.set_storage_at(
contract_address,
storage_slot, // B256
new_value, // B256
)?;
// Now the contract's storage slot contains the new value
// Useful for testing: set token balances, modify protocol state, etc.Deploy Contracts on Anvil
// Anvil provides instant mining — transactions confirm immediately
let deploy_result = MyContract::deploy_builder(&provider, initial_value)
.deploy()
.await?;
let contract_address = deploy_result.address();Anvil Methods
| Method | Description |
|---|---|
Anvil::new() | Create builder with defaults |
.fork(url) | Fork a network at latest block |
.fork_block_number(n) | Fork at specific block |
.chain_id(id) | Set chain ID |
.block_time(secs) | Set block time interval |
.arg("--...") | Pass any anvil CLI argument |
.spawn() | Launch the node process |
.endpoint_url() | Get RPC URL (http://127.0.0.1:PORT) |
.addresses() | Get generated test accounts |
.keys() | Get private keys for test accounts |
.chain_id() | Get chain ID |
.set_storage_at(addr, slot, val) | Override contract storage |
Anvil Drop (Cleanup)
The Anvil instance automatically kills the process when dropped:
{
let anvil = Anvil::new().spawn();
// ... use anvil ...
} // Anvil process is killed hereGeth
Launch Geth Programmatically
use alloy::node_bindings::Geth;
use alloy::providers::{Provider, ProviderBuilder};
let geth = Geth::new()
.arg("--dev") // Dev mode (single validator, instant mining)
.spawn();
let provider = ProviderBuilder::new()
.connect_ipc(geth.ipc_path());Connect to Running Geth Instance
If Geth is already running, connect via IPC or HTTP:
// IPC (fastest, same machine)
let provider = ProviderBuilder::new()
.connect_ipc("~/.ethereum/geth.ipc")?;
// HTTP
let provider = ProviderBuilder::new()
.connect_http("http://127.0.0.1:8545".parse()?);Reth
Launch Reth Programmatically
use alloy::node_bindings::Reth;
use alloy::providers::{Provider, ProviderBuilder};
let reth = Reth::new()
.arg("--dev") // Dev mode
.spawn();
let provider = ProviderBuilder::new()
.connect_ipc(reth.ipc_path());Testing Patterns with Local Nodes
Pattern: Fresh Anvil Per Test
use alloy::node_bindings::Anvil;
use alloy::signers::local::PrivateKeySigner;
use alloy::network::EthereumWallet;
use alloy::providers::ProviderBuilder;
#[tokio::test]
async fn test_transfer() {
let anvil = Anvil::new().spawn();
let signer: PrivateKeySigner = anvil.keys()[0].parse().unwrap();
let wallet = EthereumWallet::from(signer);
let provider = ProviderBuilder::new()
.wallet(wallet)
.connect_http(anvil.endpoint_url());
let recipient = anvil.addresses()[1];
// Deploy token, transfer, verify — all on local anvil
let token = deploy_token(&provider).await.unwrap();
let receipt = token.transfer(recipient, amount).send().await.unwrap().watch().await.unwrap();
assert!(receipt.status());
}Pattern: Fork Testing
use alloy::node_bindings::Anvil;
use alloy::signers::local::PrivateKeySigner;
use alloy::network::EthereumWallet;
use alloy::providers::ProviderBuilder;
#[tokio::test]
async fn test_uniswap_swap() {
// Fork mainnet so Uniswap contracts exist
let anvil = Anvil::new()
.fork("https://eth.llamarpc.com")
.fork_block_number(19_000_000)
.spawn();
let signer: PrivateKeySigner = anvil.keys()[0].parse().unwrap();
let wallet = EthereumWallet::from(signer);
let provider = ProviderBuilder::new()
.wallet(wallet)
.connect_http(anvil.endpoint_url());
// Interact with real Uniswap contracts on forked state
// The test account has 10000 ETH from Anvil defaults
}Pattern: Impersonate Accounts
Anvil allows you to impersonate any address, useful for testing with real mainnet holders:
// Use anvil_impersonateAccount RPC to send transactions as any address
// This is done through the provider's raw RPC call capability
let impersonated = address!("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"); // vitalik
// Set balance for impersonated account
anvil.set_balance(impersonated, U256::from(1000_ether))?;Choosing a Local Node
| Node | Best For | Start Time | Features |
|---|---|---|---|
| Anvil | Testing, forks | Instant | Fork, impersonation, storage override |
| Geth | Production-like | Slow | Full consensus, real networking |
| Reth | Production, Rust native | Medium | Full consensus, Rust ecosystem |
For most development and testing, use Anvil. It starts instantly and has powerful testing features like forking and storage manipulation.
Payment Verification
This guide covers verifying that incoming payments were received correctly, monitoring for specific transfers, parsing on-chain events, and building robust payment reconciliation systems.
Verify Transaction Receipt
The most basic verification: check that a transaction succeeded and extract relevant data.
use alloy::providers::Provider;
use eyre::Context; // Provides `ok_or_eyre`
let tx_hash: B256 = "0x...".parse()?;
// Get the receipt
let receipt = provider.get_transaction_receipt(tx_hash).await?
.ok_or_eyre("Transaction not found")?;
// Check success
if receipt.status() {
println!("Transaction succeeded");
println!("Gas used: {}", receipt.gas_used);
println!("Block number: {}", receipt.block_number.unwrap());
println!("Contract address: {:?}", receipt.contract_address);
} else {
println!("Transaction REVERTED");
// Revert reason is encoded in receipt if the node supports it
}Monitor Incoming ETH Transfers
Watch for ETH transfers to a specific address by subscribing to new blocks and checking internal transactions, or by monitoring the mempool.
Approach 1: Poll Pending Transactions (Simple)
use alloy::providers::Provider;
use alloy::primitives::Address;
let my_address: Address = "0xMyAddress".parse()?;
let provider = /* ... */;
// Poll for latest block and check transactions
let block = provider.get_block_by_number(BlockNumber::Latest, false).await?;
if let Some(block) = block {
for tx in &block.transactions {
// For full transactions
if let Some(to) = tx.to() {
if to == my_address && tx.value() > U256::ZERO {
println!(
"Received {} wei from {} in tx {}",
tx.value(),
tx.from(),
tx.hash
);
}
}
}
}Approach 2: Subscribe to New Blocks (Real-time)
use alloy::providers::{Provider, ProviderBuilder};
use alloy::primitives::Address;
use futures_util::StreamExt;
let my_address: Address = "0xMyAddress".parse()?;
// Subscribe to new block headers
let mut block_stream = provider.watch_blocks().await?;
while let Some(block_hash) = block_stream.next().await {
let block = provider.get_block_by_hash(block_hash, true.into()).await?;
if let Some(block) = block {
for tx in &block.transactions {
if let Some(to) = tx.to() {
if to == my_address {
println!("Incoming ETH: {} from {}", tx.value(), tx.from());
}
}
}
}
}Monitor ERC-20 Transfers via Event Logs
This is the most reliable approach for monitoring token payments. ERC-20 contracts emit Transfer events for every transfer.
Filter Logs for Specific Transfers
use alloy::providers::{Provider, ProviderBuilder};
use alloy::primitives::{address, Address, Log, B256};
use alloy::sol;
sol! {
event Transfer(address indexed from, address indexed to, uint256 value);
}
let token_address = address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC
let my_address: Address = "0xMyAddress".parse()?;
// Get Transfer event signature: keccak256("Transfer(address,address,uint256)")
let transfer_topic = B256::from_slice(
&alloy::primitives::keccak256(b"Transfer(address,address,uint256)")
);
// Build log filter: watch for Transfer events where `to` is my address
let filter = Filter::new()
.address(token_address)
.event_signature(transfer_topic)
.topic1(my_address); // `to` is the second indexed parameter (topic1)
// Get past logs
let logs = provider.get_logs(&filter).await?;
for log in logs {
// Decode the event
let transfer = Transfer::decode_log(&log)?;
println!(
"Received {} tokens from {} (tx: {})",
transfer.value, transfer.from, log.transaction_hash.unwrap()
);
}Subscribe to Transfer Events (Real-time)
use futures_util::StreamExt;
let filter = Filter::new()
.address(token_address)
.event_signature(transfer_topic)
.topic1(my_address);
let mut log_stream = provider.subscribe_logs(&filter).await?;
while let Some(log) = log_stream.next().await {
let transfer = Transfer::decode_log(&log)?;
println!(
"NEW PAYMENT: {} tokens from {} in tx {}",
transfer.value,
transfer.from,
log.transaction_hash.unwrap()
);
}Monitor Multiple Tokens
Watch for transfers of any token to your address:
// Watch all Transfer events to your address (any token contract)
let filter = Filter::new()
.topic1(my_address) // `to` parameter
// Or watch specific tokens
let tokens = vec![
address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), // USDC
address!("0xdAC17F958D2ee523a2206206994597C13D831ec7"), // USDT
address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), // WETH
];
let filter = Filter::new()
.address(tokens)
.event_signature(transfer_topic)
.topic1(my_address);Verify a Specific Payment
Given a transaction hash, verify that it contains the expected payment:
use alloy::providers::Provider;
use alloy::primitives::{address, U256, B256};
async fn verify_erc20_payment(
provider: &impl Provider,
tx_hash: B256,
expected_token: Address,
expected_to: Address,
expected_from: Address,
expected_amount: U256,
) -> eyre::Result<bool> {
// Get the receipt
let receipt = provider.get_transaction_receipt(tx_hash).await?
.ok_or_eyre("Transaction not found")?;
// Must be successful
if !receipt.status() {
return Ok(false);
}
// Get the Transfer event signature
let transfer_topic = B256::from_slice(
&alloy::primitives::keccak256(b"Transfer(address,address,uint256)")
);
// Check logs for matching Transfer event
for log in &receipt.inner.logs {
if log.address != expected_token {
continue;
}
if log.topics.is_empty() || log.topics[0] != transfer_topic {
continue;
}
// Check `from` (topic1) and `to` (topic2)
let from = Address::from_word(log.topics.get(1).copied().unwrap_or_default());
let to = Address::from_word(log.topics.get(2).copied().unwrap_or_default());
if from != expected_from || to != expected_to {
continue;
}
// Decode amount from data
let amount = U256::from_be_slice(&log.data.data);
if amount == expected_amount {
return Ok(true);
}
}
Ok(false)
}Build a Payment Listener Service
A complete pattern for building a service that listens for incoming payments and triggers callbacks:
use alloy::providers::{Provider, ProviderBuilder};
use alloy::primitives::{Address, Filter, B256, U256};
use alloy::sol;
use std::collections::HashMap;
use tokio::sync::mpsc;
sol! {
event Transfer(address indexed from, address indexed to, uint256 value);
}
struct PaymentListener {
provider: Arc<impl Provider>,
monitored_tokens: HashMap<Address, u8>, // token -> decimals
watched_address: Address,
}
impl PaymentListener {
async fn start(&self, tx: mpsc::Sender<PaymentEvent>) -> eyre::Result<()> {
let transfer_topic = B256::from_slice(
&alloy::primitives::keccak256(b"Transfer(address,address,uint256)")
);
let token_addresses: Vec<Address> = self.monitored_tokens.keys().copied().collect();
let filter = Filter::new()
.address(token_addresses)
.event_signature(transfer_topic)
.topic1(self.watched_address);
let mut stream = self.provider.subscribe_logs(&filter).await?;
while let Some(log) = stream.next().await {
let transfer = Transfer::decode_log(&log)?;
let decimals = self.monitored_tokens.get(&log.address).copied().unwrap_or(18);
let formatted = alloy::primitives::utils::format_units(transfer.value, decimals)?;
let event = PaymentEvent {
token: log.address,
from: transfer.from,
to: transfer.to,
amount: transfer.value,
formatted_amount: formatted,
tx_hash: log.transaction_hash.unwrap(),
block_number: log.block_number.unwrap(),
};
tx.send(event).await?;
}
Ok(())
}
}
struct PaymentEvent {
token: Address,
from: Address,
to: Address,
amount: U256,
formatted_amount: String,
tx_hash: B256,
block_number: u64,
}Verify Payment with Block Confirmations
For high-value payments, wait for multiple block confirmations before considering a payment final:
async fn verify_with_confirmations(
provider: &impl Provider,
tx_hash: B256,
required_confirmations: u64,
) -> eyre::Result<bool> {
let receipt = provider.get_transaction_receipt(tx_hash).await?
.ok_or_eyre("Not found")?;
if !receipt.status() {
return Ok(false);
}
let tx_block = receipt.block_number.unwrap();
let latest_block = provider.get_block_number().await?;
if latest_block >= tx_block + required_confirmations {
Ok(true) // Sufficiently confirmed
} else {
Ok(false) // Not enough confirmations yet
}
}Payment Reconciliation Pattern
Compare on-chain data against your expected payments:
async fn reconcile_payments(
provider: &impl Provider,
token_address: Address,
my_address: Address,
expected_payments: Vec<(Address, U256)>, // (from, amount)
) -> eyre::Result<Vec<PaymentStatus>> {
let token = IERC20::new(token_address, provider);
let mut statuses = Vec::new();
for (from, expected_amount) in expected_payments {
let actual_balance = token.balanceOf(my_address).call().await?;
// Check logs for specific transfer from this sender
let filter = Filter::new()
.address(token_address)
.topic1(from)
.topic2(my_address);
let logs = provider.get_logs(&filter).await?;
let total_received: U256 = logs.iter()
.filter_map(|log| {
let transfer = Transfer::decode_log(log).ok()?;
Some(transfer.value)
})
.sum();
statuses.push(PaymentStatus {
from,
expected: expected_amount,
received: total_received,
confirmed: total_received >= expected_amount,
});
}
Ok(statuses)
}Primitives & Types
This guide covers Alloy's core primitive types: addresses, hashes, bytes, big numbers, conversion utilities, and hashing functions.
Address
The 20-byte Ethereum address type. This is the most common type you will work with.
use alloy::primitives::{address, Address};
// From hex literal
let addr = address!("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
// From hex string
let addr: Address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045".parse()?;
// From bytes
let addr = Address::from([0xd8, 0xdA, 0x6B, 0xF2, 0x69, 0x64, 0xaF, 0x9D,
0x7e, 0xed, 0x9e, 0x03, 0xe5, 0x34, 0x15, 0xd3, 0x7a, 0xa9, 0x60, 0x45]);
// Zero address (useful for checks)
let zero = Address::ZERO;
// Display
println!("{}", addr); // 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
// Checksummed (EIP-55)
println!("{:#x}", addr); // 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045FixedBytes and B256
Fixed-size byte arrays used throughout Ethereum:
use alloy::primitives::{B256, B64, B32, B20, FixedBytes};
// B256 is a 32-byte hash (used for block hashes, tx hashes, keccak output)
let hash: B256 = "0x0000000000000000000000000000000000000000000000000000000000000001".parse()?;
// B64 (used for signatures)
let sig_bytes: B64 = "0x0000000000000000000000000000000000000000000000000000000000000001".parse()?;
// Create from array
let hash = B256::from([1u8; 32]);
// Access individual bytes
let first_byte = hash[0];
// Convert to/from slice
let slice: &[u8] = hash.as_slice();Bytes (Dynamic)
Variable-length byte arrays:
use alloy::primitives::Bytes;
// From hex
let data: Bytes = "0xdeadbeef".parse()?;
// From vector
let data = Bytes::from(vec![0xde, 0xad, 0xbe, 0xef]);
// From slice
let data = Bytes::from_static(b"hello");
// Access
println!("Length: {}", data.len());
println!("First byte: {:02x}", data[0]);U256 — 256-bit Unsigned Integer
The most important numeric type for Ethereum. Used for balances, amounts, gas, nonces, and all on-chain values.
use alloy::primitives::{U256, Uint};
// Creation
let zero = U256::ZERO;
let one = U256::ONE;
let max = U256::MAX;
let from_u64 = U256::from(1_000_000u64);
let from_u128 = U256::from(1_000_000_000_000_000_000u128);
let from_str: U256 = "1000000000000000000".parse()?;
// Arithmetic (all return U256)
let sum = a + b;
let diff = a - b; // Panics on underflow; use .checked_sub() for safe math
let product = a * b;
let quotient = a / b;
let remainder = a % b;
// Safe arithmetic
let safe_diff = a.checked_sub(b); // Returns Option<U256>
let safe_add = a.checked_add(b); // Returns Option<U256>
let safe_mul = a.checked_mul(b); // Returns Option<U256>
// Comparison
if balance > U256::ZERO { /* ... */ }
assert_eq!(a, b);
// Display
println!("{}", amount); // Decimal
println!("{:#x}", amount); // Hex with 0x prefixparse_units and format_units
Convert between human-readable token amounts and their raw on-chain representation:
use alloy::primitives::utils::{parse_units, format_units};
// parse_units: human-readable -> raw (smallest unit)
// format_units: raw -> human-readable
// ETH: 18 decimals
let eth_raw = parse_units("1.5", 18)?; // -> U256 = 1500000000000000000
let eth_display = format_units(eth_raw, 18)?; // -> "1.5"
// USDC: 6 decimals
let usdc_raw = parse_units("100.50", 6)?; // -> U256 = 100500000
let usdc_display = format_units(usdc_raw, 6)?; // -> "100.5"
// USDT: 6 decimals
let usdt_raw = parse_units("0.001", 6)?; // -> U256 = 1000
let usdt_display = format_units(usdt_raw, 6)?; // -> "0.001"
// Parse to specific Rust type
let raw_u128: u128 = parse_units("1.0", 18)?.into();
// Format with precision
let display = format_units(U256::from(123456789u64), 18)?;
// -> "0.000000000123456789"Keccak-256 Hashing
The primary hash function used in Ethereum:
use alloy::primitives::keccak256;
// Hash bytes
let hash = keccak256(b"hello");
println!("keccak256('hello') = {}", hash);
// Hash strings
let hash = keccak256("Hello, Ethereum!".as_bytes());
// Hash for event signatures (used in log filtering)
let transfer_sig = keccak256(b"Transfer(address,address,uint256)");
let approval_sig = keccak256(b"Approval(address,address,uint256)");ECDSA Recover
Recover an address from a message and signature (EIP-191):
use alloy::primitives::B256;
use alloy::signer::Signature;
let signature: Signature = /* from signer.sign_message() */;
// Recover address from EIP-191 signed message
let recovered = signature.recover_address_from_msg("hello")?;
println!("Signer address: {}", recovered);
// Recover address from hash + raw signature
let hash: B256 = keccak256(b"hello");
let recovered = signature.recover_address_from_hash(&hash)?;Type Conversions
Between U256, u64, u128, f64
let u256_val = U256::from(1_000_000u64);
let u64_val: u64 = u256_val.try_into()?; // Fails if > u64::MAX
let u128_val: u128 = u256_val.try_into()?;
let u256_back = U256::from(u64_val);
// To/from string
let s = u256_val.to_string();
let parsed: U256 = s.parse()?;Between Address and B256
let addr: Address = address!("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
// Address -> B256 (zero-padded)
let hash: B256 = addr.into_word();
// B256 -> Address (takes last 20 bytes)
let addr2: Address = Address::from_word(hash.into());Bytes Encoding
use alloy::primitives::{Bytes, B256};
// U256 to bytes
let bytes = amount.to_be_bytes::<32>(); // [u8; 32]
let bytes_vec = amount.to_be_bytes_vec();
// Bytes to U256
let amount = U256::from_be_slice(&bytes);
// Address to bytes
let addr_bytes: [u8; 20] = address.into();Common Constants
use alloy::primitives::{Address, U256, B256};
// Addresses
let zero_address = Address::ZERO;
// U256
let zero = U256::ZERO;
let one = U256::ONE;
let max_u256 = U256::MAX; // 2^256 - 1
let max_u128 = U256::from(u128::MAX);
// Useful amounts (ETH in wei)
let one_ether = U256::from(1_000_000_000_000_000_000u128); // 10^18
let one_gwei = U256::from(1_000_000_000u128); // 10^9
// Using parse_units for precision
let one_eth = parse_units("1", 18)?.into::<U256>();
let one_gwei = parse_units("1", 9)?.into::<U256>();Providers & Networking
This guide covers RPC provider setup (HTTP, WebSocket, IPC), the provider builder, middleware layers, batched RPC calls, and advanced provider patterns.
Provider Overview
A provider is your gateway to the blockchain. It sends JSON-RPC requests to an Ethereum node and returns typed responses. All blockchain interactions go through a provider.
Quick Setup (HTTP)
The simplest way to create a provider:
use alloy::providers::{Provider, ProviderBuilder};
// From URL string
let provider = ProviderBuilder::new()
.connect_http("https://eth.llamarpc.com".parse()?);
// From Url
let rpc_url = url::Url::parse("https://mainnet.infura.io/v3/YOUR_KEY")?;
let provider = ProviderBuilder::new().connect_http(rpc_url);Provider with Signer and Fillers
Most applications need a signer (to send transactions) and fillers (to auto-populate gas, nonce, chain ID):
use alloy::providers::{Provider, ProviderBuilder};
use alloy::signers::local::PrivateKeySigner;
let signer: PrivateKeySigner = "0x...".parse()?;
let provider = ProviderBuilder::new()
// Gas, Nonce, ChainId fillers
.wallet(wallet) // Add signing capability
.connect_http("https://eth.llamarpc.com".parse()?);Recommended fillers are already included when using ProviderBuilder::new(). If you need to opt out, use .disable_recommended_fillers().
1. ChainIdFiller — queries the node for chain ID and sets it on every transaction 2. NonceFiller — manages nonce tracking to prevent replay and conflicts 3. GasFiller — estimates gas price and gas limit for each transaction
⚠️ NonceFiller Concurrency Warning
If your settlement workers run with concurrency > 1 (e.g., apalis with .concurrency(4)), the default NonceFiller can cause race conditions. Multiple threads may query eth_getTransactionCount simultaneously before transactions hit the mempool, receiving the same nonce and causing "nonce too low" errors.
Solutions: 1. Restrict to concurrency(1) for settlement workers 2. Implement a local `Mutex<u64>` nonce manager that atomic-increments in memory before signing 3. Redis-backed nonce for distributed multi-instance deployments
See known-issues.md for detailed fix patterns.
WebSocket Provider
WebSocket connections are essential for subscriptions (blocks, logs, pending transactions). HTTP providers can only poll.
use alloy::providers::ProviderBuilder;
use alloy::transports::http::Http;
// WS provider with reconnect
let ws_url = "wss://eth-mainnet.g.alchemy.com/v2/YOUR_KEY";
let provider = ProviderBuilder::new()
.wallet(wallet)
.on_ws(ws_url.parse()?);WS with Authentication
// Bearer token auth
let provider = ProviderBuilder::new()
.on_ws_with_auth(
"wss://node.example.com/ws".parse()?,
Auth::Bearer("your-token".into()),
);IPC Provider
For lowest latency when running a local node (Geth, Reth) on the same machine:
let provider = ProviderBuilder::new()
.connect_ipc("/path/to/geth.ipc")?;Provider Builder Pattern
The ProviderBuilder supports a fluent API for composing layers:
let provider = ProviderBuilder::new()
// Layer order: bottom to top (transport is innermost)
.layer(logging_layer) // Optional: log all requests
.layer(retry_layer) // Optional: retry failed requests
.wallet(wallet)
.connect_http(rpc_url);Layers (Middleware)
Layers wrap the transport and add cross-cutting concerns like retry, logging, and rate limiting.
Retry Layer
Automatically retries failed RPC requests with exponential backoff:
use alloy::provider::layers::RetryLayer;
let provider = ProviderBuilder::new()
.layer(RetryLayer::new(3)) // Max 3 retries
.wallet(wallet)
.connect_http(rpc_url);Fallback Layer
Try multiple RPC endpoints in order. If the first fails, try the next:
use alloy::provider::layers::FallbackLayer;
use alloy::transports::http::{Http, Client};
let http1 = Http::<Client>::new("https://rpc1.example.com".parse()?);
let http2 = Http::<Client>::new("https://rpc2.example.com".parse()?);
let http3 = Http::<Client>::new("https://rpc3.example.com".parse()?);
let provider = ProviderBuilder::new()
.layer(FallbackLayer::new(vec![http1, http2, http3]))
.wallet(wallet)
.connect_http("https://rpc-primary.example.com".parse()?);Logging Layer
Log all RPC requests and responses for debugging:
use alloy::provider::layers::LoggingLayer;
let provider = ProviderBuilder::new()
.layer(LoggingLayer::default())
.wallet(wallet)
.connect_http(rpc_url);Custom Delay Layer
Add artificial delay between requests (rate limiting):
use std::time::Duration;
use tower::ServiceBuilder;
let provider = ProviderBuilder::new()
.layer(tower::ServiceBuilder::new()
.delay(Duration::from_millis(100)))
.wallet(wallet)
.connect_http(rpc_url);Batched RPC Calls
Execute multiple RPC calls in a single HTTP request for efficiency:
use alloy::providers::ProviderBuilder;
let provider = ProviderBuilder::new()
.connect_http("https://eth.llamarpc.com".parse()?);
// Multiple independent queries in one batch
let (block_number, gas_price, balance) = tokio::join!(
provider.get_block_number(),
provider.get_gas_price(),
provider.get_balance(my_address),
);
println!("Block: {}, Gas: {}, Balance: {}", block_number?, gas_price?, balance?);HTTP with Authentication
Some RPC providers require API keys or authentication headers:
use alloy::transports::http::{Http, hyper::Request, hyper::header};
let mut url = "https://mainnet.infura.io/v3/YOUR_KEY".parse::<url::Url>()?;
// Add custom headers
let provider = ProviderBuilder::new()
.connect_http(url);Multi-Chain Provider
When working with multiple chains, create separate providers:
let eth_provider = ProviderBuilder::new()
.wallet(eth_signer)
.connect_http("https://eth.llamarpc.com".parse()?);
let arb_provider = ProviderBuilder::new()
.wallet(arb_signer)
.connect_http("https://arb1.arbitrum.io/rpc".parse()?);
let base_provider = ProviderBuilder::new()
.wallet(base_signer)
.connect_http("https://mainnet.base.org".parse()?);Dynamic Provider (Runtime Chain Selection)
For applications that connect to different chains at runtime:
use alloy::providers::DynProvider;
fn create_provider(rpc_url: &str) -> DynProvider {
let provider = ProviderBuilder::new()
.connect_http(rpc_url.parse().unwrap());
DynProvider::new(provider)
}Common Provider Methods
| Method | Returns | Description |
|---|---|---|
get_block_number() | u64 | Latest block number |
get_block_by_number(n, full) | Option<Block> | Block by number |
get_block_by_hash(hash, full) | Option<Block> | Block by hash |
get_transaction(hash) | Transaction | Transaction details |
get_transaction_receipt(hash) | Option<Receipt> | Transaction receipt |
get_balance(address) | U256 | ETH balance |
get_transaction_count(address) | u64 | Nonce |
get_gas_price() | u128 | Current gas price |
get_code(address) | Bytes | Contract bytecode |
get_storage_at(address, slot) | B256 | Storage slot value |
call(request) | Bytes | Eth_call (read-only) |
estimate_gas(request) | u128 | Gas estimation |
send_transaction(request) | PendingTx | Broadcast transaction |
get_logs(filter) | Vec<Log> | Query event logs |
watch_blocks() | Stream<B256> | Subscribe to new blocks |
subscribe_logs(filter) | Stream<Log> | Subscribe to events |
Error Handling
use alloy::transports::TransportError;
match provider.get_balance(address).await {
Ok(balance) => println!("Balance: {}", balance),
Err(TransportError::ErrorResp(resp)) => {
eprintln!("RPC error: {} - {}", resp.code, resp.message);
}
Err(TransportError::HttpError(e)) => {
eprintln!("HTTP error: {}", e);
}
Err(e) => {
eprintln!("Other error: {}", e);
}
}Signatures & Wallets
This guide covers all signer types, message signing, EIP-191 personal sign, EIP-712 typed data, signature verification, keystore creation, and hardware wallet setup.
Private Key Signer
The simplest signer for testing and scripts:
use alloy::signers::local::PrivateKeySigner;
use alloy::signers::Signer;
let signer: PrivateKeySigner = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
.parse()?;
let address = signer.address();
println!("Address: {}", address);Sign a Message (EIP-191)
let message = "Hello, Ethereum!";
let signature = signer.sign_message(message.as_bytes()).await?;
println!("Signature: 0x{}", signature);
// Verify
let recovered = signature.recover_address_from_msg(message)?;
assert_eq!(recovered, signer.address());Sign Raw Bytes
let data = b"raw data to sign";
let signature = signer.sign_message(data).await?;Wallet Creation
EthereumWallet from Signer
use alloy::network::EthereumWallet;
use alloy::signers::local::PrivateKeySigner;
let signer: PrivateKeySigner = "0x...".parse()?;
let wallet = EthereumWallet::from(signer.clone());Signature Verification
Recover Address from Message
use alloy::sol_types::SolEvent;
use alloy::primitives::B256;
let signature = signer.sign_message(b"hello").await?;
// EIP-191 verification
let recovered = signature.recover_address_from_msg("hello")?;
assert_eq!(recovered, signer.address());Recover from Hash
use alloy::primitives::{keccak256, B256};
let hash: B256 = keccak256(b"hello");
let recovered = signature.recover_address_from_hash(&hash)?;EIP-712 Typed Data Signing
EIP-712 provides structured data signing for better UX and security:
use alloy::sol;
use alloy::signers::local::PrivateKeySigner;
sol! {
struct MyMessage {
string content;
uint256 timestamp;
}
}
let signer: PrivateKeySigner = "0x...".parse()?;
let message = MyMessage {
content: "Hello".to_string(),
timestamp: U256::from(1234567890),
};
// Sign EIP-712 typed data
let domain = alloy::eip712_domain! {
name: "MyApp",
version: "1",
chain_id: 1,
};
let signature = signer.sign_typed_data(&message, &domain).await?;Keystore File
Create a Keystore
use alloy::signers::local::PrivateKeySigner;
use std::fs;
let signer = PrivateKeySigner::random();
let keystore = signer.encrypt_keystore("./keystores", "password")?;
println!("Keystore created: {}", keystore);Load from Keystore
let signer = PrivateKeySigner::decrypt_keystore("./keystores/UTC--...", "password")?;Mnemonic (BIP-39)
Create from Mnemonic
use alloy::signers::local::PrivateKeySigner;
let signer = PrivateKeySigner::from_phrase(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
None, // password
)?;Generate Random Wallet
let signer = PrivateKeySigner::random();
let address = signer.address();
println!("Random address: {}", address);Hardware Wallets
Ledger
[dependencies]
alloy-signer-ledger = "2.0"use alloy::signers::ledger::LedgerSigner;
let signer = LedgerSigner::new().await?;
println!("Ledger address: {}", signer.address());
let signature = signer.sign_message(b"test").await?;Trezor
[dependencies]
alloy-signer-trezor = "2.0"use alloy::signers::trezor::TrezorSigner;
let signer = TrezorSigner::new().await?;Cloud KMS Signers
AWS KMS
[dependencies]
alloy-signer-aws = "2.0"use alloy::signers::aws::AwsSigner;
let signer = AwsSigner::new(
aws_config,
key_id,
Some(chain_id),
).await?;GCP KMS
[dependencies]
alloy-signer-gcp = "2.0"use alloy::signers::gcp::GcpSigner;
let signer = GcpSigner::new(
project_id,
location,
key_ring,
key_name,
version,
).await?;Signer Trait (All Signers Implement This)
use alloy::signers::Signer;
async fn sign_with_any_signer(signer: &impl Signer, message: &[u8]) -> eyre::Result<()> {
let signature = signer.sign_message(message).await?;
let recovered = signature.recover_address_from_msg(message)?;
assert_eq!(recovered, signer.address());
Ok(())
}Signing a Transaction
use alloy::signers::local::PrivateKeySigner;
use alloy::network::EthereumWallet;
use alloy::providers::ProviderBuilder;
use alloy::primitives::U256;
use alloy::rpc::types::TransactionRequest;
use alloy::network::TransactionBuilder;
let signer: PrivateKeySigner = "0x...".parse()?;
let wallet = EthereumWallet::from(signer.clone());
let provider = ProviderBuilder::new()
.wallet(wallet)
.connect_http("https://ethereum-rpc.publicnode.com".parse()?);
let tx = TransactionRequest::default()
.to(address!("0x..."))
.value(U256::from(1u128))
.with_input(Bytes::new());
// The provider signs and broadcasts
let pending = provider.send_transaction(tx).await?;Permits (EIP-2612)
sol! {
interface IERC20Permit {
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external;
}
}Test Utilities
Anvil Test Accounts
use alloy::node_bindings::Anvil;
use alloy::providers::{Provider, ProviderBuilder};
let anvil = Anvil::new().spawn();
let accounts = anvil.addresses();
let signer: PrivateKeySigner = anvil.keys()[0].parse()?;Subscriptions & Events
This guide covers real-time blockchain monitoring: subscribing to new blocks, filtering event logs, watching pending transactions, and building event-driven applications.
Overview
Subscriptions require a WebSocket or IPC connection. HTTP providers can only poll (request-response). Always use .on_ws() or .on_ipc() when you need real-time data.
use alloy::providers::ProviderBuilder;
// WS is required for subscriptions
let provider = ProviderBuilder::new()
.on_ws("wss://eth-mainnet.g.alchemy.com/v2/YOUR_KEY".parse()?);Subscribe to New Blocks
Get notified immediately when a new block is produced:
use futures_util::StreamExt;
let mut block_stream = provider.watch_blocks().await?;
while let Some(block_hash) = block_stream.next().await {
println!("New block: {}", block_hash);
// Get full block details if needed
let block = provider.get_block_by_hash(block_hash, true.into()).await?;
if let Some(block) = block {
println!(" Transactions: {}", block.transactions.len());
println!(" Gas used: {}", block.gas_used);
println!(" Timestamp: {}", block.timestamp);
}
}Subscribe to Pending Transactions
Monitor the mempool for incoming transactions before they are confirmed:
use futures_util::StreamExt;
let mut tx_stream = provider.subscribe_pending_transactions().await?;
while let Some(tx_hash) = tx_stream.next().await {
println!("Pending tx: {}", tx_hash);
// Get full transaction details
if let Some(tx) = provider.get_transaction_by_hash(tx_hash).await? {
if tx.to() == Some(my_address) {
println!(" Incoming payment of {} ETH from {}!", tx.value(), tx.from());
}
}
}Filter and Subscribe to Logs
Log subscriptions are the primary way to monitor contract events in real-time.
Basic Log Filter
use alloy::primitives::{address, Address, B256};
use alloy::rpc::types::Filter;
use futures_util::StreamExt;
let token_address = address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
// Transfer event signature
let transfer_topic = B256::from_slice(
&alloy::primitives::keccak256(b"Transfer(address,address,uint256)")
);
// Build filter
let filter = Filter::new()
.address(token_address)
.event_signature(transfer_topic);
let mut log_stream = provider.subscribe_logs(&filter).await?;
while let Some(log) = log_stream.next().await {
println!("Transfer event in tx: {:?}", log.transaction_hash);
}Filter by Indexed Parameters
Solidity events can have up to 3 indexed parameters, stored as topic1, topic2, topic3 in the log:
// Watch for transfers TO a specific address
let filter = Filter::new()
.address(token_address)
.event_signature(transfer_topic)
.topic1(my_address); // `to` parameter
// Watch for transfers FROM a specific address
let filter = Filter::new()
.address(token_address)
.event_signature(transfer_topic)
.topic0(from_address); // `from` parameter
// Watch for transfers between specific addresses
let filter = Filter::new()
.address(token_address)
.event_signature(transfer_topic)
.topic0(from_address)
.topic1(to_address);
// Watch for any value above a threshold (value is NOT indexed, so use data field)
// Note: non-indexed parameters cannot be filtered at the node level — filter in codeFilter by Block Range
For historical queries (not subscriptions):
let filter = Filter::new()
.address(token_address)
.from_block(BlockNumber::Number(18_000_000))
.to_block(BlockNumber::Number(18_100_000));
let logs = provider.get_logs(&filter).await?;Multi-Token Log Watcher
let tokens = vec![
address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), // USDC
address!("0xdAC17F958D2ee523a2206206994597C13D831ec7"), // USDT
address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), // WETH
];
let filter = Filter::new()
.address(tokens)
.event_signature(transfer_topic)
.topic1(my_address); // Only transfers TO meEvent Multiplexer
Combine multiple event subscriptions into one stream:
use futures_util::StreamExt;
use std::collections::HashMap;
let mut block_stream = provider.watch_blocks().await?;
// Create log filters for different event types
let transfer_filter = Filter::new()
.address(token_address)
.event_signature(transfer_topic);
let approval_filter = Filter::new()
.address(token_address)
.event_signature(approval_topic);
let mut log_stream = provider.subscribe_logs(&transfer_filter).await?;
// Combine into one loop using select
tokio::select! {
Some(block_hash) = block_stream.next() => {
println!("New block: {}", block_hash);
}
Some(log) = log_stream.next() => {
println!("Transfer event: {:?}", log.transaction_hash);
}
}High-Performance Block Sweeping (The 1-Millisecond Rule)
Never poll the database per transaction or poll individual user addresses. Instead, sweep logs for token contracts and intersect them with an in-memory HashSet of user addresses.
use std::collections::HashSet;
use alloy::primitives::Address;
// 1. Load all million users from DB into RAM once at startup
let my_users: HashSet<Address> = fetch_all_users_from_db().await?;
// 2. Fetch logs for a range of blocks for the USDC contract
let filter = Filter::new()
.address(usdc_contract)
.from_block(1000)
.to_block(1005)
.event_signature(transfer_topic);
let logs = provider.get_logs(&filter).await?;
// 3. Filter in RAM (takes < 1ms for thousands of logs)
for log in logs {
let transfer = Transfer::decode_log(&log)?;
// transfer.to is the recipient (topic2)
if my_users.contains(&transfer.to) {
// Send MQ message to credit user's balance
println!("User received payment: {}", transfer.value);
}
}Poll-Based Log Watching (HTTP)
When you cannot use WebSocket, poll for new logs at intervals:
use alloy::rpc::types::Filter;
use alloy::primitives::BlockNumber;
use tokio::time::{interval, Duration};
let mut last_block = provider.get_block_number().await?;
let mut poll_interval = interval(Duration::from_secs(5));
loop {
poll_interval.tick().await;
let current_block = provider.get_block_number().await?;
if current_block <= last_block {
continue;
}
let filter = Filter::new()
.address(token_address)
.from_block(BlockNumber::Number(last_block + 1))
.to_block(BlockNumber::Number(current_block))
.event_signature(transfer_topic)
.topic1(my_address);
let logs = provider.get_logs(&filter).await?;
for log in logs {
println!("Transfer: {:?}", log);
}
last_block = current_block;
}ENS Resolution
ENS (Ethereum Name Service) maps human-readable names to addresses and other records:
use alloy::providers::Provider;
// Resolve ENS name to address
let address = provider.resolve_name("vitalik.eth").await?;
println!("vitalik.eth -> {}", address);
// Reverse lookup: address to name
let name = provider.lookup_address(address).await?;
println!("{} -> {}", address, name.unwrap_or_default());Decode Events from Raw Logs
When you receive raw logs and need to decode them:
use alloy::sol;
sol! {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
event Swap(
address indexed sender,
uint256 amount0In,
uint256 amount1In,
uint256 amount0Out,
uint256 amount1Out,
address indexed to
);
}
// Decode from a raw log
let log: alloy::rpc::types::Log = /* ... */;
// Match by topic0 (event signature)
let transfer_sig = alloy::primitives::keccak256(b"Transfer(address,address,uint256)");
let approval_sig = alloy::primitives::keccak256(b"Approval(address,address,uint256)");
match log.topics.first() {
Some(topic) if *topic == transfer_sig => {
let transfer = Transfer::decode_log(&log)?;
println!("Transfer: {} -> {} = {}", transfer.from, transfer.to, transfer.value);
}
Some(topic) if *topic == approval_sig => {
let approval = Approval::decode_log(&log)?;
println!("Approval: {} -> {} = {}", approval.owner, approval.spender, approval.value);
}
_ => {
println!("Unknown event");
}
}Best Practices for Production Subscriptions
1. Reconnection: WS connections can drop. Use the retry layer or implement reconnection logic.
2. Backpressure: Process events fast enough, or use a bounded channel to avoid memory issues.
3. Block confirmation: For financial applications, wait for N confirmations before acting on events.
4. Idempotency: Events may be received multiple times (re-orgs). Track processed event IDs.
5. Checkpoint: Save the last processed block number so you can resume after restart:
// Save checkpoint
let last_processed = log.block_number.unwrap();
std::fs::write("checkpoint.txt", last_processed.to_string())?;
// Resume from checkpoint
let from_block: u64 = std::fs::read_to_string("checkpoint.txt")?.parse()?;
let filter = Filter::new().from_block(BlockNumber::Number(from_block));Transactions & Payments
This guide covers sending transactions on Ethereum and EVM-compatible chains: native ETH transfers, ERC-20 token transfers, gas estimation, transaction lifecycle, and transaction types.
Sending Native ETH
Transfer ETH to Another Address
use alloy::primitives::{address, U256};
use alloy::providers::{Provider, ProviderBuilder};
use alloy::rpc::types::TransactionRequest;
use alloy::network::TransactionBuilder;
use alloy::signers::local::PrivateKeySigner;
use alloy::network::EthereumWallet;
use eyre::Result;
#[tokio::main]
async fn main() -> Result<()> {
let signer: PrivateKeySigner = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
.parse()?;
let wallet = EthereumWallet::from(signer.clone());
let provider = ProviderBuilder::new()
.wallet(wallet)
.connect_http("https://ethereum-rpc.publicnode.com".parse()?);
let tx = TransactionRequest::default()
.to(address!("0x0000000000000000000000000000000000000000"))
.value(U256::from(1u128))
.with_input(Bytes::new());
let pending = provider.send_transaction(tx).await?;
println!("Pending tx: {}", pending.tx_hash());
Ok(())
}EIP-1559 Transaction (Recommended)
EIP-1559 transactions use a base fee + priority fee model:
let tx = TransactionRequest::default()
.to(recipient)
.value(amount)
.with_max_fee_per_gas(20_000_000_000)
.with_max_priority_fee_per_gas(1_000_000_000)
.with_gas_limit(21_000);Legacy Transaction
let tx = TransactionRequest::default()
.to(recipient)
.value(amount)
.with_gas_price(10_000_000_000)
.with_gas_limit(21_000);ERC-20 Token Transfers
Check Balance and Transfer
use alloy::primitives::{address, U256};
use alloy::providers::{Provider, ProviderBuilder};
use alloy::sol;
sol! {
#[sol(rpc)]
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
}
#[tokio::main]
async fn main() -> eyre::Result<()> {
let provider = ProviderBuilder::new()
.connect_http("https://ethereum-rpc.publicnode.com".parse()?);
let usdc = address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
let token = IERC20::new(usdc, &provider);
let holder = address!("0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503");
let balance = token.balanceOf(holder).call().await?;
println!("Balance: {}", balance);
Ok(())
}Approve and TransferFrom
// Approve a spender
let tx = token.approve(spender, U256::from(1_000_000u64))
.send()
.await?;
let receipt = tx.watch().await?;Gas Estimation
Automatic Gas Estimation (via Fillers)
ProviderBuilder::new() includes recommended fillers that auto-estimate gas:
let provider = ProviderBuilder::new()
.wallet(wallet)
.connect_http("https://ethereum-rpc.publicnode.com".parse()?);Manual Gas Estimation
let gas_estimate = provider.estimate_gas(tx.clone()).await?;
println!("Estimated gas: {}", gas_estimate);Transaction Lifecycle
// 1. Build transaction
let tx = TransactionRequest::default()
.to(recipient)
.value(amount);
// 2. Send (broadcast to network)
let pending = provider.send_transaction(tx).await?;
println!("Tx hash: {}", pending.tx_hash());
// 3. Wait for inclusion (mine)
let receipt = pending.get_receipt().await?;
println!("Mined in block: {}", receipt.block_number.unwrap());
println!("Status: {}", receipt.status());
// 4. Or use watch() for send + wait in one step
let tx_hash = token.transfer(recipient, amount)
.send()
.await?
.watch()
.await?;Transaction Types Reference
| Type | EIP | Key Feature |
|---|---|---|
| Legacy | - | Gas price only |
| EIP-1559 | 1559 | Base fee + priority fee, gas tip market |
| EIP-4844 | 4844 | Blob transactions (data availability) |
| EIP-7702 | 7702 | Account abstraction via delegation |
Common Payment Patterns
Batch Payments (Multicall)
Use Multicall3 for gas-efficient batch operations:
sol! {
#[sol(rpc)]
interface Multicall3 {
function aggregate3(Call3[] calldata calls)
external payable returns (Result[] memory returnData);
struct Call3 {
address target;
bool allowFailure;
bytes callData;
}
struct Result {
bool success;
bytes returnData;
}
}
}Checking Token Decimals Before Transfer
let decimals = token.decimals().call().await?;
let amount = parse_units("100.5", decimals)?;Handling Insufficient Balance
let balance = token.balanceOf(signer.address()).call().await?;
if balance < amount {
return Err(eyre!("Insufficient balance"));
}