Alloy RS - Complete Guide

Alloy is a modern, high-performance Ethereum library for Rust developers. Built from the ground up with type safety, performance, and developer experience in mind.

🎯What is Alloy?

Alloy is a high-performance, modern Ethereum library for Rust. It's the successor to ethers-rs and provides a complete toolkit for interacting with Ethereum and EVM-compatible blockchains.

Why Alloy?

💡 Pro Tip: Alloy is designed to work seamlessly with other Rust async ecosystems like Tokio and async-std.

✨Key Features

🔌 Providers

Connect to Ethereum nodes via HTTP, WebSocket, or IPC. Support for multiple networks and RPC providers.

🔐 Signers

Sign transactions with private keys, hardware wallets, or custom signing implementations.

📝 Contracts

Type-safe contract interactions with automatic ABI encoding/decoding.

🧮 Primitives

Native types for addresses, hashes, bytes, and large integers (U256, U512).

💰 Transactions

Build, sign, and send transactions with support for EIP-1559, EIP-2930, and legacy formats.

🔍 Events

Listen to and filter blockchain events with type-safe event parsing.

⚙️Setup & Installation

1. Add Alloy to Your Project

[dependencies]
alloy = { version = "0.5", features = ["full"] }
tokio = { version = "1", features = ["full"] }
eyre = "0.6"

2. Common Feature Flags

Feature Description When to Use
full All features enabled Quick prototyping, learning
provider-http HTTP provider support Basic RPC connections
provider-ws WebSocket provider Real-time event listening
signers Wallet signing functionality Sending transactions
signer-local Local private key signing Development, testing
contract Smart contract interactions DApp development
💡 Best Practice: In production, only enable the features you need to reduce compile time and binary size.

🔌Providers: Connecting to Ethereum

Providers are your gateway to the Ethereum network. They handle communication with Ethereum nodes.

What Can Providers Do?

  • Query blockchain data (blocks, transactions, balances)
  • Send transactions to the network
  • Call smart contract functions
  • Subscribe to real-time events
  • Estimate gas costs

Creating an HTTP Provider

use alloy::providers::{Provider, ProviderBuilder};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Connect to Ethereum mainnet via Infura
    let rpc_url = "https://mainnet.infura.io/v3/YOUR_KEY";
    let provider = ProviderBuilder::new()
        .on_http(rpc_url.parse()?);
    
    // Get latest block number
    let block_number = provider.get_block_number().await?;
    println!("Latest block: {}", block_number);
    
    Ok(())
}

Common Provider Methods

// Get balance of an address
let balance = provider.get_balance(address).await?;

// Get current gas price
let gas_price = provider.get_gas_price().await?;

// Get transaction by hash
let tx = provider.get_transaction_by_hash(tx_hash).await?;

// Get block by number
let block = provider.get_block_by_number(block_num, false).await?;

// Estimate gas for a transaction
let gas = provider.estimate_gas(&tx).await?;

// Get transaction receipt
let receipt = provider.get_transaction_receipt(tx_hash).await?;

🎯 Use Case: Portfolio Tracker

Situation: You want to build an app that shows users their token balances and transaction history.

Solution: Use a Provider to query balances, read contract states, and fetch historical transactions without needing to send any transactions.

🧮Primitives: Ethereum Data Types

Alloy provides native Rust types for Ethereum data structures.

Core Primitive Types

Address - Ethereum Addresses

use alloy::primitives::Address;

// Parse from string
let addr: Address = "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".parse()?;

// Create from bytes
let addr = Address::from([0u8; 20]);

// Zero address
let zero = Address::ZERO;

When to use: Wallet addresses, contract addresses, recipient addresses

U256 - Large Numbers (Wei, Tokens)

use alloy::primitives::U256;

// Create from various types
let amount = U256::from(1_000_000_000_000_000_000u128); // 1 ETH in wei
let from_str = U256::from_str("1000000000000000000")?;

// Arithmetic operations
let doubled = amount * U256::from(2);
let half = amount / U256::from(2);

// Convert to ETH
let eth_value = amount.to_string().parse::()? / 1e18;

When to use: Token amounts, balances, gas calculations, any value > u64::MAX

Bytes - Raw Data

use alloy::primitives::Bytes;

// Create from hex string
let data = Bytes::from(hex::decode("0x1234")?);

// Use in transactions
let tx = TransactionRequest::default()
    .input(data);

When to use: Contract call data, transaction input, raw blockchain data

🎯 Use Case: Token Balance Display

Situation: You need to show a user's USDC balance (6 decimals) in a readable format.

Solution: Use U256 to store the raw balance, then convert to human-readable format by dividing by 10^6.

let balance_raw: U256 = token_contract.balance_of(user_address).await?;
let balance_usdc = balance_raw.to_string().parse::()? / 1e6;
println!("Balance: ${:.2} USDC", balance_usdc);

💰Transactions: Sending Data to Blockchain

Transactions are how you change the state of the blockchain.

When Do You Need Transactions?

  • Sending ETH to another address
  • Calling state-changing contract functions
  • Deploying smart contracts
  • Minting/burning tokens
  • Updating contract storage
