apex_sdk_evm/
lib.rs

1//! # Apex SDK EVM Adapter
2//!
3//! EVM blockchain adapter for the Apex SDK, providing unified access to Ethereum
4//! and EVM-compatible chains.
5//!
6//! ## Supported Networks
7//!
8//! - Ethereum Mainnet
9//! - Binance Smart Chain (BSC)
10//! - Polygon (Matic)
11//! - Avalanche C-Chain
12//! - And other EVM-compatible chains
13//!
14//! ## Features
15//!
16//! - **HTTP and WebSocket Support**: Flexible connection types
17//! - **Transaction Management**: Send, track, and query transactions
18//! - **Smart Contract Interaction**: Call and deploy contracts
19//! - **Wallet Integration**: Built-in wallet and signing support
20//! - **Connection Pooling**: Efficient resource management
21//! - **Metrics Collection**: Performance monitoring
22//!
23//! ## Quick Start
24//!
25//! ```rust,no_run
26//! use apex_sdk_evm::EvmAdapter;
27//!
28//! #[tokio::main]
29//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
30//!     // Connect to Ethereum mainnet
31//!     let adapter = EvmAdapter::connect("https://eth.llamarpc.com").await?;
32//!
33//!     // Get balance
34//!     let balance = adapter.get_balance("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7").await?;
35//!     println!("Balance: {} wei", balance);
36//!
37//!     Ok(())
38//! }
39//! ```
40
41pub mod cache;
42pub mod metrics;
43pub mod pool;
44pub mod transaction;
45pub mod wallet;
46
47use apex_sdk_types::{Address, TransactionStatus};
48use async_trait::async_trait;
49use thiserror::Error;
50
51use ethers::providers::{Http, Middleware, Provider, Ws};
52use ethers::types::{Address as EthAddress, TransactionReceipt, H256, U256};
53use std::sync::Arc;
54
55/// EVM adapter error
56#[derive(Error, Debug)]
57pub enum Error {
58    #[error("Connection error: {0}")]
59    Connection(String),
60
61    #[error("Transaction error: {0}")]
62    Transaction(String),
63
64    #[error("Contract error: {0}")]
65    Contract(String),
66
67    #[error("Invalid address: {0}")]
68    InvalidAddress(String),
69
70    #[error("Other error: {0}")]
71    Other(String),
72}
73
74/// Provider type that supports both HTTP and WebSocket connections
75#[derive(Clone)]
76pub enum ProviderType {
77    Http(Arc<Provider<Http>>),
78    Ws(Arc<Provider<Ws>>),
79}
80
81impl ProviderType {
82    /// Get the underlying provider as a middleware reference
83    async fn get_block_number(&self) -> Result<U256, Error> {
84        match self {
85            ProviderType::Http(p) => p
86                .get_block_number()
87                .await
88                .map_err(|e| Error::Connection(format!("Failed to get block number: {}", e)))
89                .map(|n| U256::from(n.as_u64())),
90            ProviderType::Ws(p) => p
91                .get_block_number()
92                .await
93                .map_err(|e| Error::Connection(format!("Failed to get block number: {}", e)))
94                .map(|n| U256::from(n.as_u64())),
95        }
96    }
97
98    async fn get_transaction_receipt(
99        &self,
100        hash: H256,
101    ) -> Result<Option<TransactionReceipt>, Error> {
102        match self {
103            ProviderType::Http(p) => p
104                .get_transaction_receipt(hash)
105                .await
106                .map_err(|e| Error::Transaction(format!("Failed to get receipt: {}", e))),
107            ProviderType::Ws(p) => p
108                .get_transaction_receipt(hash)
109                .await
110                .map_err(|e| Error::Transaction(format!("Failed to get receipt: {}", e))),
111        }
112    }
113
114    async fn get_transaction(
115        &self,
116        hash: H256,
117    ) -> Result<Option<ethers::types::Transaction>, Error> {
118        match self {
119            ProviderType::Http(p) => p
120                .get_transaction(hash)
121                .await
122                .map_err(|e| Error::Transaction(format!("Failed to get transaction: {}", e))),
123            ProviderType::Ws(p) => p
124                .get_transaction(hash)
125                .await
126                .map_err(|e| Error::Transaction(format!("Failed to get transaction: {}", e))),
127        }
128    }
129
130    async fn get_balance(
131        &self,
132        address: EthAddress,
133        block: Option<ethers::types::BlockId>,
134    ) -> Result<U256, Error> {
135        match self {
136            ProviderType::Http(p) => p
137                .get_balance(address, block)
138                .await
139                .map_err(|e| Error::Connection(format!("Failed to get balance: {}", e))),
140            ProviderType::Ws(p) => p
141                .get_balance(address, block)
142                .await
143                .map_err(|e| Error::Connection(format!("Failed to get balance: {}", e))),
144        }
145    }
146
147    async fn get_chain_id(&self) -> Result<U256, Error> {
148        match self {
149            ProviderType::Http(p) => p
150                .get_chainid()
151                .await
152                .map_err(|e| Error::Connection(format!("Failed to get chain ID: {}", e))),
153            ProviderType::Ws(p) => p
154                .get_chainid()
155                .await
156                .map_err(|e| Error::Connection(format!("Failed to get chain ID: {}", e))),
157        }
158    }
159}
160
161/// EVM blockchain adapter
162pub struct EvmAdapter {
163    endpoint: String,
164    provider: ProviderType,
165    connected: bool,
166}
167
168impl EvmAdapter {
169    /// Get the endpoint URL this adapter is connected to
170    pub fn endpoint(&self) -> &str {
171        &self.endpoint
172    }
173}
174
175impl EvmAdapter {
176    /// Get a reference to the provider for transaction execution
177    pub fn provider(&self) -> &ProviderType {
178        &self.provider
179    }
180
181    /// Create a transaction executor with this adapter's provider
182    pub fn transaction_executor(&self) -> transaction::TransactionExecutor {
183        transaction::TransactionExecutor::new(self.provider.clone())
184    }
185}
186
187impl EvmAdapter {
188    /// Connect to an EVM node
189    pub async fn connect(endpoint: &str) -> Result<Self, Error> {
190        tracing::info!("Connecting to EVM endpoint: {}", endpoint);
191
192        // Determine connection type based on URL scheme
193        let provider = if endpoint.starts_with("ws://") || endpoint.starts_with("wss://") {
194            // WebSocket connection for real-time updates
195            tracing::debug!("Using WebSocket connection");
196            let ws = Ws::connect(endpoint)
197                .await
198                .map_err(|e| Error::Connection(format!("WebSocket connection failed: {}", e)))?;
199            ProviderType::Ws(Arc::new(Provider::new(ws)))
200        } else {
201            // HTTP connection for basic queries
202            tracing::debug!("Using HTTP connection");
203            let parsed_url = url::Url::parse(endpoint)
204                .map_err(|e| Error::Connection(format!("Invalid URL: {}", e)))?;
205            let http = Http::new(parsed_url);
206            ProviderType::Http(Arc::new(Provider::new(http)))
207        };
208
209        // Verify connection by getting chain ID
210        let chain_id = provider.get_chain_id().await?;
211        tracing::info!("Connected to chain ID: {}", chain_id);
212
213        Ok(Self {
214            endpoint: endpoint.to_string(),
215            provider,
216            connected: true,
217        })
218    }
219
220    /// Get transaction status
221    pub async fn get_transaction_status(&self, tx_hash: &str) -> Result<TransactionStatus, Error> {
222        if !self.connected {
223            return Err(Error::Connection("Not connected".to_string()));
224        }
225
226        tracing::debug!("Getting transaction status for: {}", tx_hash);
227
228        // Validate tx hash format (0x + 64 hex chars)
229        if !tx_hash.starts_with("0x") || tx_hash.len() != 66 {
230            return Err(Error::Transaction("Invalid transaction hash".to_string()));
231        }
232
233        // Parse transaction hash
234        let hash: H256 = tx_hash
235            .parse()
236            .map_err(|e| Error::Transaction(format!("Invalid hash format: {}", e)))?;
237
238        // Query transaction receipt
239        match self.provider.get_transaction_receipt(hash).await? {
240            Some(receipt) => {
241                // Get current block number for confirmations
242                let current_block = self.provider.get_block_number().await?;
243
244                let confirmations = if let Some(block_number) = receipt.block_number {
245                    current_block.as_u64().saturating_sub(block_number.as_u64()) as u32
246                } else {
247                    0
248                };
249
250                // Check if transaction succeeded (status == 1)
251                if receipt.status == Some(1.into()) {
252                    Ok(TransactionStatus::Confirmed {
253                        block_number: receipt.block_number.unwrap_or_default().as_u64(),
254                        confirmations,
255                    })
256                } else {
257                    Ok(TransactionStatus::Failed {
258                        error: "Transaction reverted".to_string(),
259                    })
260                }
261            }
262            None => {
263                // Transaction not found in a block - check if it's in mempool
264                match self.provider.get_transaction(hash).await? {
265                    Some(_) => Ok(TransactionStatus::Pending),
266                    None => Ok(TransactionStatus::Unknown),
267                }
268            }
269        }
270    }
271
272    /// Get balance of an address in wei
273    pub async fn get_balance(&self, address: &str) -> Result<U256, Error> {
274        if !self.connected {
275            return Err(Error::Connection("Not connected".to_string()));
276        }
277
278        tracing::debug!("Getting balance for address: {}", address);
279
280        // Parse address
281        let addr: EthAddress = address
282            .parse()
283            .map_err(|e| Error::InvalidAddress(format!("Invalid address format: {}", e)))?;
284
285        // Query balance at latest block
286        self.provider.get_balance(addr, None).await
287    }
288
289    /// Get balance of an address in a human-readable format (ETH)
290    pub async fn get_balance_eth(&self, address: &str) -> Result<String, Error> {
291        let balance_wei = self.get_balance(address).await?;
292
293        // Convert wei to ETH (1 ETH = 10^18 wei)
294        let eth_divisor = U256::from(10_u64.pow(18));
295        let eth_value = balance_wei / eth_divisor;
296        let remainder = balance_wei % eth_divisor;
297
298        // Format with up to 18 decimal places
299        Ok(format!("{}.{:018}", eth_value, remainder))
300    }
301
302    /// Validate an EVM address (0x + 40 hex chars)
303    pub fn validate_address(&self, address: &Address) -> bool {
304        match address {
305            Address::Evm(addr) => {
306                addr.starts_with("0x")
307                    && addr.len() == 42
308                    && addr[2..].chars().all(|c| c.is_ascii_hexdigit())
309            }
310            _ => false,
311        }
312    }
313
314    /// Get contract instance
315    pub fn contract(&self, address: &str) -> Result<ContractInfo<'_>, Error> {
316        if !self.connected {
317            return Err(Error::Connection("Not connected".to_string()));
318        }
319
320        if !self.validate_address(&Address::evm(address)) {
321            return Err(Error::InvalidAddress(address.to_string()));
322        }
323
324        Ok(ContractInfo {
325            address: address.to_string(),
326            adapter: self,
327        })
328    }
329}
330
331/// Contract information and interaction
332pub struct ContractInfo<'a> {
333    address: String,
334    #[allow(dead_code)]
335    adapter: &'a EvmAdapter,
336}
337
338impl ContractInfo<'_> {
339    /// Get the contract address
340    pub fn address(&self) -> &str {
341        &self.address
342    }
343}
344
345#[async_trait]
346impl apex_sdk_core::ChainAdapter for EvmAdapter {
347    async fn get_transaction_status(&self, tx_hash: &str) -> Result<TransactionStatus, String> {
348        self.get_transaction_status(tx_hash)
349            .await
350            .map_err(|e| e.to_string())
351    }
352
353    fn validate_address(&self, address: &Address) -> bool {
354        self.validate_address(address)
355    }
356
357    fn chain_name(&self) -> &str {
358        "EVM"
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[tokio::test]
367    #[ignore] // Requires network connection
368    async fn test_evm_adapter_connect() {
369        let adapter = EvmAdapter::connect("https://eth.llamarpc.com").await;
370        assert!(adapter.is_ok());
371    }
372
373    #[tokio::test]
374    #[ignore] // Requires network connection
375    async fn test_address_validation() {
376        let adapter = EvmAdapter::connect("https://eth.llamarpc.com")
377            .await
378            .unwrap();
379
380        let valid_addr = Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7");
381        assert!(adapter.validate_address(&valid_addr));
382
383        let invalid_addr = Address::evm("invalid");
384        assert!(!adapter.validate_address(&invalid_addr));
385
386        let invalid_addr2 = Address::evm("0x123");
387        assert!(!adapter.validate_address(&invalid_addr2));
388    }
389
390    #[tokio::test]
391    #[ignore] // Requires network connection
392    async fn test_transaction_status() {
393        let adapter = EvmAdapter::connect("https://eth.llamarpc.com")
394            .await
395            .unwrap();
396
397        // Test with a known transaction hash (first ETH transaction ever)
398        let result = adapter
399            .get_transaction_status(
400                "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060",
401            )
402            .await;
403        assert!(result.is_ok());
404
405        let invalid_result = adapter.get_transaction_status("invalid").await;
406        assert!(invalid_result.is_err());
407    }
408
409    #[test]
410    fn test_invalid_url_format() {
411        // Test that invalid URLs are rejected during parsing
412        // This doesn't require async or network
413        let url = url::Url::parse("not-a-valid-url");
414        assert!(url.is_err(), "Expected invalid URL to fail parsing");
415    }
416}