1pub 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#[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#[derive(Clone)]
76pub enum ProviderType {
77 Http(Arc<Provider<Http>>),
78 Ws(Arc<Provider<Ws>>),
79}
80
81impl ProviderType {
82 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
161pub struct EvmAdapter {
163 endpoint: String,
164 provider: ProviderType,
165 connected: bool,
166}
167
168impl EvmAdapter {
169 pub fn endpoint(&self) -> &str {
171 &self.endpoint
172 }
173}
174
175impl EvmAdapter {
176 pub fn provider(&self) -> &ProviderType {
178 &self.provider
179 }
180
181 pub fn transaction_executor(&self) -> transaction::TransactionExecutor {
183 transaction::TransactionExecutor::new(self.provider.clone())
184 }
185}
186
187impl EvmAdapter {
188 pub async fn connect(endpoint: &str) -> Result<Self, Error> {
190 tracing::info!("Connecting to EVM endpoint: {}", endpoint);
191
192 let provider = if endpoint.starts_with("ws://") || endpoint.starts_with("wss://") {
194 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 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 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 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 if !tx_hash.starts_with("0x") || tx_hash.len() != 66 {
230 return Err(Error::Transaction("Invalid transaction hash".to_string()));
231 }
232
233 let hash: H256 = tx_hash
235 .parse()
236 .map_err(|e| Error::Transaction(format!("Invalid hash format: {}", e)))?;
237
238 match self.provider.get_transaction_receipt(hash).await? {
240 Some(receipt) => {
241 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 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 match self.provider.get_transaction(hash).await? {
265 Some(_) => Ok(TransactionStatus::Pending),
266 None => Ok(TransactionStatus::Unknown),
267 }
268 }
269 }
270 }
271
272 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 let addr: EthAddress = address
282 .parse()
283 .map_err(|e| Error::InvalidAddress(format!("Invalid address format: {}", e)))?;
284
285 self.provider.get_balance(addr, None).await
287 }
288
289 pub async fn get_balance_eth(&self, address: &str) -> Result<String, Error> {
291 let balance_wei = self.get_balance(address).await?;
292
293 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 Ok(format!("{}.{:018}", eth_value, remainder))
300 }
301
302 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 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
331pub struct ContractInfo<'a> {
333 address: String,
334 #[allow(dead_code)]
335 adapter: &'a EvmAdapter,
336}
337
338impl ContractInfo<'_> {
339 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] 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] 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] async fn test_transaction_status() {
393 let adapter = EvmAdapter::connect("https://eth.llamarpc.com")
394 .await
395 .unwrap();
396
397 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 let url = url::Url::parse("not-a-valid-url");
414 assert!(url.is_err(), "Expected invalid URL to fail parsing");
415 }
416}