⚠️ Important: Transactions cost gas and are irreversible. Always test on testnets first!

Building a Simple Transaction

use alloy::rpc::types::TransactionRequest;
use alloy::primitives::{Address, U256};

// Send ETH to an address
let to: Address = "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".parse()?;
let value = U256::from(1_000_000_000_000_000_000u128); // 1 ETH

let tx = TransactionRequest::default()
    .to(to)
    .value(value);

// Send it (requires wallet attached to provider)
let pending = provider.send_transaction(tx).await?;
let receipt = pending.get_receipt().await?;

Contract Interaction Transaction

// Call a contract function (e.g., deposit() on WETH)
let contract_address: Address = "0xC02...".parse()?;
let function_selector = hex::decode("d0e30db0")?; // deposit()

let tx = TransactionRequest::default()
    .to(contract_address)
    .value(U256::from(1_000_000_000_000_000_000u128))
    .input(function_selector.into());

let pending = provider.send_transaction(tx).await?;
let receipt = pending.get_receipt().await?;

println!("Transaction mined in block: {}", receipt.block_number);

Transaction with Custom Gas Settings

use alloy::rpc::types::TransactionRequest;

let tx = TransactionRequest::default()
    .to(recipient)
    .value(amount)
    .gas_limit(21_000)
    .max_fee_per_gas(50_000_000_000) // 50 gwei
    .max_priority_fee_per_gas(2_000_000_000); // 2 gwei priority

🎯 Use Case: DEX Token Swap

Situation: User wants to swap USDC for ETH on Uniswap.

Solution:

  1. Approve Uniswap router to spend USDC (transaction 1)
  2. Call Uniswap's swap function (transaction 2)
  3. Wait for confirmations

Both steps require transactions with proper function selectors and parameters.

📝Smart Contracts: Type-Safe Interactions

Alloy makes interacting with smart contracts type-safe and easy.

Two Ways to Interact with Contracts

1. Manual Contract Calls (Low-Level)

// Read-only call (no gas, no transaction)
let data = hex::decode("70a08231...")?; // balanceOf(address)
let call = TransactionRequest::default()
    .to(contract_address)
    .input(data.into());

let result = provider.call(&call).await?;
let balance = U256::from_be_bytes(result.as_ref());

// State-changing call (costs gas, creates transaction)
let data = hex::decode("a9059cbb...")?; // transfer(address,uint256)
let tx = TransactionRequest::default()
    .to(contract_address)
    .input(data.into());

let pending = provider.send_transaction(tx).await?;

2. Type-Safe Contract Bindings (Recommended)

// Generate bindings from ABI
sol! {
    #[sol(rpc)]
    contract ERC20 {
        function balanceOf(address owner) external view returns (uint256);
        function transfer(address to, uint256 amount) external returns (bool);
        
        event Transfer(address indexed from, address indexed to, uint256 value);
    }
}

// Use the generated bindings
let contract = ERC20::new(contract_address, provider);

// Read balance (no transaction)
let balance = contract.balanceOf(user_address).call().await?;

// Transfer tokens (creates transaction)
let tx = contract.transfer(recipient, amount);
let pending = tx.send().await?;
let receipt = pending.get_receipt().await?;

🎯 Use Case: NFT Marketplace

Situation: Build an NFT marketplace where users can list and buy NFTs.

Solution: Use contract bindings to:

  • Read NFT ownership: ownerOf(tokenId)
  • Check approvals: isApprovedForAll(owner, operator)
  • Transfer NFTs: transferFrom(from, to, tokenId)
  • Listen to Transfer events for real-time updates

🔐Signers & Wallets: Transaction Signing

To send transactions, you need a signer to cryptographically sign them with a private key.

Types of Signers

1. Local Private Key Signer

use alloy::signers::local::PrivateKeySigner;
use alloy::network::EthereumWallet;

// From hex string
let signer: PrivateKeySigner = "your_private_key_hex".parse()?;

// Create wallet
let wallet = EthereumWallet::from(signer);

// Get address
let address = wallet.default_signer().address();

// Attach to provider
let provider = ProviderBuilder::new()
    .with_recommended_fillers()
    .wallet(wallet)
    .on_http(rpc_url.parse()?);

When to use: Development, testing, automated bots, backend services

2. Provider with Wallet (Complete Setup)

use alloy::providers::{Provider, ProviderBuilder};
use alloy::signers::local::PrivateKeySigner;
use alloy::network::EthereumWallet;

let signer: PrivateKeySigner = private_key.parse()?;
let wallet = EthereumWallet::from(signer);

// This provider can both read AND send transactions
let provider = ProviderBuilder::new()
    .with_recommended_fillers()  // Auto-fills gas, nonce, chain ID
    .wallet(wallet)               // Enables transaction signing
    .on_http(rpc_url.parse()?);

// Now you can send transactions
let tx = TransactionRequest::default()
    .to(recipient)
    .value(amount);

let pending = provider.send_transaction(tx).await?;
⚠️ Security Warning:
  • Never hardcode private keys in source code
  • Never commit private keys to version control
  • Use environment variables or secure vaults in production
  • Consider hardware wallets for high-value operations

