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_hash: receipt
254                            .block_hash
255                            .map(|h| format!("{:?}", h))
256                            .unwrap_or_default(),
257                        block_number: receipt.block_number.map(|n| n.as_u64()),
258                    })
259                } else {
260                    Ok(TransactionStatus::Failed {
261                        error: "Transaction reverted".to_string(),
262                    })
263                }
264            }
265            None => {
266                // Transaction not found in a block - check if it's in mempool
267                match self.provider.get_transaction(hash).await? {
268                    Some(_) => Ok(TransactionStatus::Pending),
269                    None => Ok(TransactionStatus::Unknown),
270                }
271            }
272        }
273    }
274
275    /// Get balance of an address in wei
276    pub async fn get_balance(&self, address: &str) -> Result<U256, Error> {
277        if !self.connected {
278            return Err(Error::Connection("Not connected".to_string()));
279        }
280
281        tracing::debug!("Getting balance for address: {}", address);
282
283        // Parse address
284        let addr: EthAddress = address
285            .parse()
286            .map_err(|e| Error::InvalidAddress(format!("Invalid address format: {}", e)))?;
287
288        // Query balance at latest block
289        self.provider.get_balance(addr, None).await
290    }
291
292    /// Get balance of an address in a human-readable format (ETH)
293    pub async fn get_balance_eth(&self, address: &str) -> Result<String, Error> {
294        let balance_wei = self.get_balance(address).await?;
295
296        // Convert wei to ETH (1 ETH = 10^18 wei)
297        let eth_divisor = U256::from(10_u64.pow(18));
298        let eth_value = balance_wei / eth_divisor;
299        let remainder = balance_wei % eth_divisor;
300
301        // Format with up to 18 decimal places
302        Ok(format!("{}.{:018}", eth_value, remainder))
303    }
304
305    /// Validate an EVM address (0x + 40 hex chars)
306    pub fn validate_address(&self, address: &Address) -> bool {
307        match address {
308            Address::Evm(addr) => {
309                addr.starts_with("0x")
310                    && addr.len() == 42
311                    && addr[2..].chars().all(|c| c.is_ascii_hexdigit())
312            }
313            _ => false,
314        }
315    }
316
317    /// Get contract instance
318    pub fn contract(&self, address: &str) -> Result<ContractInfo<'_>, Error> {
319        if !self.connected {
320            return Err(Error::Connection("Not connected".to_string()));
321        }
322
323        if !self.validate_address(&Address::evm(address)) {
324            return Err(Error::InvalidAddress(address.to_string()));
325        }
326
327        Ok(ContractInfo {
328            address: address.to_string(),
329            adapter: self,
330        })
331    }
332}
333
334/// Contract information and interaction
335pub struct ContractInfo<'a> {
336    address: String,
337    #[allow(dead_code)]
338    adapter: &'a EvmAdapter,
339}
340
341impl ContractInfo<'_> {
342    /// Get the contract address
343    pub fn address(&self) -> &str {
344        &self.address
345    }
346}
347
348#[async_trait]
349impl apex_sdk_core::ChainAdapter for EvmAdapter {
350    async fn get_transaction_status(&self, tx_hash: &str) -> Result<TransactionStatus, String> {
351        self.get_transaction_status(tx_hash)
352            .await
353            .map_err(|e| e.to_string())
354    }
355
356    fn validate_address(&self, address: &Address) -> bool {
357        self.validate_address(address)
358    }
359
360    fn chain_name(&self) -> &str {
361        "EVM"
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    #[tokio::test]
370    #[ignore] // Requires network connection
371    async fn test_evm_adapter_connect() {
372        let adapter = EvmAdapter::connect("https://eth.llamarpc.com").await;
373        assert!(adapter.is_ok());
374    }
375
376    #[tokio::test]
377    #[ignore] // Requires network connection
378    async fn test_address_validation() {
379        let adapter = EvmAdapter::connect("https://eth.llamarpc.com")
380            .await
381            .unwrap();
382
383        let valid_addr = Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7");
384        assert!(adapter.validate_address(&valid_addr));
385
386        let invalid_addr = Address::evm("invalid");
387        assert!(!adapter.validate_address(&invalid_addr));
388
389        let invalid_addr2 = Address::evm("0x123");
390        assert!(!adapter.validate_address(&invalid_addr2));
391    }
392
393    #[tokio::test]
394    #[ignore] // Requires network connection
395    async fn test_transaction_status() {
396        let adapter = EvmAdapter::connect("https://eth.llamarpc.com")
397            .await
398            .unwrap();
399
400        // Test with a known transaction hash (first ETH transaction ever)
401        let result = adapter
402            .get_transaction_status(
403                "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060",
404            )
405            .await;
406        assert!(result.is_ok());
407
408        let invalid_result = adapter.get_transaction_status("invalid").await;
409        assert!(invalid_result.is_err());
410    }
411
412    #[test]
413    fn test_invalid_url_format() {
414        // Test that invalid URLs are rejected during parsing
415        // This doesn't require async or network
416        let url = url::Url::parse("not-a-valid-url");
417        assert!(url.is_err(), "Expected invalid URL to fail parsing");
418    }
419}