use alloy::hex;
use alloy::primitives::Address;
use riglr_core::{provider::ApplicationContext, signer::SignerContext, ToolError};
use riglr_evm_common::get_chain_info;
use riglr_macros::tool;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use std::sync::Arc;
use tracing::{debug, info};
use crate::error::EvmToolError;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EthBalance {
pub balance_formatted: String,
pub unit: String,
pub raw_balance: String,
pub balance_raw: String, pub address: String,
pub chain_name: String,
pub chain_id: u64,
pub block_number: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenBalance {
pub balance_formatted: String,
pub token_symbol: Option<String>,
pub raw_balance: String,
pub decimals: u8,
pub token_address: String,
}
pub(crate) async fn get_eth_balance_with_context(
address: String,
chain_id: Option<u64>,
app_context: &ApplicationContext,
) -> Result<EthBalance, EvmToolError> {
debug!("Getting ETH balance for address: {}", address);
let resolved_chain_id = if let Some(id) = chain_id {
debug!("Using explicit chain_id: {}", id);
id
} else if let Ok(signer) = SignerContext::current_as_evm().await {
let id = signer.chain_id();
debug!("Using chain_id from EVM SignerContext: {}", id);
id
} else {
debug!("No explicit chain_id or EVM signer context, defaulting to Ethereum mainnet (1)");
1 };
let addr = Address::from_str(&address)
.map_err(|e| EvmToolError::InvalidAddress(format!("Invalid address: {}", e)))?;
let provider = app_context
.get_extension::<Arc<dyn alloy::providers::Provider>>()
.ok_or_else(|| EvmToolError::ProviderError("Provider not found in context".to_string()))?;
let balance = provider
.get_balance(addr)
.await
.map_err(|e| EvmToolError::ProviderError(format!("Failed to get balance: {}", e)))?;
let block_number = provider
.get_block_number()
.await
.map_err(|e| EvmToolError::ProviderError(format!("Failed to get block number: {}", e)))?;
let balance_in_eth = balance.to_string().parse::<f64>().unwrap_or(0.0) / 1e18;
let chain_name = get_chain_info(resolved_chain_id)
.map(|info| info.name)
.unwrap_or_else(|| "Unknown Chain".to_string());
info!(
"ETH balance for {} on chain {}: {} ETH",
address, resolved_chain_id, balance_in_eth
);
Ok(EthBalance {
balance_formatted: format!("{:.6} ETH", balance_in_eth),
unit: "ETH".to_string(),
raw_balance: balance.to_string(),
balance_raw: balance.to_string(),
address,
chain_name,
chain_id: resolved_chain_id,
block_number,
})
}
#[tool]
pub async fn get_eth_balance(
address: String,
chain_id: Option<u64>,
context: &riglr_core::provider::ApplicationContext,
) -> Result<EthBalance, ToolError> {
get_eth_balance_with_context(address, chain_id, context)
.await
.map_err(Into::into)
}
pub(crate) async fn get_erc20_balance_with_context(
token_address: String,
wallet_address: String,
chain_id: Option<u64>,
app_context: &ApplicationContext,
) -> Result<TokenBalance, EvmToolError> {
debug!(
"Getting ERC20 balance for wallet: {} token: {}",
wallet_address, token_address
);
let _resolved_chain_id = if let Some(id) = chain_id {
debug!("Using explicit chain_id: {}", id);
id
} else if let Ok(signer) = SignerContext::current_as_evm().await {
let id = signer.chain_id();
debug!("Using chain_id from EVM SignerContext: {}", id);
id
} else {
debug!("No explicit chain_id or EVM signer context, defaulting to Ethereum mainnet (1)");
1 };
let _ = _resolved_chain_id;
let token_addr = Address::from_str(&token_address)
.map_err(|e| EvmToolError::InvalidAddress(format!("Invalid token address: {}", e)))?;
let wallet_addr = Address::from_str(&wallet_address)
.map_err(|e| EvmToolError::InvalidAddress(format!("Invalid wallet address: {}", e)))?;
let provider = app_context
.get_extension::<Arc<dyn alloy::providers::Provider>>()
.ok_or_else(|| EvmToolError::ProviderError("Provider not found in context".to_string()))?;
let client = &**provider;
let decimals = get_token_decimals(client, token_addr).await?;
let token_symbol = get_token_symbol(client, token_addr).await.ok();
use alloy::dyn_abi::{DynSolType, DynSolValue};
use alloy::primitives::Bytes;
let function_selector = hex::decode("70a08231")
.map_err(|e| EvmToolError::InvalidParameter(format!("Failed to decode selector: {}", e)))?;
let param = DynSolValue::Address(wallet_addr);
let param_bytes = param.abi_encode_params();
let mut calldata = Vec::with_capacity(4 + param_bytes.len());
calldata.extend_from_slice(&function_selector);
calldata.extend_from_slice(¶m_bytes);
let tx_request = alloy::rpc::types::TransactionRequest::default()
.to(token_addr)
.input(Bytes::from(calldata).into());
let result = client
.call(tx_request)
.await
.map_err(|e| EvmToolError::ProviderError(format!("Failed to call balanceOf: {}", e)))?;
let balance_type = DynSolType::Uint(256);
let decoded = balance_type
.abi_decode(&result)
.map_err(|e| EvmToolError::ContractError(format!("Failed to decode balance: {}", e)))?;
let raw_balance = if let DynSolValue::Uint(value, _) = decoded {
value
} else {
return Err(EvmToolError::ContractError(
"Invalid balance format".to_string(),
));
};
let divisor = alloy::primitives::U256::from(10u64).pow(alloy::primitives::U256::from(decimals));
let whole = raw_balance / divisor;
let fraction = raw_balance % divisor;
let balance_formatted = if fraction.is_zero() {
whole.to_string()
} else {
let fraction_str = format!("{:0>width$}", fraction, width = decimals as usize);
let trimmed_fraction = fraction_str.trim_end_matches('0');
if trimmed_fraction.is_empty() {
whole.to_string()
} else {
format!("{}.{}", whole, trimmed_fraction)
}
};
info!(
"ERC20 balance check for token {} wallet {}: {} (raw: {})",
token_address, wallet_address, balance_formatted, raw_balance
);
Ok(TokenBalance {
balance_formatted,
token_symbol,
raw_balance: raw_balance.to_string(),
decimals,
token_address,
})
}
#[tool]
pub async fn get_erc20_balance(
token_address: String,
wallet_address: String,
chain_id: Option<u64>,
context: &riglr_core::provider::ApplicationContext,
) -> Result<TokenBalance, ToolError> {
get_erc20_balance_with_context(token_address, wallet_address, chain_id, context)
.await
.map_err(Into::into)
}
pub async fn get_token_symbol(
client: &dyn alloy::providers::Provider,
token_address: Address,
) -> Result<String, EvmToolError> {
debug!("Getting token symbol for: {:?}", token_address);
use alloy::dyn_abi::{DynSolType, DynSolValue};
use alloy::primitives::Bytes;
let function_selector = hex::decode("95d89b41")
.map_err(|e| EvmToolError::InvalidParameter(format!("Failed to decode selector: {}", e)))?;
let calldata = function_selector;
let tx_request = alloy::rpc::types::TransactionRequest::default()
.to(token_address)
.input(Bytes::from(calldata).into());
let result = client
.call(tx_request)
.await
.map_err(|e| EvmToolError::ProviderError(format!("Failed to call symbol: {}", e)))?;
let string_type = DynSolType::String;
let decoded = string_type
.abi_decode(&result)
.map_err(|e| EvmToolError::ContractError(format!("Failed to decode symbol: {}", e)))?;
if let DynSolValue::String(symbol) = decoded {
Ok(symbol)
} else {
Err(EvmToolError::ContractError(
"Invalid symbol format".to_string(),
))
}
}
pub async fn get_token_name(
client: &dyn alloy::providers::Provider,
token_address: Address,
) -> Result<String, EvmToolError> {
debug!("Getting token name for: {:?}", token_address);
use alloy::dyn_abi::{DynSolType, DynSolValue};
use alloy::primitives::Bytes;
let function_selector = hex::decode("06fdde03")
.map_err(|e| EvmToolError::InvalidParameter(format!("Failed to decode selector: {}", e)))?;
let calldata = function_selector;
let tx_request = alloy::rpc::types::TransactionRequest::default()
.to(token_address)
.input(Bytes::from(calldata).into());
let result = client
.call(tx_request)
.await
.map_err(|e| EvmToolError::ProviderError(format!("Failed to call name: {}", e)))?;
let string_type = DynSolType::String;
let decoded = string_type
.abi_decode(&result)
.map_err(|e| EvmToolError::ContractError(format!("Failed to decode name: {}", e)))?;
if let DynSolValue::String(name) = decoded {
Ok(name)
} else {
Err(EvmToolError::ContractError(
"Invalid name format".to_string(),
))
}
}
pub async fn get_token_decimals(
client: &dyn alloy::providers::Provider,
token_address: Address,
) -> Result<u8, EvmToolError> {
debug!("Getting token decimals for: {:?}", token_address);
use alloy::dyn_abi::{DynSolType, DynSolValue};
use alloy::primitives::Bytes;
let function_selector = hex::decode("313ce567")
.map_err(|e| EvmToolError::InvalidParameter(format!("Failed to decode selector: {}", e)))?;
let calldata = function_selector;
let tx_request = alloy::rpc::types::TransactionRequest::default()
.to(token_address)
.input(Bytes::from(calldata).into());
let result = client
.call(tx_request)
.await
.map_err(|e| EvmToolError::ProviderError(format!("Failed to call decimals: {}", e)))?;
let uint8_type = DynSolType::Uint(8);
let decoded = uint8_type
.abi_decode(&result)
.map_err(|e| EvmToolError::ContractError(format!("Failed to decode decimals: {}", e)))?;
if let DynSolValue::Uint(value, _) = decoded {
if value <= alloy::primitives::U256::from(255u8) {
Ok(value.to::<u8>())
} else {
Err(EvmToolError::ContractError(format!(
"Decimals value {} too large for u8",
value
)))
}
} else {
Err(EvmToolError::ContractError(
"Invalid decimals format".to_string(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use riglr_config::ConfigBuilder;
#[tokio::test]
async fn test_get_eth_balance_with_fantom_chain() {
let config = ConfigBuilder::default().build().unwrap();
let _context = ApplicationContext::from_config(&config);
let balance = EthBalance {
balance_formatted: "1.0 ETH".to_string(),
unit: "ETH".to_string(),
raw_balance: "1000000000000000000".to_string(),
balance_raw: "1000000000000000000".to_string(),
address: "0x742d35Cc67A5b747bE4C506C5e8b0A146d7b2E9e".to_string(),
chain_name: get_chain_info(250)
.map(|info| info.name)
.unwrap_or_else(|| "Unknown Chain".to_string()),
chain_id: 250,
block_number: 12345678,
};
assert_eq!(balance.chain_name, "Fantom");
assert_eq!(balance.chain_id, 250);
}
#[test]
fn test_chain_name_resolution() {
let test_cases = vec![
(1, "Ethereum"),
(137, "Polygon"),
(56, "BNB Smart Chain"),
(42161, "Arbitrum"),
(10, "Optimism"),
(250, "Fantom"), (43114, "Avalanche"), (8453, "Base"), ];
for (chain_id, expected_name) in test_cases {
let chain_name = get_chain_info(chain_id)
.map(|info| info.name)
.unwrap_or_else(|| "Unknown Chain".to_string());
assert_eq!(
chain_name, expected_name,
"Chain ID {} should map to {}",
chain_id, expected_name
);
}
}
#[test]
fn test_unknown_chain_fallback() {
let chain_name = get_chain_info(999999)
.map(|info| info.name)
.unwrap_or_else(|| "Unknown Chain".to_string());
assert_eq!(chain_name, "Unknown Chain");
}
}