riglr-evm-tools

Crates.ioriglr-evm-tools
lib.rsriglr-evm-tools
version0.3.0
created_at2025-09-10 19:48:58.836043+00
updated_at2025-09-10 19:48:58.836043+00
descriptionEVM blockchain tools for the Riglr framework
homepage
repositoryhttps://github.com/riglr/riglr
max_upload_size
id1832952
size333,715
J.Warp (jwarp-riglr)

documentation

README

riglr-evm-tools

Production-grade EVM blockchain tools for riglr agents, providing comprehensive Ethereum and EVM-compatible chain interactions.

Features

  • 🔐 Secure Transaction Management: Thread-safe SignerContext pattern for multi-tenant key management
  • 💰 Balance Operations: Check ETH and ERC20 token balances with ApplicationContext
  • 📤 Token Transfers: Send ETH and ERC20 tokens with SignerContext and automatic gas estimation
  • 🔄 DeFi Integration: Uniswap V3 support for token swaps and liquidity operations
  • 🌐 Multi-Chain Support: Works with Ethereum, Polygon, Arbitrum, Optimism, Base, and more
  • High Performance: Async/await with connection pooling and retry logic
  • 🛡️ Error Handling: Type-safe ToolError for distinguishing retriable and permanent failures
  • 🔒 Multi-Tenant Safe: Automatic isolation between concurrent user requests
  • 📖 Clear Separation: ApplicationContext for read operations, SignerContext for write operations

Installation

Add to your Cargo.toml:

[dependencies]
riglr-evm-tools = "0.1.0"

Quick Start

Using LocalEvmSigner

The LocalEvmSigner is the primary concrete implementation for EVM transaction signing:

use riglr_evm_tools::LocalEvmSigner;
use riglr_config::EvmNetworkConfig;
use riglr_core::{ApplicationContext, UnifiedSigner};
use std::sync::Arc;

// Create network config
let config = EvmNetworkConfig::new(
    "mainnet",
    1,
    "https://eth-mainnet.g.alchemy.com/v2/your-key".to_string()
);

// Create signer from private key
let signer = LocalEvmSigner::new(
    "0xYOUR_PRIVATE_KEY_HEX",
    config
)?;

// Set up application context
let app_context = ApplicationContext::new()?;
let unified_signer: Arc<dyn UnifiedSigner> = Arc::new(signer);
app_context.set_signer(unified_signer).await?;

// Now you can use any EVM tool
use riglr_evm_tools::transaction::send_eth;
let tx_hash = send_eth("0xRECIPIENT_ADDRESS", U256::from(1_000_000_000_000_000_000u64)).await?;

Read Operations with ApplicationContext

Read-only operations (like checking balances) use the ApplicationContext pattern:

use riglr_core::provider::ApplicationContext;
use riglr_evm_tools::balance::{get_eth_balance, get_erc20_balance};
use alloy::providers::{Provider, ProviderBuilder};
use std::sync::Arc;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Create application context with provider
    let app_context = ApplicationContext::from_env();
    
    // Add a Provider to the context for EVM operations
    let provider = ProviderBuilder::new()
        .on_http("https://eth.llamarpc.com".parse()?)
        .boxed();
    app_context.add_extension(Arc::new(provider) as Arc<dyn Provider>);
    
    // Check ETH balance
    let balance = get_eth_balance(
        "0x742d35Cc6634C0532925a3b844Bc9e7595f0eA4B".to_string(),
        Some(1), // Ethereum mainnet
        &app_context
    ).await?;
    
    println!("Balance: {}", balance.balance_formatted);
    Ok(())
}

Write Operations with SignerContext

Write operations (like transfers) require a SignerContext with proper signing capabilities:

use riglr_core::signer::{SignerContext, UnifiedSigner};
use riglr_evm_tools::signer::LocalEvmSigner;
use riglr_evm_tools::transaction::send_eth;
use alloy::primitives::{Address, U256};
use std::sync::Arc;
use std::str::FromStr;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Create configuration for the network
    let config = riglr_config::EvmNetworkConfig::new(
        "mainnet",
        1,
        "https://eth.llamarpc.com".to_string()
    );
    
    // Create a signer with private key
    let signer = Arc::new(LocalEvmSigner::new(
        std::env::var("EVM_PRIVATE_KEY")?,
        config
    )?) as Arc<dyn UnifiedSigner>;
    
    // Execute transfer within SignerContext
    let tx_hash = SignerContext::with_signer(signer, async {
        let to = Address::from_str("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb5")?;
        let amount = U256::from(1_000_000_000_000_000u64); // 0.001 ETH in wei
        
        let hash = send_eth(to, amount).await?;
        Ok::<_, anyhow::Error>(hash)
    }).await?;
    
    println!("Transaction sent: {}", tx_hash);
    Ok(())
}

DeFi Operations (Uniswap)

use riglr_core::signer::{SignerContext, UnifiedSigner};
use riglr_evm_tools::signer::LocalEvmSigner;
use riglr_evm_tools::swap::{get_uniswap_quote, perform_uniswap_swap};
use std::sync::Arc;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Create signer for swap operations
    let config = riglr_config::EvmNetworkConfig::new(
        "mainnet",
        1,
        "https://eth.llamarpc.com".to_string()
    );
    
    let signer = Arc::new(LocalEvmSigner::new(
        std::env::var("EVM_PRIVATE_KEY")?,
        config
    )?) as Arc<dyn UnifiedSigner>;
    
    // Execute swap within SignerContext
    SignerContext::with_signer(signer, async {
        // Get a swap quote (read operation)
        let quote = get_uniswap_quote(
            "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string(), // USDC
            "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".to_string(), // WETH
            "1000".to_string(),       // Amount in
            6,                        // USDC decimals
            18,                       // WETH decimals
            Some(3000),              // 0.3% fee tier
            Some(50),                // 0.5% slippage
        ).await?;
        
        println!("Expected output: {} WETH", quote.amount_out);
        
        // Execute the swap (write operation)
        let swap = perform_uniswap_swap(
            "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string(), // USDC
            "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".to_string(), // WETH
            "1000".to_string(),
            6,
            quote.amount_out_minimum,
            Some(3000),
            Some(300), // 5 minute deadline
        ).await?;
        
        println!("Swap executed: {}", swap.tx_hash);
        Ok(())
    }).await?;
    
    Ok(())
}

