use alloy::network::Ethereum;
use alloy::primitives::{Address, Bytes, U256};
use alloy::providers::{DynProvider, Provider, ProviderBuilder};
use alloy::rpc::types::eth::{TransactionReceipt, TransactionRequest};
use async_trait::async_trait;
use super::provider::{ChainFamily, ChainProvider, TxStatus};
use crate::core::types::ExchangeError;
const BALANCE_OF_SELECTOR: [u8; 4] = [0x70, 0xa0, 0x82, 0x31];
#[async_trait]
pub trait EvmChain: ChainProvider {
async fn eth_call(&self, call: TransactionRequest) -> Result<Bytes, ExchangeError>;
async fn estimate_gas(&self, call: TransactionRequest) -> Result<u64, ExchangeError>;
async fn gas_price(&self) -> Result<U256, ExchangeError>;
async fn max_priority_fee(&self) -> Result<U256, ExchangeError>;
async fn get_receipt(&self, tx_hash: &str) -> Result<Option<TransactionReceipt>, ExchangeError>;
async fn erc20_balance(&self, token: Address, account: Address) -> Result<U256, ExchangeError>;
fn inner(&self) -> &DynProvider<Ethereum>;
}
pub struct EvmProvider {
provider: DynProvider<Ethereum>,
chain_id: u64,
chain_name: String,
}
impl EvmProvider {
pub fn new(provider: DynProvider<Ethereum>, chain_id: u64, chain_name: &str) -> Self {
Self {
provider,
chain_id,
chain_name: chain_name.to_lowercase(),
}
}
pub async fn connect_http(
rpc_url: &str,
chain_id: u64,
chain_name: &str,
) -> Result<Self, ExchangeError> {
let url: reqwest::Url = rpc_url.parse().map_err(|e| {
ExchangeError::InvalidRequest(format!("Invalid RPC URL '{}': {}", rpc_url, e))
})?;
let provider = ProviderBuilder::new().connect_http(url);
Ok(Self::new(DynProvider::new(provider), chain_id, chain_name))
}
pub fn ethereum(rpc_url: &str) -> Result<Self, ExchangeError> {
let url: reqwest::Url = rpc_url.parse().map_err(|e| {
ExchangeError::InvalidRequest(format!("Invalid Ethereum RPC URL '{}': {}", rpc_url, e))
})?;
let provider = ProviderBuilder::new().connect_http(url);
Ok(Self::new(DynProvider::new(provider), 1, "ethereum"))
}
pub fn arbitrum() -> Self {
let url: reqwest::Url = "https://arb1.arbitrum.io/rpc".parse().expect("static URL");
let provider = ProviderBuilder::new().connect_http(url);
Self::new(DynProvider::new(provider), 42161, "arbitrum")
}
pub fn base() -> Self {
let url: reqwest::Url = "https://mainnet.base.org".parse().expect("static URL");
let provider = ProviderBuilder::new().connect_http(url);
Self::new(DynProvider::new(provider), 8453, "base")
}
pub fn polygon() -> Self {
let url: reqwest::Url = "https://polygon-rpc.com".parse().expect("static URL");
let provider = ProviderBuilder::new().connect_http(url);
Self::new(DynProvider::new(provider), 137, "polygon")
}
pub fn avalanche() -> Self {
let url: reqwest::Url = "https://api.avax.network/ext/bc/C/rpc"
.parse()
.expect("static URL");
let provider = ProviderBuilder::new().connect_http(url);
Self::new(DynProvider::new(provider), 43114, "avalanche")
}
pub fn bsc() -> Self {
let url: reqwest::Url = "https://bsc-dataseed.binance.org".parse().expect("static URL");
let provider = ProviderBuilder::new().connect_http(url);
Self::new(DynProvider::new(provider), 56, "bsc")
}
pub fn optimism() -> Self {
let url: reqwest::Url = "https://mainnet.optimism.io".parse().expect("static URL");
let provider = ProviderBuilder::new().connect_http(url);
Self::new(DynProvider::new(provider), 10, "optimism")
}
pub fn chain_name(&self) -> &str {
&self.chain_name
}
}
#[async_trait]
impl ChainProvider for EvmProvider {
fn chain_family(&self) -> ChainFamily {
ChainFamily::Evm { chain_id: self.chain_id }
}
async fn broadcast_tx(&self, tx_bytes: &[u8]) -> Result<String, ExchangeError> {
let pending = self
.provider
.send_raw_transaction(tx_bytes)
.await
.map_err(|e| ExchangeError::Network(format!("send_raw_transaction failed: {}", e)))?;
Ok(format!("{:#x}", pending.tx_hash()))
}
async fn get_height(&self) -> Result<u64, ExchangeError> {
self.provider
.get_block_number()
.await
.map_err(|e| ExchangeError::Network(format!("eth_blockNumber failed: {}", e)))
}
async fn get_nonce(&self, address: &str) -> Result<u64, ExchangeError> {
let addr: Address = address.parse().map_err(|e| {
ExchangeError::InvalidRequest(format!("Invalid address '{}': {}", address, e))
})?;
let count = self
.provider
.get_transaction_count(addr)
.await
.map_err(|e| ExchangeError::Network(format!("eth_getTransactionCount failed: {}", e)))?;
Ok(count)
}
async fn get_native_balance(&self, address: &str) -> Result<String, ExchangeError> {
let addr: Address = address.parse().map_err(|e| {
ExchangeError::InvalidRequest(format!("Invalid address '{}': {}", address, e))
})?;
let balance = self
.provider
.get_balance(addr)
.await
.map_err(|e| ExchangeError::Network(format!("eth_getBalance failed: {}", e)))?;
Ok(balance.to_string())
}
async fn get_tx_status(&self, tx_hash: &str) -> Result<TxStatus, ExchangeError> {
let hash: alloy::primitives::TxHash = tx_hash.parse().map_err(|e| {
ExchangeError::InvalidRequest(format!("Invalid tx hash '{}': {}", tx_hash, e))
})?;
let receipt = self
.provider
.get_transaction_receipt(hash)
.await
.map_err(|e| ExchangeError::Network(format!("eth_getTransactionReceipt failed: {}", e)))?;
match receipt {
None => Ok(TxStatus::Pending),
Some(r) => {
let block = r.block_number.unwrap_or(0);
if r.status() {
Ok(TxStatus::Confirmed { block })
} else {
Ok(TxStatus::Failed {
reason: "transaction reverted".to_string(),
})
}
}
}
}
}
#[async_trait]
impl EvmChain for EvmProvider {
async fn eth_call(&self, call: TransactionRequest) -> Result<Bytes, ExchangeError> {
self.provider
.call(call)
.await
.map_err(|e| ExchangeError::Network(format!("eth_call failed: {}", e)))
}
async fn estimate_gas(&self, call: TransactionRequest) -> Result<u64, ExchangeError> {
self.provider
.estimate_gas(call)
.await
.map_err(|e| ExchangeError::Network(format!("eth_estimateGas failed: {}", e)))
}
async fn gas_price(&self) -> Result<U256, ExchangeError> {
let price_u128 = self
.provider
.get_gas_price()
.await
.map_err(|e| ExchangeError::Network(format!("eth_gasPrice failed: {}", e)))?;
Ok(U256::from(price_u128))
}
async fn max_priority_fee(&self) -> Result<U256, ExchangeError> {
let fee_u128 = self
.provider
.get_max_priority_fee_per_gas()
.await
.map_err(|e| {
ExchangeError::Network(format!("eth_maxPriorityFeePerGas failed: {}", e))
})?;
Ok(U256::from(fee_u128))
}
async fn get_receipt(
&self,
tx_hash: &str,
) -> Result<Option<TransactionReceipt>, ExchangeError> {
let hash: alloy::primitives::TxHash = tx_hash.parse().map_err(|e| {
ExchangeError::InvalidRequest(format!("Invalid tx hash '{}': {}", tx_hash, e))
})?;
self.provider
.get_transaction_receipt(hash)
.await
.map_err(|e| {
ExchangeError::Network(format!("eth_getTransactionReceipt failed: {}", e))
})
}
async fn erc20_balance(
&self,
token: Address,
account: Address,
) -> Result<U256, ExchangeError> {
let mut calldata = Vec::with_capacity(4 + 32);
calldata.extend_from_slice(&BALANCE_OF_SELECTOR);
calldata.extend_from_slice(&[0u8; 12]);
calldata.extend_from_slice(account.as_slice());
let tx = TransactionRequest::default()
.to(token)
.input(Bytes::from(calldata).into());
let result = self
.provider
.call(tx)
.await
.map_err(|e| ExchangeError::Network(format!("eth_call (balanceOf) failed: {}", e)))?;
if result.len() < 32 {
return Err(ExchangeError::Parse(format!(
"balanceOf returned {} bytes, expected at least 32",
result.len()
)));
}
Ok(U256::from_be_slice(&result[..32]))
}
fn inner(&self) -> &DynProvider<Ethereum> {
&self.provider
}
}