mod.rs•13.2 kB
//! Ethereum service module - organized into sub-modules for better maintainability
//!
//! This module provides the main EthereumService that handles:
//! - Balance queries (balance.rs)
//! - Token pricing (price.rs)
//! - Uniswap pool operations (pool.rs)
//! - Swap transactions (swap.rs)
//! - Utility functions (utils.rs)
mod balance;
mod price;
mod pool;
mod swap;
mod utils;
pub use balance::*;
pub use price::*;
pub use pool::*;
pub use swap::*;
pub use utils::*;
use crate::types::{BalanceQuery, SwapParams, SwapQuote};
use alloy::primitives::Address;
use alloy::providers::Provider;
use alloy::providers::ProviderBuilder;
use alloy::signers::local::PrivateKeySigner;
use anyhow::{Context, Result};
use rust_decimal::Decimal;
use std::env;
use std::str::FromStr;
pub struct EthereumService {
rpc_url: String,
uniswap_v3_router: Address,
uniswap_v3_pool_factory: Address,
signer_address: PrivateKeySigner,
}
impl EthereumService {
pub async fn init(rpc_url: Option<String>) -> Result<Self> {
let signer_address = Self::load_signer_address()?;
let rpc = rpc_url.unwrap_or_else(|| "https://eth.llamarpc.com".to_string());
Ok(Self {
rpc_url: rpc,
uniswap_v3_router: Address::from_str("0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45")?,
uniswap_v3_pool_factory: Address::from_str("0x1F98431c8aD98523631AE4a59f267346ea31F984")?,
signer_address,
})
}
/// Helper method to create a provider
async fn create_provider(&self) -> Result<impl Provider + Send + Sync + 'static> {
let provider = ProviderBuilder::new()
.wallet(self.signer_address.clone())
.connect(&self.rpc_url)
.await
.context("Failed to connect to RPC")?;
Ok(provider)
}
/// Load signer address from environment variable ETHEREUM_PRIVATE_KEY
fn load_signer_address() -> Result<PrivateKeySigner> {
let private_key_hex = env::var("ETHEREUM_PRIVATE_KEY")
.context("ETHEREUM_PRIVATE_KEY environment variable is not set. Please set it to your private key.")?;
tracing::info!("Found private key in environment variable");
let key_str = private_key_hex.strip_prefix("0x").unwrap_or(&private_key_hex);
let wallet = PrivateKeySigner::from_str(key_str)
.context("Failed to parse private key from ETHEREUM_PRIVATE_KEY environment variable")?;
let address = Address::from(wallet.address());
tracing::info!("Signer address loaded: {:?}", address);
Ok(wallet)
}
/// Get the signer address
pub fn get_signer_address(&self) -> Address {
Address::from(self.signer_address.address())
}
/// Get the signer
pub fn get_signer(&self) -> &PrivateKeySigner {
&self.signer_address
}
// Balance operations
pub async fn get_token_decimals(&self, token_address: Address) -> Result<u8> {
let provider = self.create_provider().await?;
balance::get_token_decimals(token_address, &provider).await
}
pub async fn get_eth_balance(&self, address: &str) -> Result<String> {
let provider = self.create_provider().await?;
balance::get_eth_balance(address, &provider).await
}
pub async fn get_token_balance(&self, token_address: &str, holder_address: &str) -> Result<String> {
let provider = self.create_provider().await?;
balance::get_token_balance(token_address, holder_address, &provider).await
}
pub async fn query_balance(&self, params: &BalanceQuery) -> Result<String> {
let provider = self.create_provider().await?;
balance::query_balance(params, &provider).await
}
// Price operations
pub async fn get_token_price(&self, token: &str, currency: Option<&str>) -> Result<String> {
price::get_token_price(token, currency).await
}
// Swap operations
pub async fn get_swap_quote(&self, params: &SwapParams) -> Result<SwapQuote> {
let provider = self.create_provider().await?;
swap::get_swap_quote(params, self.uniswap_v3_pool_factory, &provider).await
}
pub async fn build_swap_transaction(&self, params: &SwapParams) -> Result<String> {
let user_address = self.get_signer_address();
let provider = self.create_provider().await?;
swap::build_swap_transaction(
params,
self.uniswap_v3_router,
self.uniswap_v3_pool_factory,
user_address,
&provider,
).await
}
pub fn validate_swap_params(&self, params: &SwapParams) -> Result<()> {
swap::validate_swap_params(params)
}
// Pool operations
pub async fn get_uniswap_v3_pool_address(
&self,
token_a: Address,
token_b: Address,
fee: u64,
) -> Result<Address> {
let provider = self.create_provider().await?;
pool::get_uniswap_v3_pool_address(token_a, token_b, fee, self.uniswap_v3_pool_factory, &provider).await
}
pub async fn get_pool_price(&self, pool_address: Address) -> Result<Decimal> {
let provider = self.create_provider().await?;
pool::get_pool_price(pool_address, &provider).await
}
pub async fn calculate_min_amount_out(
&self,
pool_address: Address,
from_token: Address,
to_token: Address,
amount_in: Decimal,
slippage_percentage: Decimal,
) -> Result<Decimal> {
let provider = self.create_provider().await?;
pool::calculate_min_amount_out(
pool_address,
from_token,
to_token,
amount_in,
slippage_percentage,
&provider,
).await
}
pub async fn check_token_balance_for_swap(&self, token_address: &str, user_address: Address, _amount_in: Decimal) -> Result<String> {
let provider = self.create_provider().await?;
swap::check_token_balance_for_swap(token_address, user_address, &provider).await
}
pub async fn check_token_allowance_for_swap(&self, token_address: Address, user_address: Address, required_amount: alloy::primitives::U256) -> Result<String> {
let provider = self.create_provider().await?;
swap::check_token_allowance_for_swap(
token_address,
user_address,
self.uniswap_v3_router,
required_amount,
&provider,
).await
}
pub fn calculate_path(&self, from_token: Address, to_token: Address, fee: u64) -> alloy::primitives::Bytes {
utils::calculate_path(from_token, to_token, fee)
}
}
// These basic tests verify module initialization works
#[cfg(test)]
mod tests {
use super::*;
use crate::types::*;
mod service_tests {
use super::*;
#[tokio::test]
async fn test_new_service_with_custom_rpc() {
let custom_rpc = "https://mainnet.infura.io/v3/test".to_string();
let _service = EthereumService::init(Some(custom_rpc.clone())).await.unwrap();
}
#[tokio::test]
async fn test_new_service_with_default_rpc() {
let _service = EthereumService::init(None).await.unwrap();
}
}
mod query_balance_tests {
use super::*;
#[tokio::test]
async fn test_query_balance_eth() {
let service = EthereumService::init(None).await.unwrap();
let params = BalanceQuery {
address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045".to_string(),
token_address: None,
};
match service.query_balance(¶ms).await {
Ok(balance) => {
assert!(balance.contains("ETH"));
println!("ETH Balance: {}", balance);
},
Err(e) => println!("RPC not available: {}", e),
}
}
#[tokio::test]
async fn test_query_balance_token() {
let service = EthereumService::init(None).await.unwrap();
let params = BalanceQuery {
address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045".to_string(),
token_address: Some("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string()),
};
match service.query_balance(¶ms).await {
Ok(balance) => println!("Token balance: {}", balance),
Err(e) => println!("Error: {}", e),
}
}
#[tokio::test]
async fn test_query_balance_invalid_address() {
let service = EthereumService::init(None).await.unwrap();
let params = BalanceQuery {
address: "invalid_address".to_string(),
token_address: None,
};
let result = service.query_balance(¶ms).await;
assert!(result.is_err());
}
}
mod get_token_price_tests {
use super::*;
#[tokio::test]
async fn test_get_token_price_eth() {
let service = EthereumService::init(None).await.unwrap();
match service.get_token_price("ETH", None).await {
Ok(price) => {
assert!(price.contains("ETH:"));
println!("ETH price: {}", price);
},
Err(e) => println!("Failed: {}", e),
}
}
#[tokio::test]
async fn test_get_token_price_usdc() {
let service = EthereumService::init(None).await.unwrap();
match service.get_token_price("USDC", None).await {
Ok(price) => println!("USDC price: {}", price),
Err(e) => println!("Failed: {}", e),
}
}
#[tokio::test]
async fn test_get_token_price_invalid_token() {
let service = EthereumService::init(None).await.unwrap();
let result = service.get_token_price("INVALIDTOKEN123", None).await;
assert!(result.is_err());
}
}
mod build_swap_transaction_tests {
use super::*;
use rust_decimal::Decimal;
use std::str::FromStr;
#[tokio::test]
async fn test_build_swap_transaction_basic() {
let service = EthereumService::init(None).await.unwrap();
let params = SwapParams {
from_token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string(),
to_token: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".to_string(),
amount_in: "100".to_string(),
slippage_tolerance: Some(Decimal::from_str("0.5").unwrap()),
recipient: None,
};
match service.build_swap_transaction(¶ms).await {
Ok(response) => println!("Swap response: {}", response),
Err(e) => println!("Swap failed: {}", e),
}
}
#[tokio::test]
async fn test_validate_swap_params() {
let service = EthereumService::init(None).await.unwrap();
let params = SwapParams {
from_token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string(),
to_token: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".to_string(),
amount_in: "100".to_string(),
slippage_tolerance: Some(Decimal::from_str("0.5").unwrap()),
recipient: None,
};
assert!(service.validate_swap_params(¶ms).is_ok());
}
#[tokio::test]
async fn test_validate_swap_params_invalid() {
let service = EthereumService::init(None).await.unwrap();
let params = SwapParams {
from_token: "invalid".to_string(),
to_token: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".to_string(),
amount_in: "100".to_string(),
slippage_tolerance: None,
recipient: None,
};
assert!(service.validate_swap_params(¶ms).is_err());
}
#[tokio::test]
async fn test_get_swap_quote() {
let service = EthereumService::init(None).await.unwrap();
let params = SwapParams {
from_token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string(),
to_token: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".to_string(),
amount_in: "1000".to_string(),
slippage_tolerance: None,
recipient: None,
};
match service.get_swap_quote(¶ms).await {
Ok(quote) => {
assert_eq!(quote.from_token, params.from_token);
assert_eq!(quote.to_token, params.to_token);
println!("Quote: {} -> {}", quote.amount_in, quote.estimated_amount_out);
},
Err(e) => println!("Quote failed: {}", e),
}
}
}
}