use alloy::network::EthereumWallet;
use alloy::primitives::{Address, U256};
use alloy::providers::{Provider, ProviderBuilder};
use alloy::rpc::types::TransactionRequest;
use alloy::signers::local::PrivateKeySigner;
use async_trait::async_trait;
use std::str::FromStr;
use std::sync::Arc;
use tracing::{debug, info};
use crate::error::EvmToolError;
use riglr_config::EvmNetworkConfig;
use riglr_core::signer::{
EvmClient, EvmSigner as EvmSignerTrait, SignerBase, SignerError, UnifiedSigner,
};
use std::any::Any;
#[derive(Debug, Clone)]
pub struct GasConfig {
pub use_eip1559: bool,
pub gas_price_multiplier: f64,
pub max_gas_price: Option<U256>,
pub max_priority_fee: Option<U256>,
}
impl Default for GasConfig {
fn default() -> Self {
Self {
use_eip1559: true,
gas_price_multiplier: 1.1, max_gas_price: None,
max_priority_fee: Some(U256::from(2_000_000_000u64)), }
}
}
pub struct LocalEvmSigner {
wallet: EthereumWallet,
config: EvmNetworkConfig,
gas_config: GasConfig,
}
impl LocalEvmSigner {
pub fn new(private_key: String, config: EvmNetworkConfig) -> Result<Self, SignerError> {
let signer = PrivateKeySigner::from_str(&private_key).map_err(|e| {
SignerError::InvalidPrivateKey(format!("Invalid EVM private key: {}", e))
})?;
let wallet = EthereumWallet::from(signer);
Ok(Self {
wallet,
config,
gas_config: GasConfig::default(),
})
}
pub fn new_with_url(
private_key: String,
rpc_url: String,
chain_id: u64,
) -> Result<Self, SignerError> {
let config = EvmNetworkConfig::new("custom", chain_id, rpc_url);
Self::new(private_key, config)
}
pub fn new_with_gas_config(
private_key: String,
config: EvmNetworkConfig,
gas_config: GasConfig,
) -> Result<Self, SignerError> {
let signer = PrivateKeySigner::from_str(&private_key).map_err(|e| {
SignerError::InvalidPrivateKey(format!("Invalid EVM private key: {}", e))
})?;
let wallet = EthereumWallet::from(signer);
Ok(Self {
wallet,
config,
gas_config,
})
}
pub fn get_address(&self) -> Address {
self.wallet.default_signer().address()
}
async fn get_provider(&self) -> Result<impl Provider, SignerError> {
let provider = ProviderBuilder::new()
.wallet(self.wallet.clone())
.connect(&self.config.rpc_url)
.await
.map_err(|e| SignerError::ProviderError(format!("Failed to create provider: {}", e)))?;
Ok(provider)
}
pub async fn estimate_gas_price(&self) -> Result<u128, EvmToolError> {
let provider = self.get_provider().await.map_err(|e| {
EvmToolError::ProviderError(format!("Failed to create provider: {}", e))
})?;
if self.gas_config.use_eip1559 {
let base_fee = provider.get_gas_price().await.map_err(|e| {
EvmToolError::ProviderError(format!("Failed to get base fee: {}", e))
})?;
let priority_fee = self
.gas_config
.max_priority_fee
.unwrap_or_else(|| U256::from(1_000_000_000u64));
let total = base_fee + priority_fee.to::<u128>();
let adjusted = (total as f64 * self.gas_config.gas_price_multiplier) as u128;
if let Some(max) = self.gas_config.max_gas_price {
Ok(adjusted.min(max.to::<u128>()))
} else {
Ok(adjusted)
}
} else {
let gas_price = provider.get_gas_price().await.map_err(|e| {
EvmToolError::ProviderError(format!("Failed to get gas price: {}", e))
})?;
let adjusted = (gas_price as f64 * self.gas_config.gas_price_multiplier) as u128;
if let Some(max) = self.gas_config.max_gas_price {
Ok(adjusted.min(max.to::<u128>()))
} else {
Ok(adjusted)
}
}
}
pub async fn estimate_gas_limit(&self, tx: &TransactionRequest) -> Result<u64, EvmToolError> {
let provider = self.get_provider().await.map_err(|e| {
EvmToolError::ProviderError(format!("Failed to create provider: {}", e))
})?;
let estimate = provider
.estimate_gas(tx.clone())
.await
.map_err(|_e| EvmToolError::GasEstimationFailed)?;
Ok((estimate as f64 * 1.2) as u64)
}
pub async fn prepare_transaction(
&self,
mut tx: TransactionRequest,
) -> Result<TransactionRequest, EvmToolError> {
if tx.gas.is_none() {
let gas_limit = self.estimate_gas_limit(&tx).await?;
tx.gas = Some(gas_limit);
debug!("Set gas limit to {}", gas_limit);
}
let provider = self.get_provider().await.map_err(|e| {
EvmToolError::ProviderError(format!("Failed to create provider: {}", e))
})?;
if self.gas_config.use_eip1559 {
if tx.max_fee_per_gas.is_none() || tx.max_priority_fee_per_gas.is_none() {
let base_fee = provider.get_gas_price().await.map_err(|e| {
EvmToolError::ProviderError(format!("Failed to get base fee: {}", e))
})?;
let priority_fee = self
.gas_config
.max_priority_fee
.unwrap_or_else(|| U256::from(2_000_000_000u64));
let max_priority_fee = priority_fee.to::<u128>();
let max_fee = base_fee + max_priority_fee * 2;
tx.max_priority_fee_per_gas = Some(max_priority_fee);
tx.max_fee_per_gas = Some(max_fee);
debug!(
"Set EIP-1559 gas: max_fee={}, priority_fee={}",
max_fee, priority_fee
);
}
} else if tx.gas_price.is_none() {
let gas_price = self.estimate_gas_price().await?;
tx.gas_price = Some(gas_price);
debug!("Set gas price to {}", gas_price);
}
Ok(tx)
}
pub async fn simulate_transaction(&self, tx: &TransactionRequest) -> Result<(), EvmToolError> {
let provider = self.get_provider().await.map_err(|e| {
EvmToolError::ProviderError(format!("Failed to create provider: {}", e))
})?;
let _result =
provider
.call(tx.clone())
.await
.map_err(|e| EvmToolError::TransactionReverted {
reason: format!("Transaction simulation failed: {}", e),
})?;
info!("Transaction simulation successful");
Ok(())
}
}
impl std::fmt::Debug for LocalEvmSigner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LocalEvmSigner")
.field("address", &self.get_address().to_string())
.field("chain_id", &self.config.chain_id)
.field("network", &self.config.name)
.field("gas_config", &self.gas_config)
.finish()
}
}
impl SignerBase for LocalEvmSigner {
fn locale(&self) -> String {
"en".to_string()
}
fn user_id(&self) -> Option<String> {
None
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[async_trait]
impl EvmSignerTrait for LocalEvmSigner {
fn chain_id(&self) -> u64 {
self.config.chain_id
}
fn address(&self) -> String {
self.get_address().to_string()
}
async fn sign_and_send_transaction(
&self,
tx_json: serde_json::Value,
) -> Result<String, SignerError> {
let mut tx: TransactionRequest = serde_json::from_value(tx_json)
.map_err(|e| SignerError::Signing(format!("Failed to parse transaction: {}", e)))?;
if tx.chain_id.is_none() {
tx.chain_id = Some(self.config.chain_id);
}
let provider = self.get_provider().await?;
let pending_tx = provider.send_transaction(tx).await.map_err(|e| {
SignerError::TransactionFailed(format!("Failed to send transaction: {}", e))
})?;
let tx_hash = pending_tx.tx_hash().to_string();
let _receipt = pending_tx
.get_receipt()
.await
.map_err(|e| SignerError::TransactionFailed(format!("Failed to get receipt: {}", e)))?;
Ok(tx_hash.to_string())
}
fn client(&self) -> Result<Arc<dyn EvmClient>, SignerError> {
Ok(Arc::new(EvmClientImpl {
wallet: self.wallet.clone(),
provider_url: self.config.rpc_url.clone(),
chain_id: self.config.chain_id,
}))
}
}
impl UnifiedSigner for LocalEvmSigner {
fn supports_solana(&self) -> bool {
false
}
fn supports_evm(&self) -> bool {
true
}
fn as_solana(&self) -> Option<&dyn riglr_core::signer::granular_traits::SolanaSigner> {
None
}
fn as_evm(&self) -> Option<&dyn EvmSignerTrait> {
Some(self)
}
fn as_multi_chain(&self) -> Option<&dyn riglr_core::signer::granular_traits::MultiChainSigner> {
None
}
}
#[derive(Debug)]
struct EvmClientImpl {
wallet: EthereumWallet,
provider_url: String,
chain_id: u64,
}
#[async_trait]
impl EvmClient for EvmClientImpl {
async fn get_balance(&self, address: &str) -> Result<String, SignerError> {
let provider = ProviderBuilder::new()
.wallet(self.wallet.clone())
.connect(&self.provider_url)
.await
.map_err(|e| SignerError::ProviderError(format!("Failed to create provider: {}", e)))?;
let address = address.parse::<Address>().map_err(|e| {
SignerError::InvalidPrivateKey(format!("Invalid address format: {}", e))
})?;
let balance = provider
.get_balance(address)
.await
.map_err(|e| SignerError::ProviderError(format!("Failed to get balance: {}", e)))?;
Ok(balance.to_string())
}
async fn send_transaction(&self, tx_json: &serde_json::Value) -> Result<String, SignerError> {
let tx: TransactionRequest = serde_json::from_value(tx_json.clone())
.map_err(|e| SignerError::Signing(format!("Failed to parse transaction: {}", e)))?;
let provider = ProviderBuilder::new()
.wallet(self.wallet.clone())
.connect(&self.provider_url)
.await
.map_err(|e| SignerError::ProviderError(format!("Failed to create provider: {}", e)))?;
let mut tx_request = tx.clone();
if tx_request.chain_id.is_none() {
tx_request.chain_id = Some(self.chain_id);
}
let pending_tx = provider.send_transaction(tx_request).await.map_err(|e| {
SignerError::TransactionFailed(format!("Failed to send transaction: {}", e))
})?;
Ok(pending_tx.tx_hash().to_string())
}
async fn call(&self, tx_json: &serde_json::Value) -> Result<String, SignerError> {
let tx: TransactionRequest = serde_json::from_value(tx_json.clone())
.map_err(|e| SignerError::Signing(format!("Failed to parse transaction: {}", e)))?;
let provider = ProviderBuilder::new()
.wallet(self.wallet.clone())
.connect(&self.provider_url)
.await
.map_err(|e| SignerError::ProviderError(format!("Failed to create provider: {}", e)))?;
let result = provider
.call(tx.clone())
.await
.map_err(|e| SignerError::ProviderError(format!("Failed to call contract: {}", e)))?;
Ok(result.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gas_config_default() {
let config = GasConfig::default();
assert!(config.use_eip1559);
assert_eq!(config.gas_price_multiplier, 1.1);
assert!(config.max_gas_price.is_none());
assert_eq!(config.max_priority_fee, Some(U256::from(2_000_000_000u64)));
}
#[test]
fn test_gas_config_debug_clone() {
let config = GasConfig {
use_eip1559: false,
gas_price_multiplier: 1.5,
max_gas_price: Some(U256::from(50_000_000_000u64)),
max_priority_fee: Some(U256::from(1_000_000_000u64)),
};
let cloned = config.clone();
assert_eq!(config.use_eip1559, cloned.use_eip1559);
assert_eq!(config.gas_price_multiplier, cloned.gas_price_multiplier);
assert_eq!(config.max_gas_price, cloned.max_gas_price);
assert_eq!(config.max_priority_fee, cloned.max_priority_fee);
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("GasConfig"));
}
use alloy::primitives::Bytes;
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone)]
struct MockProvider {
gas_price_responses: Arc<Mutex<Vec<Result<u128, MockProviderError>>>>,
gas_estimate_responses: Arc<Mutex<Vec<Result<u64, MockProviderError>>>>,
call_responses: Arc<Mutex<Vec<Result<Bytes, MockProviderError>>>>,
balance_responses: Arc<Mutex<Vec<Result<U256, MockProviderError>>>>,
method_calls: Arc<Mutex<Vec<String>>>,
}
#[derive(Debug, Clone)]
enum MockProviderError {
Rpc(String),
Network(String),
InsufficientFunds,
}
impl std::fmt::Display for MockProviderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MockProviderError::Rpc(msg) => write!(f, "RPC error: {}", msg),
MockProviderError::Network(msg) => write!(f, "Network error: {}", msg),
MockProviderError::InsufficientFunds => write!(f, "Insufficient funds"),
}
}
}
impl std::error::Error for MockProviderError {}
impl MockProvider {
fn new() -> Self {
Self {
gas_price_responses: Arc::new(Mutex::new(Vec::new())),
gas_estimate_responses: Arc::new(Mutex::new(Vec::new())),
call_responses: Arc::new(Mutex::new(Vec::new())),
balance_responses: Arc::new(Mutex::new(Vec::new())),
method_calls: Arc::new(Mutex::new(Vec::new())),
}
}
fn set_gas_price_response(&self, response: Result<u128, MockProviderError>) {
self.gas_price_responses.lock().unwrap().push(response);
}
fn set_gas_estimate_response(&self, response: Result<u64, MockProviderError>) {
self.gas_estimate_responses.lock().unwrap().push(response);
}
fn set_call_response(&self, response: Result<Bytes, MockProviderError>) {
self.call_responses.lock().unwrap().push(response);
}
fn set_balance_response(&self, response: Result<U256, MockProviderError>) {
self.balance_responses.lock().unwrap().push(response);
}
fn get_method_calls(&self) -> Vec<String> {
self.method_calls.lock().unwrap().clone()
}
fn record_call(&self, method: &str) {
self.method_calls.lock().unwrap().push(method.to_string());
}
fn pop_gas_price_response(&self) -> Option<Result<u128, MockProviderError>> {
self.gas_price_responses.lock().unwrap().pop()
}
fn pop_gas_estimate_response(&self) -> Option<Result<u64, MockProviderError>> {
self.gas_estimate_responses.lock().unwrap().pop()
}
fn pop_call_response(&self) -> Option<Result<Bytes, MockProviderError>> {
self.call_responses.lock().unwrap().pop()
}
fn pop_balance_response(&self) -> Option<Result<U256, MockProviderError>> {
self.balance_responses.lock().unwrap().pop()
}
}
fn create_test_signer_with_gas_config(gas_config: GasConfig) -> LocalEvmSigner {
let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
let config = riglr_config::EvmNetworkConfig::new("test", 1, "https://test.rpc".to_string());
LocalEvmSigner::new_with_gas_config(private_key.to_string(), config, gas_config)
.expect("Failed to create test signer")
}
#[test]
fn test_gas_config_custom_values() {
let gas_config = GasConfig {
use_eip1559: false,
gas_price_multiplier: 1.5,
max_gas_price: Some(U256::from(100_000_000_000u64)), max_priority_fee: Some(U256::from(3_000_000_000u64)), };
let signer = create_test_signer_with_gas_config(gas_config);
assert!(!signer.gas_config.use_eip1559);
assert_eq!(signer.gas_config.gas_price_multiplier, 1.5);
assert_eq!(
signer.gas_config.max_gas_price,
Some(U256::from(100_000_000_000u64))
);
assert_eq!(
signer.gas_config.max_priority_fee,
Some(U256::from(3_000_000_000u64))
);
}
#[test]
fn test_gas_config_eip1559_enabled() {
let gas_config = GasConfig {
use_eip1559: true,
gas_price_multiplier: 1.2,
max_gas_price: Some(U256::from(200_000_000_000u64)), max_priority_fee: Some(U256::from(5_000_000_000u64)), };
let signer = create_test_signer_with_gas_config(gas_config);
assert!(signer.gas_config.use_eip1559);
assert_eq!(signer.gas_config.gas_price_multiplier, 1.2);
assert_eq!(
signer.gas_config.max_gas_price,
Some(U256::from(200_000_000_000u64))
);
assert_eq!(
signer.gas_config.max_priority_fee,
Some(U256::from(5_000_000_000u64))
);
}
#[test]
fn test_gas_config_no_max_limits() {
let gas_config = GasConfig {
use_eip1559: true,
gas_price_multiplier: 2.0,
max_gas_price: None,
max_priority_fee: None,
};
let signer = create_test_signer_with_gas_config(gas_config);
assert!(signer.gas_config.use_eip1559);
assert_eq!(signer.gas_config.gas_price_multiplier, 2.0);
assert_eq!(signer.gas_config.max_gas_price, None);
assert_eq!(signer.gas_config.max_priority_fee, None);
}
#[tokio::test]
async fn test_transaction_request_preparation() {
use alloy::rpc::types::TransactionRequest;
let gas_config = GasConfig::default();
let _signer = create_test_signer_with_gas_config(gas_config);
let tx = TransactionRequest {
to: Some(alloy::primitives::Address::ZERO.into()),
value: Some(U256::from(1000000000000000000u64)), ..Default::default()
};
assert!(tx.gas.is_none()); assert!(tx.gas_price.is_none()); assert!(tx.max_fee_per_gas.is_none()); }
#[test]
fn test_mock_provider_setup() {
let mock = MockProvider::new();
mock.set_gas_price_response(Ok(20_000_000_000u128)); mock.set_gas_estimate_response(Ok(21000u64)); mock.set_call_response(Ok(Bytes::new()));
mock.set_balance_response(Ok(U256::from(1_000_000_000_000_000_000u64)));
assert_eq!(
mock.pop_gas_price_response().unwrap().unwrap(),
20_000_000_000u128
);
assert_eq!(mock.pop_gas_estimate_response().unwrap().unwrap(), 21000u64);
assert!(mock.pop_call_response().unwrap().is_ok());
assert_eq!(
mock.pop_balance_response().unwrap().unwrap(),
U256::from(1_000_000_000_000_000_000u64)
);
}
#[test]
fn test_mock_provider_error_responses() {
let mock = MockProvider::new();
mock.set_gas_price_response(Err(MockProviderError::Network(
"Connection timeout".to_string(),
)));
mock.set_gas_estimate_response(Err(MockProviderError::Rpc(
"Invalid transaction".to_string(),
)));
mock.set_balance_response(Err(MockProviderError::InsufficientFunds));
let gas_price_error = mock.pop_gas_price_response().unwrap().unwrap_err();
assert!(gas_price_error.to_string().contains("Connection timeout"));
let gas_estimate_error = mock.pop_gas_estimate_response().unwrap().unwrap_err();
assert!(gas_estimate_error
.to_string()
.contains("Invalid transaction"));
let balance_error = mock.pop_balance_response().unwrap().unwrap_err();
assert!(balance_error.to_string().contains("Insufficient funds"));
}
#[test]
fn test_mock_provider_method_tracking() {
let mock = MockProvider::new();
mock.record_call("get_gas_price");
mock.record_call("estimate_gas");
mock.record_call("get_balance");
mock.record_call("send_transaction");
let calls = mock.get_method_calls();
assert_eq!(calls.len(), 4);
assert_eq!(calls[0], "get_gas_price");
assert_eq!(calls[1], "estimate_gas");
assert_eq!(calls[2], "get_balance");
assert_eq!(calls[3], "send_transaction");
}
#[test]
fn test_evm_signer_creation_with_different_configs() {
let signer1 = create_test_signer_with_gas_config(GasConfig::default());
assert!(signer1.gas_config.use_eip1559);
let legacy_config = GasConfig {
use_eip1559: false,
gas_price_multiplier: 1.0,
max_gas_price: Some(U256::from(50_000_000_000u64)),
max_priority_fee: None,
};
let signer2 = create_test_signer_with_gas_config(legacy_config);
assert!(!signer2.gas_config.use_eip1559);
assert_eq!(signer2.gas_config.gas_price_multiplier, 1.0);
}
#[test]
fn test_debug_formatting() {
let signer = create_test_signer_with_gas_config(GasConfig::default());
let debug_str = format!("{:?}", signer);
assert!(debug_str.contains("LocalEvmSigner"));
assert!(debug_str.contains("address"));
assert!(debug_str.contains("chain_id"));
assert!(debug_str.contains("gas_config"));
}
}