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 alloy::primitives::{Address as EthAddress, B256, U256};
53use alloy::providers::{Provider, ProviderBuilder};
54use alloy::rpc::types::TransactionReceipt;
55
56#[derive(Error, Debug)]
58pub enum Error {
59 #[error("Connection error: {0}")]
60 Connection(String),
61
62 #[error("Transaction error: {0}")]
63 Transaction(String),
64
65 #[error("Contract error: {0}")]
66 Contract(String),
67
68 #[error("Invalid address: {0}")]
69 InvalidAddress(String),
70
71 #[error("Other error: {0}")]
72 Other(String),
73}
74
75type AlloyHttpProvider = alloy::providers::fillers::FillProvider<
77 alloy::providers::fillers::JoinFill<
78 alloy::providers::Identity,
79 alloy::providers::fillers::JoinFill<
80 alloy::providers::fillers::GasFiller,
81 alloy::providers::fillers::JoinFill<
82 alloy::providers::fillers::BlobGasFiller,
83 alloy::providers::fillers::JoinFill<
84 alloy::providers::fillers::NonceFiller,
85 alloy::providers::fillers::ChainIdFiller,
86 >,
87 >,
88 >,
89 >,
90 alloy::providers::RootProvider<alloy::network::Ethereum>,
91 alloy::network::Ethereum,
92>;
93
94#[derive(Clone)]
97pub struct ProviderType {
98 inner: AlloyHttpProvider,
99}
100
101impl ProviderType {
102 async fn get_block_number(&self) -> Result<u64, Error> {
104 self.inner
105 .get_block_number()
106 .await
107 .map_err(|e| Error::Connection(format!("Failed to get block number: {}", e)))
108 }
109
110 async fn get_transaction_receipt(
111 &self,
112 hash: B256,
113 ) -> Result<Option<TransactionReceipt>, Error> {
114 self.inner
115 .get_transaction_receipt(hash)
116 .await
117 .map_err(|e| Error::Transaction(format!("Failed to get receipt: {}", e)))
118 }
119
120 async fn get_transaction(
121 &self,
122 hash: B256,
123 ) -> Result<Option<alloy::rpc::types::Transaction>, Error> {
124 self.inner
125 .get_transaction_by_hash(hash)
126 .await
127 .map_err(|e| Error::Transaction(format!("Failed to get transaction: {}", e)))
128 }
129
130 async fn get_balance(&self, address: EthAddress) -> Result<U256, Error> {
131 self.inner
132 .get_balance(address)
133 .await
134 .map_err(|e| Error::Connection(format!("Failed to get balance: {}", e)))
135 }
136
137 pub async fn get_chain_id(&self) -> Result<u64, Error> {
138 self.inner
139 .get_chain_id()
140 .await
141 .map_err(|e| Error::Connection(format!("Failed to get chain ID: {}", e)))
142 }
143}
144
145pub struct EvmAdapter {
147 endpoint: String,
148 provider: ProviderType,
149 connected: bool,
150}
151
152impl EvmAdapter {
153 pub fn endpoint(&self) -> &str {
155 &self.endpoint
156 }
157}
158
159impl EvmAdapter {
160 pub fn provider(&self) -> &ProviderType {
162 &self.provider
163 }
164
165 pub fn transaction_executor(&self) -> transaction::TransactionExecutor {
167 transaction::TransactionExecutor::new(self.provider.clone())
168 }
169}
170
171impl EvmAdapter {
172 pub async fn connect(endpoint: &str) -> Result<Self, Error> {
174 tracing::info!("Connecting to EVM endpoint: {}", endpoint);
175
176 tracing::debug!("Using HTTP connection");
178 let parsed_url = endpoint
179 .parse()
180 .map_err(|e| Error::Connection(format!("Invalid URL: {}", e)))?;
181 let inner = ProviderBuilder::new().connect_http(parsed_url);
182 let provider = ProviderType { inner };
183
184 let chain_id = provider.get_chain_id().await?;
186 tracing::info!("Connected to chain ID: {}", chain_id);
187
188 Ok(Self {
189 endpoint: endpoint.to_string(),
190 provider,
191 connected: true,
192 })
193 }
194
195 pub async fn get_transaction_status(&self, tx_hash: &str) -> Result<TransactionStatus, Error> {
197 if !self.connected {
198 return Err(Error::Connection("Not connected".to_string()));
199 }
200
201 tracing::debug!("Getting transaction status for: {}", tx_hash);
202
203 if !tx_hash.starts_with("0x") || tx_hash.len() != 66 {
205 return Err(Error::Transaction("Invalid transaction hash".to_string()));
206 }
207
208 let hash: B256 = tx_hash
210 .parse()
211 .map_err(|e| Error::Transaction(format!("Invalid hash format: {}", e)))?;
212
213 match self.provider.get_transaction_receipt(hash).await? {
215 Some(receipt) => {
216 let current_block = self.provider.get_block_number().await?;
218
219 let _confirmations = if let Some(block_number) = receipt.block_number {
220 current_block.saturating_sub(block_number) as u32
221 } else {
222 0
223 };
224
225 if receipt.status() {
227 Ok(TransactionStatus::Confirmed {
228 block_hash: receipt
229 .block_hash
230 .map(|h| format!("{:?}", h))
231 .unwrap_or_default(),
232 block_number: receipt.block_number,
233 })
234 } else {
235 Ok(TransactionStatus::Failed {
236 error: "Transaction reverted".to_string(),
237 })
238 }
239 }
240 None => {
241 match self.provider.get_transaction(hash).await? {
243 Some(_) => Ok(TransactionStatus::Pending),
244 None => Ok(TransactionStatus::Unknown),
245 }
246 }
247 }
248 }
249
250 pub async fn get_balance(&self, address: &str) -> Result<U256, Error> {
252 if !self.connected {
253 return Err(Error::Connection("Not connected".to_string()));
254 }
255
256 tracing::debug!("Getting balance for address: {}", address);
257
258 let addr: EthAddress = address
260 .parse()
261 .map_err(|e| Error::InvalidAddress(format!("Invalid address format: {}", e)))?;
262
263 self.provider.get_balance(addr).await
265 }
266
267 pub async fn get_balance_eth(&self, address: &str) -> Result<String, Error> {
269 let balance_wei = self.get_balance(address).await?;
270
271 let eth_divisor = U256::from(10_u64.pow(18));
273 let eth_value = balance_wei / eth_divisor;
274 let remainder = balance_wei % eth_divisor;
275
276 Ok(format!("{}.{:018}", eth_value, remainder))
278 }
279
280 pub fn validate_address(&self, address: &Address) -> bool {
282 match address {
283 Address::Evm(addr) => {
284 addr.starts_with("0x")
285 && addr.len() == 42
286 && addr[2..].chars().all(|c| c.is_ascii_hexdigit())
287 }
288 _ => false,
289 }
290 }
291
292 pub fn contract(&self, address: &str) -> Result<ContractInfo<'_>, Error> {
294 if !self.connected {
295 return Err(Error::Connection("Not connected".to_string()));
296 }
297
298 if !self.validate_address(&Address::evm(address)) {
299 return Err(Error::InvalidAddress(address.to_string()));
300 }
301
302 Ok(ContractInfo {
303 address: address.to_string(),
304 adapter: self,
305 })
306 }
307}
308
309pub struct ContractInfo<'a> {
311 address: String,
312 #[allow(dead_code)]
313 adapter: &'a EvmAdapter,
314}
315
316impl ContractInfo<'_> {
317 pub fn address(&self) -> &str {
319 &self.address
320 }
321}
322
323#[async_trait]
324impl apex_sdk_core::ChainAdapter for EvmAdapter {
325 async fn get_transaction_status(&self, tx_hash: &str) -> Result<TransactionStatus, String> {
326 self.get_transaction_status(tx_hash)
327 .await
328 .map_err(|e| e.to_string())
329 }
330
331 fn validate_address(&self, address: &Address) -> bool {
332 self.validate_address(address)
333 }
334
335 fn chain_name(&self) -> &str {
336 "EVM"
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 #[tokio::test]
345 #[ignore] async fn test_evm_adapter_connect() {
347 let adapter = EvmAdapter::connect("https://eth.llamarpc.com").await;
348 assert!(adapter.is_ok());
349 }
350
351 #[tokio::test]
352 #[ignore] async fn test_address_validation() {
354 let adapter = EvmAdapter::connect("https://eth.llamarpc.com")
355 .await
356 .unwrap();
357
358 let valid_addr = Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7");
359 assert!(adapter.validate_address(&valid_addr));
360
361 let invalid_addr = Address::evm("invalid");
362 assert!(!adapter.validate_address(&invalid_addr));
363
364 let invalid_addr2 = Address::evm("0x123");
365 assert!(!adapter.validate_address(&invalid_addr2));
366 }
367
368 #[tokio::test]
369 #[ignore] async fn test_transaction_status() {
371 let adapter = EvmAdapter::connect("https://eth.llamarpc.com")
372 .await
373 .unwrap();
374
375 let result = adapter
377 .get_transaction_status(
378 "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060",
379 )
380 .await;
381 assert!(result.is_ok());
382
383 let invalid_result = adapter.get_transaction_status("invalid").await;
384 assert!(invalid_result.is_err());
385 }
386
387 #[test]
388 fn test_invalid_url_format() {
389 let url = url::Url::parse("not-a-valid-url");
392 assert!(url.is_err(), "Expected invalid URL to fail parsing");
393 }
394}