use std::str::FromStr; use alloy::{ contract::{ContractInstance, Interface}, dyn_abi::DynSolValue, eips::eip1898::BlockId, primitives::{address, map::AddressHashMap, Address, U256}, providers::{Provider, ProviderBuilder}, rpc::types::{ state::AccountOverride, trace::geth::{ GethDebugBuiltInTracerType, GethDebugTracerType, GethDebugTracingCallOptions, GethDebugTracingOptions, }, Block, TransactionRequest, }, sol_types::SolValue, transports::http::reqwest::Url, }; use miette::{IntoDiagnostic, WrapErr}; use serde_json::Value; use tracing::info; use tycho_simulation::tycho_common::Bytes; use crate::traces::print_call_trace; const NATIVE_ALIASES: &[Address] = &[ address!("0x0000000000000000000000000000000000000000"), address!("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"), ]; const ERC_20_ABI: &str = r#"[{"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"stateMutability":"view","type":"function"}]"#; pub struct RPCProvider { pub url: Url, trace: bool, } impl RPCProvider { pub fn new(url: String, trace: bool) -> Self { let url = url.as_str().parse().unwrap(); RPCProvider { url, trace } } pub async fn get_token_balance( &self, token_address: Address, wallet_address: Address, block_number: u64, ) -> miette::Result { let provider = ProviderBuilder::new().connect_http(self.url.clone()); let block_id: BlockId = BlockId::from(block_number); match NATIVE_ALIASES.contains(&token_address) { true => provider .get_balance(wallet_address) .block_id(block_id) .await .into_diagnostic() .wrap_err("Failed to fetch token balance"), false => { let abi = serde_json::from_str(ERC_20_ABI) .into_diagnostic() .wrap_err("invalid ABI")?; let contract = ContractInstance::new(token_address, provider, Interface::new(abi)); let wallet_addr = DynSolValue::from(wallet_address); let result_value = contract .function("balanceOf", &[wallet_addr]) .expect("Failed to build function call") .block(block_id) .call() .await .into_diagnostic() .wrap_err("Failed to fetch ERC-20 Balance")?; let result: U256 = result_value .first() .ok_or_else(|| miette::miette!("No value returned from contract call"))? .as_uint() .ok_or_else(|| miette::miette!("Returned value is not a uint"))? .0; Ok(result) } } } pub async fn get_block_header(&self, block_number: u64) -> miette::Result { let provider = ProviderBuilder::new().connect_http(self.url.clone()); let block_id: BlockId = BlockId::from(block_number); provider .get_block(block_id) .await .into_diagnostic() .wrap_err("Failed to fetch block header") .and_then(|block_opt| block_opt.ok_or_else(|| miette::miette!("Block not found"))) } pub async fn simulate_transactions_with_tracing( &self, transaction: TransactionRequest, block_number: u64, state_overwrites: AddressHashMap, ) -> miette::Result { let provider = ProviderBuilder::new().connect_http(self.url.clone()); // Configure tracing options - use callTracer for better formatted results let tracing_options = GethDebugTracingOptions { tracer: Some(GethDebugTracerType::BuiltInTracer( GethDebugBuiltInTracerType::CallTracer, )), config: Default::default(), tracer_config: Default::default(), timeout: None, }; let trace_options = GethDebugTracingCallOptions { tracing_options, state_overrides: if state_overwrites.is_empty() { None } else { Some(state_overwrites) }, block_overrides: None, }; let result: Value = provider .client() .request("debug_traceCall", (transaction, BlockId::from(block_number), trace_options)) .await .map_err(|e| { tracing::error!("debug_traceCall RPC error: {:#}", e); e }) .into_diagnostic() .wrap_err("Failed to debug trace call many")?; if self.trace { print_call_trace(&result, 0).await; } let has_error = result .as_object() .and_then(|obj| obj.get("error")) .is_some(); let has_failed = result .as_object() .and_then(|obj| obj.get("failed")) .and_then(|v| v.as_bool()) .unwrap_or(false); if has_error || has_failed { if let Some(result_obj) = result.as_object() { if let Some(error) = result_obj.get("error") { return Err(miette::miette!("Transaction execution failed: {}", error)); } if let Some(revert_reason) = result_obj.get("revertReason") { return Err(miette::miette!("Transaction reverted: {}", revert_reason)); } } return Err(miette::miette!("Transaction failed")); } else { info!("Transaction successfully simulated."); } let mut executed_amount_out = U256::ZERO; if let Some(result_obj) = result.as_object() { if let Some(gas_used) = result_obj .get("gasUsed") .and_then(|v| v.as_str()) { let gas_used_decoded = U256::from_str_radix(gas_used.trim_start_matches("0x"), 16) .into_diagnostic()?; info!("Gas used: {}", gas_used_decoded); } if let Some(output) = result_obj .get("output") .and_then(|v| v.as_str()) { executed_amount_out = U256::abi_decode(&Bytes::from_str(output).into_diagnostic()?) .into_diagnostic()?; } } Ok(executed_amount_out) } } #[cfg(test)] mod tests { use std::{env, str::FromStr}; use alloy::primitives::address; use super::*; #[tokio::test] async fn get_token_balance_native_token() { let eth_rpc_url = env::var("RPC_URL").expect("Missing RPC_URL in environment"); let rpc_provider = RPCProvider::new(eth_rpc_url, false); let token_address = address!("0x0000000000000000000000000000000000000000"); let wallet_address = address!("0x787B8840100d9BaAdD7463f4a73b5BA73B00C6cA"); let block_number = 21998530; let balance = rpc_provider .get_token_balance(token_address, wallet_address, block_number) .await .unwrap(); assert_eq!( balance, U256::from_str("1070041574684539264153").expect("Failed to convert ETH value") ); } #[tokio::test] async fn get_token_balance_erc20_token() { let eth_rpc_url = env::var("RPC_URL").expect("Missing RPC_URL in environment"); let rpc_provider = RPCProvider::new(eth_rpc_url, false); let token_address = address!("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"); let wallet_address = address!("0x787B8840100d9BaAdD7463f4a73b5BA73B00C6cA"); let block_number = 21998530; let balance = rpc_provider .get_token_balance(token_address, wallet_address, block_number) .await .unwrap(); assert_eq!(balance, U256::from(717250938432_u64)); } #[tokio::test] async fn get_block_header() { let eth_rpc_url = env::var("RPC_URL").expect("Missing RPC_URL in environment"); let rpc_provider = RPCProvider::new(eth_rpc_url, false); let block_number = 21998530; let block_header = rpc_provider .get_block_header(block_number) .await .unwrap(); // Verify that we got a block with the correct number assert_eq!(block_number, block_header.header.number); assert_eq!(block_header.header.timestamp, 1741393115); } }