Available Tools

Balance Tools

  • get_eth_balance - Get ETH balance for an address
  • get_erc20_balance - Get ERC20 token balance

Transaction Tools

  • transfer_eth - Transfer ETH to another address
  • transfer_erc20 - Transfer ERC20 tokens
  • get_transaction_receipt - Get receipt for a transaction

DeFi Tools

  • get_uniswap_quote - Get swap quote from Uniswap V3
  • perform_uniswap_swap - Execute a token swap on Uniswap

Configuration

Environment Variables

# Convention-based RPC endpoints (RPC_URL_{CHAIN_ID})
RPC_URL_1=https://eth.llamarpc.com        # Ethereum (Chain ID: 1)
RPC_URL_137=https://polygon-rpc.com       # Polygon (Chain ID: 137)
RPC_URL_42161=https://arb1.arbitrum.io/rpc # Arbitrum (Chain ID: 42161)
RPC_URL_8453=https://base.llamarpc.com    # Base (Chain ID: 8453)

# Add any EVM chain by setting RPC_URL_{CHAIN_ID}
# Example: RPC_URL_10=https://optimism.io  # Optimism (Chain ID: 10)

# Private key for transactions (required for signing)
EVM_PRIVATE_KEY=0x...

Extensible Chain Support

The EVM tools now support adding new chains without code changes. Simply set an environment variable following the RPC_URL_{CHAIN_ID} convention:

// Any chain with RPC_URL_{ID} set is automatically supported
let supported_chains = riglr_evm_tools::util::get_supported_chains();
println!("Supported chains: {:?}", supported_chains);

// Check if a specific chain is supported
if riglr_evm_tools::util::is_chain_supported(10) {
    println!("Optimism is configured!");
}

Supported Networks

  • Ethereum Mainnet
  • Polygon
  • Arbitrum One
  • Optimism
  • Base
  • BNB Smart Chain
  • Avalanche C-Chain
  • Any EVM-compatible chain with custom RPC

Architecture Patterns

ApplicationContext for Read Operations

Read-only operations use ApplicationContext with a Provider extension:

use riglr_core::provider::ApplicationContext;
use alloy::providers::Provider;

// Setup once in your application
let app_context = ApplicationContext::from_env();
let provider = /* create provider */;
app_context.add_extension(Arc::new(provider) as Arc<dyn Provider>);

// Tools use the provider from context
let balance = get_eth_balance(address, chain_id, &app_context).await?;

SignerContext for Write Operations

Write operations require SignerContext with proper signing capabilities:

use riglr_core::signer::{SignerContext, UnifiedSigner};

// Create signer with private key
let signer = Arc::new(LocalEvmSigner::new(key, config)?) as Arc<dyn UnifiedSigner>;

// Execute within context
SignerContext::with_signer(signer, async {
    // Operations here have access to signing capabilities
    let tx_hash = send_eth(to, amount).await?;
    Ok(tx_hash)
}).await?;

Key Benefits

  1. Clear Separation: Read vs write operations are clearly distinguished
  2. Thread Safety: Each async task has its own isolated context
  3. Multi-Tenant Support: Multiple users can execute operations concurrently
  4. Type Safety: Operations are type-checked at compile time
  5. Automatic Context Propagation: Tools automatically access the current context

Type-Safe Chain Access

use riglr_core::signer::SignerContext;

// Get EVM-specific signer (fails if current signer doesn't support EVM)
let evm_signer = SignerContext::current_as_evm().await?;
let address = evm_signer.address();
let chain_id = evm_signer.chain_id();

// Check capabilities before operations
let signer = SignerContext::current().await?;
if signer.supports_evm() {
    // Perform EVM operations
}

Error Handling

All tools use the ToolError pattern to distinguish between retriable and permanent failures:

use riglr_core::ToolError;
use riglr_evm_tools::balance::get_eth_balance;

match get_eth_balance(address, chain_id, &app_context).await {
    Ok(balance) => println!("Success: {}", balance.balance_formatted),
    Err(e) => match e {
        ToolError::Permanent { message, .. } => {
            // Invalid input, don't retry
            println!("Permanent error: {}", message);
        }
        ToolError::Retriable { message, .. } => {
            // Network issues, can retry
            println!("Temporary error: {}", message);
        }
        _ => println!("Other error: {:?}", e),
    }
}

Integration with rig

All tools are compatible with the rig framework:

use rig::agents::Agent;
use riglr_evm_tools::get_eth_balance_tool;

let agent = Agent::builder()
    .preamble("You are an EVM blockchain analyst.")
    .tool(get_eth_balance_tool())
    .build();

Safety and Security

  • Private keys are managed per-client instance, not globally
  • All addresses are validated before use
  • Automatic nonce management prevents transaction conflicts
  • Gas estimation with configurable limits
  • Slippage protection for DeFi operations
  • Thread-safe client instances can be cloned and shared

License

MIT

Contributing

Contributions are welcome! Please read our contributing guidelines and submit PRs to our GitHub repository.

Support

For issues and questions, please open an issue on GitHub or reach out to the maintainers.

Commit count: 209

cargo fmt