use alloy::{
primitives::{Bytes, TxKind, Uint},
providers::{Provider, ProviderBuilder, RootProvider},
rpc::types::{
Block as BlockResponse, BlockNumberOrTag, BlockTransactionsKind, FeeHistory,
TransactionInput, TransactionReceipt, TransactionRequest,
},
transports::http::{Client, Http},
};
use async_trait::async_trait;
use eyre::{eyre, Result};
use crate::models::{EvmTransactionData, TransactionError, U256};
#[cfg(test)]
use mockall::automock;
#[derive(Clone)]
pub struct EvmProvider {
provider: RootProvider<Http<Client>>,
}
#[async_trait]
#[cfg_attr(test, automock)]
#[allow(dead_code)]
pub trait EvmProviderTrait: Send + Sync {
async fn get_balance(&self, address: &str) -> Result<U256>;
async fn get_block_number(&self) -> Result<u64>;
async fn estimate_gas(&self, tx: &EvmTransactionData) -> Result<u64>;
async fn get_gas_price(&self) -> Result<u128>;
async fn send_transaction(&self, tx: TransactionRequest) -> Result<String>;
async fn send_raw_transaction(&self, tx: &[u8]) -> Result<String>;
async fn health_check(&self) -> Result<bool>;
async fn get_transaction_count(&self, address: &str) -> Result<u64>;
async fn get_fee_history(
&self,
block_count: u64,
newest_block: BlockNumberOrTag,
reward_percentiles: Vec<f64>,
) -> Result<FeeHistory>;
async fn get_block_by_number(&self) -> Result<BlockResponse>;
async fn get_transaction_receipt(&self, tx_hash: &str) -> Result<Option<TransactionReceipt>>;
async fn call_contract(&self, tx: &TransactionRequest) -> Result<Bytes>;
}
impl EvmProvider {
pub fn new(url: &str) -> Result<Self> {
let rpc_url = url.parse()?;
let provider = ProviderBuilder::new().on_http(rpc_url);
Ok(Self { provider })
}
}
impl AsRef<EvmProvider> for EvmProvider {
fn as_ref(&self) -> &EvmProvider {
self
}
}
#[async_trait]
impl EvmProviderTrait for EvmProvider {
async fn get_balance(&self, address: &str) -> Result<U256> {
let address = address.parse()?;
self.provider
.get_balance(address)
.await
.map_err(|e| eyre!("Failed to get balance: {}", e))
}
async fn get_block_number(&self) -> Result<u64> {
self.provider
.get_block_number()
.await
.map_err(|e| eyre!("Failed to get block number: {}", e))
}
async fn estimate_gas(&self, tx: &EvmTransactionData) -> Result<u64> {
let transaction_request = TransactionRequest::try_from(tx)?;
self.provider
.estimate_gas(&transaction_request)
.await
.map_err(|e| eyre!("Failed to estimate gas: {}", e))
}
async fn get_gas_price(&self) -> Result<u128> {
self.provider
.get_gas_price()
.await
.map_err(|e| eyre!("Failed to get gas price: {}", e))
}
async fn send_transaction(&self, tx: TransactionRequest) -> Result<String> {
let pending_tx = self
.provider
.send_transaction(tx)
.await
.map_err(|e| eyre!("Failed to send transaction: {}", e))?;
let tx_hash = pending_tx.tx_hash().to_string();
Ok(tx_hash)
}
async fn send_raw_transaction(&self, tx: &[u8]) -> Result<String> {
let pending_tx = self
.provider
.send_raw_transaction(tx)
.await
.map_err(|e| eyre!("Failed to send raw transaction: {}", e))?;
let tx_hash = pending_tx.tx_hash().to_string();
Ok(tx_hash)
}
async fn health_check(&self) -> Result<bool> {
self.get_block_number()
.await
.map(|_| true)
.map_err(|e| eyre!("Health check failed: {}", e))
}
async fn get_transaction_count(&self, address: &str) -> Result<u64> {
let address = address.parse()?;
let result = self
.provider
.get_transaction_count(address)
.await
.map_err(|e| eyre!("Health check failed: {}", e))?;
Ok(result)
}
async fn get_fee_history(
&self,
block_count: u64,
newest_block: BlockNumberOrTag,
reward_percentiles: Vec<f64>,
) -> Result<FeeHistory> {
let fee_history = self
.provider
.get_fee_history(block_count, newest_block, &reward_percentiles)
.await
.map_err(|e| eyre!("Failed to get fee history: {}", e))?;
Ok(fee_history)
}
async fn get_block_by_number(&self) -> Result<BlockResponse> {
self.provider
.get_block_by_number(BlockNumberOrTag::Latest, BlockTransactionsKind::Hashes)
.await
.map_err(|e| eyre!("Failed to get block by number: {}", e))?
.ok_or_else(|| eyre!("Block not found"))
}
async fn get_transaction_receipt(&self, tx_hash: &str) -> Result<Option<TransactionReceipt>> {
let tx_hash = tx_hash.parse()?;
self.provider
.get_transaction_receipt(tx_hash)
.await
.map_err(|e| eyre!("Failed to get transaction receipt: {}", e))
}
async fn call_contract(&self, tx: &TransactionRequest) -> Result<Bytes> {
self.provider
.call(tx)
.await
.map_err(|e| eyre!("Failed to call contract: {}", e))
}
}
impl TryFrom<&EvmTransactionData> for TransactionRequest {
type Error = TransactionError;
fn try_from(tx: &EvmTransactionData) -> Result<Self, Self::Error> {
Ok(TransactionRequest {
from: Some(tx.from.clone().parse().map_err(|_| {
TransactionError::InvalidType("Invalid address format".to_string())
})?),
to: Some(TxKind::Call(
tx.to
.clone()
.unwrap_or("".to_string())
.parse()
.map_err(|_| {
TransactionError::InvalidType("Invalid address format".to_string())
})?,
)),
gas_price: Some(
Uint::<256, 4>::from(tx.gas_price.unwrap_or(0))
.try_into()
.map_err(|_| TransactionError::InvalidType("Invalid gas price".to_string()))?,
),
value: Some(Uint::<256, 4>::from(tx.value)),
input: TransactionInput::from(
hex::decode(tx.data.clone().unwrap_or("".to_string()).into_bytes()).map_err(
|e| TransactionError::InvalidType(format!("Invalid hex data: {}", e)),
)?,
),
nonce: Some(
Uint::<256, 4>::from(tx.nonce.ok_or_else(|| {
TransactionError::InvalidType("Nonce must be defined".to_string())
})?)
.try_into()
.map_err(|_| TransactionError::InvalidType("Invalid nonce".to_string()))?,
),
chain_id: Some(tx.chain_id),
..Default::default()
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy::primitives::Address;
use futures::FutureExt;
use std::str::FromStr;
#[test]
fn test_new_provider() {
let provider = EvmProvider::new("http://localhost:8545");
assert!(provider.is_ok());
let provider = EvmProvider::new("invalid-url");
assert!(provider.is_err());
}
#[test]
fn test_transaction_request_conversion() {
let tx_data = EvmTransactionData {
from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
gas_price: Some(1000000000),
value: Uint::<256, 4>::from(1000000000),
data: Some("0x".to_string()),
nonce: Some(1),
chain_id: 1,
gas_limit: 21000,
hash: None,
signature: None,
speed: None,
max_fee_per_gas: None,
max_priority_fee_per_gas: None,
raw: None,
};
let result = TransactionRequest::try_from(&tx_data);
assert!(result.is_ok());
let tx_request = result.unwrap();
assert_eq!(
tx_request.from,
Some(Address::from_str("0x742d35Cc6634C0532925a3b844Bc454e4438f44e").unwrap())
);
assert_eq!(tx_request.chain_id, Some(1));
}
#[tokio::test]
async fn test_mock_provider_methods() {
let mut mock = MockEvmProviderTrait::new();
mock.expect_get_balance()
.with(mockall::predicate::eq(
"0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
))
.times(1)
.returning(|_| async { Ok(U256::from(100)) }.boxed());
mock.expect_get_block_number()
.times(1)
.returning(|| async { Ok(12345) }.boxed());
mock.expect_get_gas_price()
.times(1)
.returning(|| async { Ok(20000000000) }.boxed());
mock.expect_health_check()
.times(1)
.returning(|| async { Ok(true) }.boxed());
mock.expect_get_transaction_count()
.with(mockall::predicate::eq(
"0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
))
.times(1)
.returning(|_| async { Ok(42) }.boxed());
mock.expect_get_fee_history()
.with(
mockall::predicate::eq(10u64),
mockall::predicate::eq(BlockNumberOrTag::Latest),
mockall::predicate::eq(vec![25.0, 50.0, 75.0]),
)
.times(1)
.returning(|_, _, _| {
async {
Ok(FeeHistory {
oldest_block: 100,
base_fee_per_gas: vec![1000],
gas_used_ratio: vec![0.5],
reward: Some(vec![vec![500]]),
base_fee_per_blob_gas: vec![1000],
blob_gas_used_ratio: vec![0.5],
})
}
.boxed()
});
let balance = mock
.get_balance("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
.await;
assert!(balance.is_ok());
assert_eq!(balance.unwrap(), U256::from(100));
let block_number = mock.get_block_number().await;
assert!(block_number.is_ok());
assert_eq!(block_number.unwrap(), 12345);
let gas_price = mock.get_gas_price().await;
assert!(gas_price.is_ok());
assert_eq!(gas_price.unwrap(), 20000000000);
let health = mock.health_check().await;
assert!(health.is_ok());
assert!(health.unwrap());
let count = mock
.get_transaction_count("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
.await;
assert!(count.is_ok());
assert_eq!(count.unwrap(), 42);
let fee_history = mock
.get_fee_history(10, BlockNumberOrTag::Latest, vec![25.0, 50.0, 75.0])
.await;
assert!(fee_history.is_ok());
let fee_history = fee_history.unwrap();
assert_eq!(fee_history.oldest_block, 100);
assert_eq!(fee_history.gas_used_ratio, vec![0.5]);
}
#[tokio::test]
async fn test_mock_transaction_operations() {
let mut mock = MockEvmProviderTrait::new();
let tx_data = EvmTransactionData {
from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
gas_price: Some(1000000000),
value: Uint::<256, 4>::from(1000000000),
data: Some("0x".to_string()),
nonce: Some(1),
chain_id: 1,
gas_limit: 21000,
hash: None,
signature: None,
speed: None,
max_fee_per_gas: None,
max_priority_fee_per_gas: None,
raw: None,
};
mock.expect_estimate_gas()
.with(mockall::predicate::always())
.times(1)
.returning(|_| async { Ok(21000) }.boxed());
mock.expect_send_raw_transaction()
.with(mockall::predicate::always())
.times(1)
.returning(|_| async { Ok("0x123456789abcdef".to_string()) }.boxed());
let gas_estimate = mock.estimate_gas(&tx_data).await;
assert!(gas_estimate.is_ok());
assert_eq!(gas_estimate.unwrap(), 21000);
let tx_hash = mock.send_raw_transaction(&[0u8; 32]).await;
assert!(tx_hash.is_ok());
assert_eq!(tx_hash.unwrap(), "0x123456789abcdef");
}
#[test]
fn test_invalid_transaction_request_conversion() {
let tx_data = EvmTransactionData {
from: "invalid-address".to_string(),
to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string()),
gas_price: Some(1000000000),
value: Uint::<256, 4>::from(1000000000),
data: Some("0x".to_string()),
nonce: Some(1),
chain_id: 1,
gas_limit: 21000,
hash: None,
signature: None,
speed: None,
max_fee_per_gas: None,
max_priority_fee_per_gas: None,
raw: None,
};
let result = TransactionRequest::try_from(&tx_data);
assert!(result.is_err());
}
#[tokio::test]
async fn test_mock_additional_methods() {
let mut mock = MockEvmProviderTrait::new();
mock.expect_health_check()
.times(1)
.returning(|| async { Ok(true) }.boxed());
mock.expect_get_transaction_count()
.with(mockall::predicate::eq(
"0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
))
.times(1)
.returning(|_| async { Ok(42) }.boxed());
mock.expect_get_fee_history()
.with(
mockall::predicate::eq(10u64),
mockall::predicate::eq(BlockNumberOrTag::Latest),
mockall::predicate::eq(vec![25.0, 50.0, 75.0]),
)
.times(1)
.returning(|_, _, _| {
async {
Ok(FeeHistory {
oldest_block: 100,
base_fee_per_gas: vec![1000],
gas_used_ratio: vec![0.5],
reward: Some(vec![vec![500]]),
base_fee_per_blob_gas: vec![1000],
blob_gas_used_ratio: vec![0.5],
})
}
.boxed()
});
let health = mock.health_check().await;
assert!(health.is_ok());
assert!(health.unwrap());
let count = mock
.get_transaction_count("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
.await;
assert!(count.is_ok());
assert_eq!(count.unwrap(), 42);
let fee_history = mock
.get_fee_history(10, BlockNumberOrTag::Latest, vec![25.0, 50.0, 75.0])
.await;
assert!(fee_history.is_ok());
let fee_history = fee_history.unwrap();
assert_eq!(fee_history.oldest_block, 100);
assert_eq!(fee_history.gas_used_ratio, vec![0.5]);
}
#[tokio::test]
async fn test_call_contract() {
let mut mock = MockEvmProviderTrait::new();
let tx = TransactionRequest {
from: Some(Address::from_str("0x742d35Cc6634C0532925a3b844Bc454e4438f44e").unwrap()),
to: Some(TxKind::Call(
Address::from_str("0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC").unwrap(),
)),
input: TransactionInput::from(
hex::decode("a9059cbb000000000000000000000000742d35cc6634c0532925a3b844bc454e4438f44e0000000000000000000000000000000000000000000000000de0b6b3a7640000").unwrap()
),
..Default::default()
};
mock.expect_call_contract()
.with(mockall::predicate::always())
.times(1)
.returning(|_| {
async {
Ok(Bytes::from(
hex::decode(
"0000000000000000000000000000000000000000000000000000000000000001",
)
.unwrap(),
))
}
.boxed()
});
let result = mock.call_contract(&tx).await;
assert!(result.is_ok());
let data = result.unwrap();
assert_eq!(
hex::encode(data),
"0000000000000000000000000000000000000000000000000000000000000001"
);
}
}