🎯 Use Case: Trading Bot

Situation: Create an automated bot that monitors prices and executes trades.

Solution: Use a local signer with API keys stored securely:

// Load from environment variable
let private_key = std::env::var("PRIVATE_KEY")?;
let signer: PrivateKeySigner = private_key.parse()?;
let wallet = EthereumWallet::from(signer);

let provider = ProviderBuilder::new()
    .with_recommended_fillers()
    .wallet(wallet)
    .on_http(rpc_url.parse()?);

// Bot can now automatically sign and send trades
loop {
    let price = check_price(&provider).await?;
    if should_buy(price) {
        execute_trade(&provider, amount).await?;
    }
    tokio::time::sleep(Duration::from_secs(60)).await;
}

🎯Real-World Use Cases

1. 💼 Wallet Application

What you need:

  • Provider: Read balances, gas prices
  • Signer: Sign transactions
  • Primitives: Handle addresses and amounts

Features: View balance, send ETH, view transaction history, estimate fees

2. 🎨 NFT Minting Platform

What you need:

  • Contract bindings: Interact with NFT contract
  • Event listeners: Monitor mint events
  • IPFS integration: Store metadata

Features: Mint NFTs, check ownership, transfer NFTs, royalty payments

3. 📊 DeFi Dashboard

What you need:

  • Multiple providers: Connect to different protocols
  • Contract calls: Read pool data, positions
  • Batch queries: Efficient data fetching

Features: Portfolio tracking, APY calculations, yield farming opportunities

4. 🤖 MEV Bot

What you need:

  • WebSocket provider: Real-time mempool monitoring
  • Fast transaction submission
  • Gas estimation: Optimize for profit

Features: Arbitrage detection, frontrunning (ethically), backrunning

5. 🎮 GameFi Backend

What you need:

  • Smart contract interactions: Game state, items
  • Batch transactions: Efficient player actions
  • Event subscriptions: Real-time game updates

Features: Item minting, trading, leaderboards, rewards distribution

6. 💱 Token Swapper (Like Your Project!)

What you need:

  • Provider with wallet: Send transactions
  • Contract interactions: DEX protocols (Uniswap, Curve)
  • Gas estimation: Show users costs

Features: Swap tokens, wrap/unwrap ETH, slippage protection

💻Complete Code Examples

Example 1: Check Multiple Wallet Balances

use alloy::providers::{Provider, ProviderBuilder};
use alloy::primitives::Address;
use eyre::Result;

#[tokio::main]
async fn main() -> Result<()> {
    let provider = ProviderBuilder::new()
        .on_http("https://mainnet.infura.io/v3/YOUR_KEY".parse()?);
    
    let addresses = vec![
        "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
        "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
    ];
    
    for addr_str in addresses {
        let addr: Address = addr_str.parse()?;
        let balance = provider.get_balance(addr).await?;
        let eth = balance.to_string().parse::()? / 1e18;
        
        println!("{}: {:.4} ETH", addr_str, eth);
    }
    
    Ok(())
}

Example 2: Monitor Gas Prices

use alloy::providers::{Provider, ProviderBuilder};
use std::time::Duration;
use eyre::Result;

#[tokio::main]
async fn main() -> Result<()> {
    let provider = ProviderBuilder::new()
        .on_http("https://mainnet.infura.io/v3/YOUR_KEY".parse()?);
    
    loop {
        let gas_price = provider.get_gas_price().await?;
        let gwei = gas_price / 1_000_000_000;
        
        println!("Current gas price: {} gwei", gwei);
        
        if gwei < 30 {
            println!("⚡ Low gas detected! Good time to transact.");
        }
        
        tokio::time::sleep(Duration::from_secs(60)).await;
    }
}

Example 3: Transaction with Retry Logic

use alloy::providers::{Provider, ProviderBuilder};
use alloy::rpc::types::TransactionRequest;
use alloy::primitives::{Address, U256};
use eyre::Result;

async fn send_with_retry(
    provider: &impl Provider,
    tx: TransactionRequest,
    max_retries: u32
) -> Result<()> {
    for attempt in 1..=max_retries {
        match provider.send_transaction(tx.clone()).await {
            Ok(pending) => {
                let receipt = pending.get_receipt().await?;
                println!("✅ Success! Tx: {:?}", receipt.transaction_hash);
                return Ok(());
            }
            Err(e) if attempt < max_retries => {
                println!("⚠️ Attempt {} failed: {}", attempt, e);
                tokio::time::sleep(Duration::from_secs(5)).await;
            }
            Err(e) => return Err(e.into()),
        }
    }
    Ok(())
}
💡 Pro Tip: Always test your code on testnets (Sepolia, Goerli) before deploying to mainnet. This saves you from costly mistakes!

📚Additional Resources

🎓Learning Path

  1. Week 1: Setup and basic provider usage (read-only operations)
  2. Week 2: Understanding primitives and working with addresses/amounts
  3. Week 3: Building and sending simple transactions on testnet
  4. Week 4: Smart contract interactions with manual calls
  5. Week 5: Type-safe contract bindings and event listening
  6. Week 6: Build a complete DApp (like your WETH wrapper!)