1 unstable release
| 0.3.0 | Sep 10, 2025 |
|---|
#85 in #evm
1MB
16K
SLoC
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 addressget_erc20_balance- Get ERC20 token balance
Transaction Tools
transfer_eth- Transfer ETH to another addresstransfer_erc20- Transfer ERC20 tokensget_transaction_receipt- Get receipt for a transaction
DeFi Tools
get_uniswap_quote- Get swap quote from Uniswap V3perform_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
- Clear Separation: Read vs write operations are clearly distinguished
- Thread Safety: Each async task has its own isolated context
- Multi-Tenant Support: Multiple users can execute operations concurrently
- Type Safety: Operations are type-checked at compile time
- 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.
Dependencies
~54–77MB
~1.5M SLoC