1#[cfg(feature = "evm")]
27pub mod evm;
28
29#[cfg(feature = "solana")]
30pub mod solana;
31
32use crate::{Error, Result, Signature};
33use async_trait::async_trait;
34use serde::{Deserialize, Serialize};
35use std::fmt;
36
37#[cfg(feature = "evm")]
38pub use evm::{EvmAdapter, EvmConfig};
39
40#[cfg(feature = "aa")]
41pub use evm::aa::{SmartAccountConfig, SmartAccountModule, UserOperation};
42
43#[cfg(feature = "solana")]
44pub use solana::{SolanaAdapter, SolanaConfig};
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
52pub struct ChainId(pub u64);
53
54impl ChainId {
55 pub const ETHEREUM_MAINNET: ChainId = ChainId(1);
57 pub const ETHEREUM_SEPOLIA: ChainId = ChainId(11155111);
58 pub const ARBITRUM_ONE: ChainId = ChainId(42161);
59 pub const OPTIMISM: ChainId = ChainId(10);
60 pub const BASE: ChainId = ChainId(8453);
61 pub const POLYGON: ChainId = ChainId(137);
62 pub const BSC: ChainId = ChainId(56);
63 pub const AVALANCHE: ChainId = ChainId(43114);
64
65 pub const SOLANA_MAINNET: ChainId = ChainId(101);
67 pub const SOLANA_DEVNET: ChainId = ChainId(102);
68 pub const SOLANA_TESTNET: ChainId = ChainId(103);
69
70 pub fn name(&self) -> &'static str {
72 match self.0 {
73 1 => "Ethereum Mainnet",
74 11155111 => "Ethereum Sepolia",
75 42161 => "Arbitrum One",
76 10 => "Optimism",
77 8453 => "Base",
78 137 => "Polygon",
79 56 => "BNB Smart Chain",
80 43114 => "Avalanche C-Chain",
81 101 => "Solana Mainnet",
82 102 => "Solana Devnet",
83 103 => "Solana Testnet",
84 _ => "Unknown Chain",
85 }
86 }
87
88 pub fn is_solana(&self) -> bool {
90 matches!(self.0, 101 | 102 | 103)
91 }
92
93 pub fn is_evm(&self) -> bool {
95 !self.is_solana()
96 }
97}
98
99impl fmt::Display for ChainId {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 write!(f, "{} ({})", self.name(), self.0)
102 }
103}
104
105impl From<u64> for ChainId {
106 fn from(id: u64) -> Self {
107 ChainId(id)
108 }
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct Balance {
114 pub raw: String,
116 pub formatted: String,
118 pub symbol: String,
120 pub decimals: u8,
122}
123
124impl Balance {
125 pub fn new(raw: impl Into<String>, decimals: u8, symbol: impl Into<String>) -> Self {
127 let raw_str = raw.into();
128 let symbol_str = symbol.into();
129 let formatted = Self::format_balance(&raw_str, decimals);
130
131 Self {
132 raw: raw_str,
133 formatted,
134 symbol: symbol_str,
135 decimals,
136 }
137 }
138
139 fn format_balance(raw: &str, decimals: u8) -> String {
141 let raw_value: u128 = raw.parse().unwrap_or(0);
142 if raw_value == 0 {
143 return "0".to_string();
144 }
145
146 let divisor = 10u128.pow(decimals as u32);
147 let whole = raw_value / divisor;
148 let fraction = raw_value % divisor;
149
150 if fraction == 0 {
151 whole.to_string()
152 } else {
153 let fraction_str = format!("{:0>width$}", fraction, width = decimals as usize);
154 let trimmed = fraction_str.trim_end_matches('0');
155 format!("{}.{}", whole, trimmed)
156 }
157 }
158
159 pub fn is_zero(&self) -> bool {
161 self.raw == "0" || self.raw.is_empty()
162 }
163
164 pub fn raw_value(&self) -> u128 {
166 self.raw.parse().unwrap_or(0)
167 }
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct TxParams {
173 pub from: String,
175 pub to: String,
177 pub value: String,
179 #[serde(default)]
181 pub data: Option<Vec<u8>>,
182 #[serde(default)]
184 pub gas_limit: Option<u64>,
185 #[serde(default)]
187 pub nonce: Option<u64>,
188 #[serde(default)]
190 pub priority: TxPriority,
191}
192
193impl TxParams {
194 pub fn new(from: impl Into<String>, to: impl Into<String>, value: impl Into<String>) -> Self {
196 Self {
197 from: from.into(),
198 to: to.into(),
199 value: value.into(),
200 data: None,
201 gas_limit: None,
202 nonce: None,
203 priority: TxPriority::Medium,
204 }
205 }
206
207 pub fn with_data(mut self, data: Vec<u8>) -> Self {
209 self.data = Some(data);
210 self
211 }
212
213 pub fn with_gas_limit(mut self, limit: u64) -> Self {
215 self.gas_limit = Some(limit);
216 self
217 }
218
219 pub fn with_nonce(mut self, nonce: u64) -> Self {
221 self.nonce = Some(nonce);
222 self
223 }
224
225 pub fn with_priority(mut self, priority: TxPriority) -> Self {
227 self.priority = priority;
228 self
229 }
230}
231
232#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
234#[serde(rename_all = "lowercase")]
235pub enum TxPriority {
236 Low,
238 #[default]
240 Medium,
241 High,
243 Urgent,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct UnsignedTx {
250 pub chain_id: ChainId,
252 pub signing_payload: Vec<u8>,
254 pub raw_tx: Vec<u8>,
256 pub summary: TxSummary,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct TxSummary {
263 pub tx_type: String,
265 pub from: String,
267 pub to: String,
269 pub value: String,
271 pub estimated_fee: String,
273 #[serde(default)]
275 pub details: Option<String>,
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct SignedTx {
281 pub chain_id: ChainId,
283 pub raw_tx: Vec<u8>,
285 pub tx_hash: String,
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct TxHash {
292 pub hash: String,
294 pub explorer_url: Option<String>,
296}
297
298impl TxHash {
299 pub fn new(hash: impl Into<String>) -> Self {
301 Self {
302 hash: hash.into(),
303 explorer_url: None,
304 }
305 }
306
307 pub fn with_explorer_url(mut self, url: impl Into<String>) -> Self {
309 self.explorer_url = Some(url.into());
310 self
311 }
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct GasPrices {
317 pub low: GasPrice,
319 pub medium: GasPrice,
321 pub high: GasPrice,
323 pub base_fee: Option<u128>,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct GasPrice {
330 pub max_fee: u128,
332 pub max_priority_fee: u128,
334 pub estimated_wait_secs: Option<u64>,
336}
337
338#[async_trait]
347pub trait ChainAdapter: Send + Sync {
348 fn chain_id(&self) -> ChainId;
350
351 fn native_symbol(&self) -> &str;
353
354 fn native_decimals(&self) -> u8;
356
357 async fn get_balance(&self, address: &str) -> Result<Balance>;
359
360 async fn get_nonce(&self, address: &str) -> Result<u64>;
362
363 async fn build_transaction(&self, params: TxParams) -> Result<UnsignedTx>;
365
366 async fn broadcast(&self, signed_tx: &SignedTx) -> Result<TxHash>;
368
369 fn derive_address(&self, public_key: &[u8]) -> Result<String>;
371
372 async fn get_gas_prices(&self) -> Result<GasPrices>;
374
375 async fn estimate_gas(&self, params: &TxParams) -> Result<u64>;
377
378 async fn wait_for_confirmation(&self, tx_hash: &str, timeout_secs: u64) -> Result<TxReceipt>;
380
381 fn is_valid_address(&self, address: &str) -> bool;
383
384 fn explorer_tx_url(&self, tx_hash: &str) -> Option<String>;
386
387 fn explorer_address_url(&self, address: &str) -> Option<String>;
389
390 fn finalize_transaction(
392 &self,
393 unsigned_tx: &UnsignedTx,
394 signature: &Signature,
395 ) -> Result<SignedTx>;
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize)]
400pub struct TxReceipt {
401 pub tx_hash: String,
403 pub block_number: u64,
405 pub status: TxStatus,
407 pub gas_used: Option<u64>,
409 pub effective_gas_price: Option<u128>,
411}
412
413#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
415pub enum TxStatus {
416 Success,
418 Failed,
420 Pending,
422}
423
424#[cfg(feature = "runtime")]
430#[derive(Clone)]
431pub struct RpcClient {
432 urls: Vec<String>,
433 client: reqwest::Client,
434 current_index: std::sync::Arc<std::sync::atomic::AtomicUsize>,
435}
436
437#[cfg(feature = "runtime")]
438impl RpcClient {
439 pub fn new(urls: Vec<String>) -> Result<Self> {
441 if urls.is_empty() {
442 return Err(Error::InvalidConfig("At least one RPC URL required".into()));
443 }
444
445 let client = reqwest::Client::builder()
446 .timeout(std::time::Duration::from_secs(30))
447 .build()
448 .map_err(|e| Error::ChainError(format!("Failed to create HTTP client: {}", e)))?;
449
450 Ok(Self {
451 urls,
452 client,
453 current_index: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
454 })
455 }
456
457 fn current_url(&self) -> &str {
459 let idx = self
460 .current_index
461 .load(std::sync::atomic::Ordering::Relaxed);
462 &self.urls[idx % self.urls.len()]
463 }
464
465 fn rotate_url(&self) {
467 self.current_index
468 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
469 }
470
471 pub async fn request<T: serde::de::DeserializeOwned>(
473 &self,
474 method: &str,
475 params: serde_json::Value,
476 ) -> Result<T> {
477 let mut last_error = None;
478
479 for _ in 0..self.urls.len() {
480 let url = self.current_url();
481
482 match self.make_request(url, method, params.clone()).await {
483 Ok(result) => return Ok(result),
484 Err(e) => {
485 tracing::warn!("RPC request failed on {}: {}", url, e);
486 last_error = Some(e);
487 self.rotate_url();
488 }
489 }
490 }
491
492 Err(last_error.unwrap_or_else(|| Error::ChainError("All RPC endpoints failed".into())))
493 }
494
495 async fn make_request<T: serde::de::DeserializeOwned>(
496 &self,
497 url: &str,
498 method: &str,
499 params: serde_json::Value,
500 ) -> Result<T> {
501 let request_body = serde_json::json!({
502 "jsonrpc": "2.0",
503 "method": method,
504 "params": params,
505 "id": 1
506 });
507
508 let response = self
509 .client
510 .post(url)
511 .json(&request_body)
512 .send()
513 .await
514 .map_err(|e| Error::ChainError(format!("RPC request failed: {}", e)))?;
515
516 let response_body: serde_json::Value = response
517 .json()
518 .await
519 .map_err(|e| Error::ChainError(format!("Failed to parse RPC response: {}", e)))?;
520
521 if let Some(error) = response_body.get("error") {
522 return Err(Error::ChainError(format!("RPC error: {}", error)));
523 }
524
525 let result = response_body
526 .get("result")
527 .ok_or_else(|| Error::ChainError("Missing result in RPC response".into()))?;
528
529 serde_json::from_value(result.clone())
530 .map_err(|e| Error::ChainError(format!("Failed to deserialize result: {}", e)))
531 }
532}
533
534#[cfg(feature = "runtime")]
535impl std::fmt::Debug for RpcClient {
536 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
537 f.debug_struct("RpcClient")
538 .field("urls", &self.urls)
539 .field(
540 "current_index",
541 &self
542 .current_index
543 .load(std::sync::atomic::Ordering::Relaxed),
544 )
545 .finish()
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552
553 #[test]
554 fn test_chain_id_names() {
555 assert_eq!(ChainId::ETHEREUM_MAINNET.name(), "Ethereum Mainnet");
556 assert_eq!(ChainId::SOLANA_MAINNET.name(), "Solana Mainnet");
557 assert!(ChainId::ETHEREUM_MAINNET.is_evm());
558 assert!(ChainId::SOLANA_MAINNET.is_solana());
559 }
560
561 #[test]
562 fn test_balance_formatting() {
563 let balance = Balance::new("1000000000000000000", 18, "ETH");
565 assert_eq!(balance.formatted, "1");
566
567 let balance = Balance::new("1500000000000000000", 18, "ETH");
569 assert_eq!(balance.formatted, "1.5");
570
571 let balance = Balance::new("1000000000000000", 18, "ETH");
573 assert_eq!(balance.formatted, "0.001");
574
575 let balance = Balance::new("0", 18, "ETH");
577 assert_eq!(balance.formatted, "0");
578 }
579
580 #[test]
581 fn test_tx_params_builder() {
582 let params = TxParams::new("0xfrom", "0xto", "1.0")
583 .with_gas_limit(21000)
584 .with_priority(TxPriority::High);
585
586 assert_eq!(params.gas_limit, Some(21000));
587 assert_eq!(params.priority, TxPriority::High);
588 }
589
590 #[test]
591 fn test_tx_priority_default() {
592 let priority = TxPriority::default();
593 assert_eq!(priority, TxPriority::Medium);
594 }
595}