mpc_wallet_core/chain/evm/
mod.rs

1//! # EVM Chain Adapter
2//!
3//! Adapter for Ethereum and EVM-compatible chains with support for:
4//! - EIP-1559 transaction building
5//! - Gas estimation and priority fees
6//! - RPC failover
7//! - Nonce management
8//!
9//! ## Example
10//!
11//! ```rust,ignore
12//! use mpc_wallet_core::chain::evm::{EvmAdapter, EvmConfig};
13//!
14//! let config = EvmConfig::ethereum_mainnet();
15//! let adapter = EvmAdapter::new(config)?;
16//!
17//! let balance = adapter.get_balance("0x...").await?;
18//! ```
19
20#[cfg(feature = "aa")]
21pub mod aa;
22
23use super::{
24    Balance, ChainAdapter, ChainId, GasPrice, GasPrices, RpcClient, SignedTx, TxHash, TxParams,
25    TxPriority, TxReceipt, TxStatus, TxSummary, UnsignedTx,
26};
27use crate::{Error, Result, Signature};
28use alloy_primitives::{Address, Bytes, U256};
29use alloy_rlp::{Encodable, RlpEncodable};
30use async_trait::async_trait;
31use k256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint};
32use serde::{Deserialize, Serialize};
33use std::str::FromStr;
34use tiny_keccak::{Hasher, Keccak};
35
36// ============================================================================
37// Configuration
38// ============================================================================
39
40/// Configuration for EVM adapter
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct EvmConfig {
43    /// Chain identifier
44    pub chain_id: ChainId,
45    /// RPC endpoint URLs (for failover)
46    pub rpc_urls: Vec<String>,
47    /// Block explorer URL (optional)
48    pub explorer_url: Option<String>,
49    /// Native currency symbol
50    pub symbol: String,
51    /// Native currency decimals (18 for most EVM chains)
52    pub decimals: u8,
53    /// Whether EIP-1559 is supported
54    pub eip1559_supported: bool,
55}
56
57impl EvmConfig {
58    /// Create config for Ethereum Mainnet
59    pub fn ethereum_mainnet() -> Self {
60        Self {
61            chain_id: ChainId::ETHEREUM_MAINNET,
62            rpc_urls: vec![
63                "https://eth.llamarpc.com".to_string(),
64                "https://rpc.ankr.com/eth".to_string(),
65                "https://cloudflare-eth.com".to_string(),
66            ],
67            explorer_url: Some("https://etherscan.io".to_string()),
68            symbol: "ETH".to_string(),
69            decimals: 18,
70            eip1559_supported: true,
71        }
72    }
73
74    /// Create config for Ethereum Sepolia testnet
75    pub fn ethereum_sepolia() -> Self {
76        Self {
77            chain_id: ChainId::ETHEREUM_SEPOLIA,
78            rpc_urls: vec![
79                "https://rpc.sepolia.org".to_string(),
80                "https://rpc.ankr.com/eth_sepolia".to_string(),
81            ],
82            explorer_url: Some("https://sepolia.etherscan.io".to_string()),
83            symbol: "ETH".to_string(),
84            decimals: 18,
85            eip1559_supported: true,
86        }
87    }
88
89    /// Create config for Arbitrum One
90    pub fn arbitrum_one() -> Self {
91        Self {
92            chain_id: ChainId::ARBITRUM_ONE,
93            rpc_urls: vec![
94                "https://arb1.arbitrum.io/rpc".to_string(),
95                "https://rpc.ankr.com/arbitrum".to_string(),
96            ],
97            explorer_url: Some("https://arbiscan.io".to_string()),
98            symbol: "ETH".to_string(),
99            decimals: 18,
100            eip1559_supported: true,
101        }
102    }
103
104    /// Create config for Base
105    pub fn base() -> Self {
106        Self {
107            chain_id: ChainId::BASE,
108            rpc_urls: vec![
109                "https://mainnet.base.org".to_string(),
110                "https://base.llamarpc.com".to_string(),
111            ],
112            explorer_url: Some("https://basescan.org".to_string()),
113            symbol: "ETH".to_string(),
114            decimals: 18,
115            eip1559_supported: true,
116        }
117    }
118
119    /// Create config for Optimism
120    pub fn optimism() -> Self {
121        Self {
122            chain_id: ChainId::OPTIMISM,
123            rpc_urls: vec![
124                "https://mainnet.optimism.io".to_string(),
125                "https://rpc.ankr.com/optimism".to_string(),
126            ],
127            explorer_url: Some("https://optimistic.etherscan.io".to_string()),
128            symbol: "ETH".to_string(),
129            decimals: 18,
130            eip1559_supported: true,
131        }
132    }
133
134    /// Create config for Polygon
135    pub fn polygon() -> Self {
136        Self {
137            chain_id: ChainId::POLYGON,
138            rpc_urls: vec![
139                "https://polygon-rpc.com".to_string(),
140                "https://rpc.ankr.com/polygon".to_string(),
141            ],
142            explorer_url: Some("https://polygonscan.com".to_string()),
143            symbol: "MATIC".to_string(),
144            decimals: 18,
145            eip1559_supported: true,
146        }
147    }
148
149    /// Create config for BNB Smart Chain
150    pub fn bsc() -> Self {
151        Self {
152            chain_id: ChainId::BSC,
153            rpc_urls: vec![
154                "https://bsc-dataseed.binance.org".to_string(),
155                "https://rpc.ankr.com/bsc".to_string(),
156            ],
157            explorer_url: Some("https://bscscan.com".to_string()),
158            symbol: "BNB".to_string(),
159            decimals: 18,
160            eip1559_supported: false,
161        }
162    }
163
164    /// Create a custom config
165    pub fn custom(chain_id: u64, rpc_urls: Vec<String>, symbol: &str) -> Self {
166        Self {
167            chain_id: ChainId(chain_id),
168            rpc_urls,
169            explorer_url: None,
170            symbol: symbol.to_string(),
171            decimals: 18,
172            eip1559_supported: true,
173        }
174    }
175
176    /// Set explorer URL
177    pub fn with_explorer(mut self, url: impl Into<String>) -> Self {
178        self.explorer_url = Some(url.into());
179        self
180    }
181
182    /// Set EIP-1559 support
183    pub fn with_eip1559(mut self, supported: bool) -> Self {
184        self.eip1559_supported = supported;
185        self
186    }
187}
188
189// ============================================================================
190// EIP-1559 Transaction Type
191// ============================================================================
192
193/// EIP-1559 transaction structure
194#[derive(Debug, Clone, RlpEncodable)]
195struct Eip1559Transaction {
196    chain_id: u64,
197    nonce: u64,
198    max_priority_fee_per_gas: u128,
199    max_fee_per_gas: u128,
200    gas_limit: u64,
201    to: Address,
202    value: U256,
203    data: Bytes,
204    access_list: Vec<AccessListItem>,
205}
206
207/// Access list item for EIP-2930
208#[derive(Debug, Clone, RlpEncodable)]
209struct AccessListItem {
210    address: Address,
211    storage_keys: Vec<alloy_primitives::B256>,
212}
213
214impl Eip1559Transaction {
215    /// Get the signing hash for EIP-1559 transaction
216    fn signing_hash(&self) -> [u8; 32] {
217        let mut encoded = vec![0x02]; // EIP-1559 type
218        self.encode(&mut encoded);
219
220        let mut hasher = Keccak::v256();
221        hasher.update(&encoded);
222        let mut hash = [0u8; 32];
223        hasher.finalize(&mut hash);
224        hash
225    }
226
227    /// Encode the transaction with signature
228    fn encode_signed(&self, signature: &Signature) -> Vec<u8> {
229        // EIP-1559 signed tx: 0x02 || rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, signatureYParity, signatureR, signatureS])
230        let mut stream = alloy_rlp::BytesMut::new();
231
232        // Create the full list including signature
233        alloy_rlp::Header {
234            list: true,
235            payload_length: self.rlp_payload_length() + signature_rlp_length(signature),
236        }
237        .encode(&mut stream);
238
239        // Encode transaction fields
240        self.chain_id.encode(&mut stream);
241        self.nonce.encode(&mut stream);
242        self.max_priority_fee_per_gas.encode(&mut stream);
243        self.max_fee_per_gas.encode(&mut stream);
244        self.gas_limit.encode(&mut stream);
245        self.to.encode(&mut stream);
246        self.value.encode(&mut stream);
247        self.data.encode(&mut stream);
248        self.access_list.encode(&mut stream);
249
250        // Encode signature (y_parity, r, s)
251        let y_parity: u8 = signature.recovery_id;
252        y_parity.encode(&mut stream);
253
254        let r = U256::from_be_slice(&signature.r);
255        r.encode(&mut stream);
256
257        let s = U256::from_be_slice(&signature.s);
258        s.encode(&mut stream);
259
260        // Prepend type byte
261        let mut result = vec![0x02];
262        result.extend_from_slice(&stream);
263        result
264    }
265
266    fn rlp_payload_length(&self) -> usize {
267        self.chain_id.length()
268            + self.nonce.length()
269            + self.max_priority_fee_per_gas.length()
270            + self.max_fee_per_gas.length()
271            + self.gas_limit.length()
272            + self.to.length()
273            + self.value.length()
274            + self.data.length()
275            + self.access_list.length()
276    }
277}
278
279fn signature_rlp_length(sig: &Signature) -> usize {
280    let y_parity: u8 = sig.recovery_id;
281    let r = U256::from_be_slice(&sig.r);
282    let s = U256::from_be_slice(&sig.s);
283    y_parity.length() + r.length() + s.length()
284}
285
286// ============================================================================
287// Legacy Transaction Type
288// ============================================================================
289
290/// Legacy transaction for non-EIP-1559 chains
291#[derive(Debug, Clone, RlpEncodable)]
292struct LegacyTransaction {
293    nonce: u64,
294    gas_price: u128,
295    gas_limit: u64,
296    to: Address,
297    value: U256,
298    data: Bytes,
299}
300
301impl LegacyTransaction {
302    /// Get the signing hash for legacy transaction (EIP-155)
303    fn signing_hash(&self, chain_id: u64) -> [u8; 32] {
304        // EIP-155 signing: rlp([nonce, gasprice, gas, to, value, data, chainId, 0, 0])
305        let mut stream = alloy_rlp::BytesMut::new();
306
307        alloy_rlp::Header {
308            list: true,
309            payload_length: self.rlp_payload_length()
310                + chain_id.length()
311                + 0u8.length()
312                + 0u8.length(),
313        }
314        .encode(&mut stream);
315
316        self.nonce.encode(&mut stream);
317        self.gas_price.encode(&mut stream);
318        self.gas_limit.encode(&mut stream);
319        self.to.encode(&mut stream);
320        self.value.encode(&mut stream);
321        self.data.encode(&mut stream);
322        chain_id.encode(&mut stream);
323        0u8.encode(&mut stream);
324        0u8.encode(&mut stream);
325
326        let mut hasher = Keccak::v256();
327        hasher.update(&stream);
328        let mut hash = [0u8; 32];
329        hasher.finalize(&mut hash);
330        hash
331    }
332
333    /// Encode the transaction with signature (EIP-155)
334    fn encode_signed(&self, signature: &Signature, chain_id: u64) -> Vec<u8> {
335        // v = recovery_id + 35 + chain_id * 2
336        let v = signature.recovery_id as u64 + 35 + chain_id * 2;
337        let r = U256::from_be_slice(&signature.r);
338        let s = U256::from_be_slice(&signature.s);
339
340        let mut stream = alloy_rlp::BytesMut::new();
341
342        alloy_rlp::Header {
343            list: true,
344            payload_length: self.rlp_payload_length() + v.length() + r.length() + s.length(),
345        }
346        .encode(&mut stream);
347
348        self.nonce.encode(&mut stream);
349        self.gas_price.encode(&mut stream);
350        self.gas_limit.encode(&mut stream);
351        self.to.encode(&mut stream);
352        self.value.encode(&mut stream);
353        self.data.encode(&mut stream);
354        v.encode(&mut stream);
355        r.encode(&mut stream);
356        s.encode(&mut stream);
357
358        stream.to_vec()
359    }
360
361    fn rlp_payload_length(&self) -> usize {
362        self.nonce.length()
363            + self.gas_price.length()
364            + self.gas_limit.length()
365            + self.to.length()
366            + self.value.length()
367            + self.data.length()
368    }
369}
370
371// ============================================================================
372// EVM Adapter
373// ============================================================================
374
375/// EVM chain adapter implementation
376#[derive(Debug, Clone)]
377pub struct EvmAdapter {
378    config: EvmConfig,
379    rpc: RpcClient,
380}
381
382impl EvmAdapter {
383    /// Create a new EVM adapter
384    pub fn new(config: EvmConfig) -> Result<Self> {
385        let rpc = RpcClient::new(config.rpc_urls.clone())?;
386        Ok(Self { config, rpc })
387    }
388
389    /// Get the configuration
390    pub fn config(&self) -> &EvmConfig {
391        &self.config
392    }
393
394    /// Parse a value string to wei
395    fn parse_value(&self, value: &str) -> Result<U256> {
396        // Check if value contains a decimal point
397        if value.contains('.') {
398            let parts: Vec<&str> = value.split('.').collect();
399            if parts.len() != 2 {
400                return Err(Error::InvalidConfig(format!("Invalid value: {}", value)));
401            }
402
403            let whole: u128 = parts[0]
404                .parse()
405                .map_err(|_| Error::InvalidConfig(format!("Invalid whole part: {}", parts[0])))?;
406
407            let mut fraction = parts[1].to_string();
408            if fraction.len() > self.config.decimals as usize {
409                return Err(Error::InvalidConfig(format!(
410                    "Too many decimal places: {}",
411                    value
412                )));
413            }
414
415            // Pad with zeros
416            while fraction.len() < self.config.decimals as usize {
417                fraction.push('0');
418            }
419
420            let fraction_value: u128 = fraction.parse().map_err(|_| {
421                Error::InvalidConfig(format!("Invalid fraction part: {}", parts[1]))
422            })?;
423
424            let multiplier = 10u128.pow(self.config.decimals as u32);
425            let total = whole
426                .checked_mul(multiplier)
427                .and_then(|v| v.checked_add(fraction_value))
428                .ok_or_else(|| Error::InvalidConfig("Value overflow".into()))?;
429
430            Ok(U256::from(total))
431        } else {
432            // Assume raw wei value if no decimal
433            let value: u128 = value
434                .parse()
435                .map_err(|_| Error::InvalidConfig(format!("Invalid value: {}", value)))?;
436            Ok(U256::from(value))
437        }
438    }
439
440    /// Get gas prices using eth_feeHistory
441    async fn get_eip1559_prices(&self) -> Result<GasPrices> {
442        #[derive(Deserialize)]
443        struct FeeHistory {
444            #[serde(rename = "baseFeePerGas")]
445            base_fee_per_gas: Vec<String>,
446            reward: Option<Vec<Vec<String>>>,
447        }
448
449        let result: FeeHistory = self
450            .rpc
451            .request(
452                "eth_feeHistory",
453                serde_json::json!([20, "latest", [10, 50, 90]]),
454            )
455            .await?;
456
457        // Parse base fee (last value is predicted next block)
458        let base_fee = result
459            .base_fee_per_gas
460            .last()
461            .and_then(|s| parse_hex_u128(s).ok())
462            .unwrap_or(0);
463
464        // Calculate priority fees from reward data
465        let (low_tip, medium_tip, high_tip) = if let Some(rewards) = &result.reward {
466            let low_tips: Vec<u128> = rewards
467                .iter()
468                .filter_map(|r| r.first().and_then(|s| parse_hex_u128(s).ok()))
469                .collect();
470            let medium_tips: Vec<u128> = rewards
471                .iter()
472                .filter_map(|r| r.get(1).and_then(|s| parse_hex_u128(s).ok()))
473                .collect();
474            let high_tips: Vec<u128> = rewards
475                .iter()
476                .filter_map(|r| r.get(2).and_then(|s| parse_hex_u128(s).ok()))
477                .collect();
478
479            (
480                median(&low_tips).unwrap_or(1_000_000_000),    // 1 gwei
481                median(&medium_tips).unwrap_or(2_000_000_000), // 2 gwei
482                median(&high_tips).unwrap_or(5_000_000_000),   // 5 gwei
483            )
484        } else {
485            // Default fallback values
486            (1_000_000_000, 2_000_000_000, 5_000_000_000)
487        };
488
489        Ok(GasPrices {
490            low: GasPrice {
491                max_fee: base_fee + low_tip,
492                max_priority_fee: low_tip,
493                estimated_wait_secs: Some(60),
494            },
495            medium: GasPrice {
496                max_fee: base_fee * 2 + medium_tip,
497                max_priority_fee: medium_tip,
498                estimated_wait_secs: Some(30),
499            },
500            high: GasPrice {
501                max_fee: base_fee * 3 + high_tip,
502                max_priority_fee: high_tip,
503                estimated_wait_secs: Some(15),
504            },
505            base_fee: Some(base_fee),
506        })
507    }
508
509    /// Get legacy gas price
510    async fn get_legacy_price(&self) -> Result<GasPrices> {
511        let gas_price: String = self
512            .rpc
513            .request("eth_gasPrice", serde_json::json!([]))
514            .await?;
515        let price = parse_hex_u128(&gas_price)?;
516
517        Ok(GasPrices {
518            low: GasPrice {
519                max_fee: price,
520                max_priority_fee: 0,
521                estimated_wait_secs: Some(60),
522            },
523            medium: GasPrice {
524                max_fee: price * 110 / 100, // +10%
525                max_priority_fee: 0,
526                estimated_wait_secs: Some(30),
527            },
528            high: GasPrice {
529                max_fee: price * 130 / 100, // +30%
530                max_priority_fee: 0,
531                estimated_wait_secs: Some(15),
532            },
533            base_fee: None,
534        })
535    }
536}
537
538#[async_trait]
539impl ChainAdapter for EvmAdapter {
540    fn chain_id(&self) -> ChainId {
541        self.config.chain_id
542    }
543
544    fn native_symbol(&self) -> &str {
545        &self.config.symbol
546    }
547
548    fn native_decimals(&self) -> u8 {
549        self.config.decimals
550    }
551
552    async fn get_balance(&self, address: &str) -> Result<Balance> {
553        let result: String = self
554            .rpc
555            .request("eth_getBalance", serde_json::json!([address, "latest"]))
556            .await?;
557
558        let raw_value = parse_hex_u128(&result)?;
559
560        Ok(Balance::new(
561            raw_value.to_string(),
562            self.config.decimals,
563            &self.config.symbol,
564        ))
565    }
566
567    async fn get_nonce(&self, address: &str) -> Result<u64> {
568        let result: String = self
569            .rpc
570            .request(
571                "eth_getTransactionCount",
572                serde_json::json!([address, "latest"]),
573            )
574            .await?;
575
576        parse_hex_u64(&result)
577    }
578
579    async fn build_transaction(&self, params: TxParams) -> Result<UnsignedTx> {
580        // Get nonce if not provided
581        let nonce = match params.nonce {
582            Some(n) => n,
583            None => self.get_nonce(&params.from).await?,
584        };
585
586        // Parse destination address
587        let to = Address::from_str(&params.to)
588            .map_err(|e| Error::InvalidConfig(format!("Invalid to address: {}", e)))?;
589
590        // Parse value
591        let value = self.parse_value(&params.value)?;
592
593        // Get gas prices
594        let gas_prices = self.get_gas_prices().await?;
595        let gas_price = match params.priority {
596            TxPriority::Low => &gas_prices.low,
597            TxPriority::Medium => &gas_prices.medium,
598            TxPriority::High | TxPriority::Urgent => &gas_prices.high,
599        };
600
601        // Estimate gas if not provided
602        let gas_limit = match params.gas_limit {
603            Some(limit) => limit,
604            None => self.estimate_gas(&params).await?,
605        };
606
607        // Prepare data
608        let data = params
609            .data
610            .as_ref()
611            .map(|d| Bytes::from(d.clone()))
612            .unwrap_or_default();
613
614        let (signing_payload, raw_tx) = if self.config.eip1559_supported {
615            let tx = Eip1559Transaction {
616                chain_id: self.config.chain_id.0,
617                nonce,
618                max_priority_fee_per_gas: gas_price.max_priority_fee,
619                max_fee_per_gas: gas_price.max_fee,
620                gas_limit,
621                to,
622                value,
623                data,
624                access_list: vec![],
625            };
626
627            let signing_hash = tx.signing_hash();
628            let mut raw = vec![0x02];
629            tx.encode(&mut raw);
630
631            (signing_hash.to_vec(), raw)
632        } else {
633            let tx = LegacyTransaction {
634                nonce,
635                gas_price: gas_price.max_fee,
636                gas_limit,
637                to,
638                value,
639                data,
640            };
641
642            let signing_hash = tx.signing_hash(self.config.chain_id.0);
643            let mut raw = alloy_rlp::BytesMut::new();
644            tx.encode(&mut raw);
645
646            (signing_hash.to_vec(), raw.to_vec())
647        };
648
649        // Calculate estimated fee
650        let estimated_fee_wei = gas_price.max_fee * gas_limit as u128;
651        let estimated_fee = Balance::new(
652            estimated_fee_wei.to_string(),
653            self.config.decimals,
654            &self.config.symbol,
655        )
656        .formatted;
657
658        let summary = TxSummary {
659            tx_type: if params.data.is_some() {
660                "Contract Call".to_string()
661            } else {
662                "Transfer".to_string()
663            },
664            from: params.from.clone(),
665            to: params.to.clone(),
666            value: format!("{} {}", params.value, self.config.symbol),
667            estimated_fee: format!("{} {}", estimated_fee, self.config.symbol),
668            details: None,
669        };
670
671        Ok(UnsignedTx {
672            chain_id: self.config.chain_id,
673            signing_payload,
674            raw_tx,
675            summary,
676        })
677    }
678
679    async fn broadcast(&self, signed_tx: &SignedTx) -> Result<TxHash> {
680        let raw_hex = format!("0x{}", hex::encode(&signed_tx.raw_tx));
681
682        let result: String = self
683            .rpc
684            .request("eth_sendRawTransaction", serde_json::json!([raw_hex]))
685            .await?;
686
687        let explorer_url = self.explorer_tx_url(&result);
688
689        Ok(TxHash {
690            hash: result,
691            explorer_url,
692        })
693    }
694
695    fn derive_address(&self, public_key: &[u8]) -> Result<String> {
696        // Public key should be uncompressed (65 bytes) or compressed (33 bytes)
697        let pk_bytes = if public_key.len() == 33 {
698            // Decompress the public key
699            let point = k256::EncodedPoint::from_bytes(public_key)
700                .map_err(|e| Error::Crypto(format!("Invalid public key: {}", e)))?;
701            let affine = k256::AffinePoint::from_encoded_point(&point);
702            let affine: k256::AffinePoint = Option::from(affine)
703                .ok_or_else(|| Error::Crypto("Failed to decompress public key".into()))?;
704            affine.to_encoded_point(false).as_bytes()[1..].to_vec() // Skip 0x04 prefix
705        } else if public_key.len() == 65 {
706            public_key[1..].to_vec() // Skip 0x04 prefix
707        } else if public_key.len() == 64 {
708            public_key.to_vec()
709        } else {
710            return Err(Error::Crypto(format!(
711                "Invalid public key length: {}",
712                public_key.len()
713            )));
714        };
715
716        // Keccak256 hash of public key
717        let mut hasher = Keccak::v256();
718        hasher.update(&pk_bytes);
719        let mut hash = [0u8; 32];
720        hasher.finalize(&mut hash);
721
722        // Take last 20 bytes
723        let address = &hash[12..];
724        Ok(format!("0x{}", hex::encode(address)))
725    }
726
727    async fn get_gas_prices(&self) -> Result<GasPrices> {
728        if self.config.eip1559_supported {
729            self.get_eip1559_prices().await
730        } else {
731            self.get_legacy_price().await
732        }
733    }
734
735    async fn estimate_gas(&self, params: &TxParams) -> Result<u64> {
736        let tx_object = serde_json::json!({
737            "from": params.from,
738            "to": params.to,
739            "value": format!("0x{:x}", self.parse_value(&params.value)?),
740            "data": params.data.as_ref().map(|d| format!("0x{}", hex::encode(d))),
741        });
742
743        let result: String = self
744            .rpc
745            .request("eth_estimateGas", serde_json::json!([tx_object]))
746            .await?;
747
748        let gas = parse_hex_u64(&result)?;
749        // Add 20% buffer
750        Ok(gas * 120 / 100)
751    }
752
753    async fn wait_for_confirmation(&self, tx_hash: &str, timeout_secs: u64) -> Result<TxReceipt> {
754        let start = std::time::Instant::now();
755        let timeout = std::time::Duration::from_secs(timeout_secs);
756
757        loop {
758            if start.elapsed() > timeout {
759                return Err(Error::Timeout(format!(
760                    "Transaction {} not confirmed within {} seconds",
761                    tx_hash, timeout_secs
762                )));
763            }
764
765            #[derive(Deserialize)]
766            struct Receipt {
767                #[serde(rename = "blockNumber")]
768                block_number: Option<String>,
769                status: Option<String>,
770                #[serde(rename = "gasUsed")]
771                gas_used: Option<String>,
772                #[serde(rename = "effectiveGasPrice")]
773                effective_gas_price: Option<String>,
774            }
775
776            let result: Option<Receipt> = self
777                .rpc
778                .request("eth_getTransactionReceipt", serde_json::json!([tx_hash]))
779                .await?;
780
781            if let Some(receipt) = result
782                && let Some(block_num) = receipt.block_number
783            {
784                let status = receipt
785                    .status
786                    .as_ref()
787                    .map(|s| {
788                        if s == "0x1" {
789                            TxStatus::Success
790                        } else {
791                            TxStatus::Failed
792                        }
793                    })
794                    .unwrap_or(TxStatus::Pending);
795
796                return Ok(TxReceipt {
797                    tx_hash: tx_hash.to_string(),
798                    block_number: parse_hex_u64(&block_num)?,
799                    status,
800                    gas_used: receipt
801                        .gas_used
802                        .as_ref()
803                        .and_then(|s| parse_hex_u64(s).ok()),
804                    effective_gas_price: receipt
805                        .effective_gas_price
806                        .as_ref()
807                        .and_then(|s| parse_hex_u128(s).ok()),
808                });
809            }
810
811            // Wait before polling again
812            tokio::time::sleep(std::time::Duration::from_secs(2)).await;
813        }
814    }
815
816    fn is_valid_address(&self, address: &str) -> bool {
817        // Check basic format
818        if !address.starts_with("0x") || address.len() != 42 {
819            return false;
820        }
821        // Check if all characters after 0x are valid hex
822        address[2..].chars().all(|c| c.is_ascii_hexdigit())
823    }
824
825    fn explorer_tx_url(&self, tx_hash: &str) -> Option<String> {
826        self.config
827            .explorer_url
828            .as_ref()
829            .map(|base| format!("{}/tx/{}", base, tx_hash))
830    }
831
832    fn explorer_address_url(&self, address: &str) -> Option<String> {
833        self.config
834            .explorer_url
835            .as_ref()
836            .map(|base| format!("{}/address/{}", base, address))
837    }
838
839    fn finalize_transaction(
840        &self,
841        unsigned_tx: &UnsignedTx,
842        signature: &Signature,
843    ) -> Result<SignedTx> {
844        // Decode the raw transaction to get the original structure
845        let raw_tx = if unsigned_tx.raw_tx.first() == Some(&0x02) {
846            // EIP-1559 transaction
847            // Re-decode and re-encode with signature
848            // For simplicity, we parse the encoded tx and add signature
849            self.finalize_eip1559_tx(unsigned_tx, signature)?
850        } else {
851            // Legacy transaction
852            self.finalize_legacy_tx(unsigned_tx, signature)?
853        };
854
855        // Calculate tx hash
856        let mut hasher = Keccak::v256();
857        hasher.update(&raw_tx);
858        let mut hash = [0u8; 32];
859        hasher.finalize(&mut hash);
860
861        Ok(SignedTx {
862            chain_id: self.config.chain_id,
863            raw_tx,
864            tx_hash: format!("0x{}", hex::encode(hash)),
865        })
866    }
867}
868
869impl EvmAdapter {
870    fn finalize_eip1559_tx(
871        &self,
872        unsigned_tx: &UnsignedTx,
873        signature: &Signature,
874    ) -> Result<Vec<u8>> {
875        // Skip the 0x02 type byte and decode the RLP
876        let rlp_data = &unsigned_tx.raw_tx[1..];
877        let decoded: Vec<alloy_rlp::Bytes> = alloy_rlp::Decodable::decode(&mut &rlp_data[..])
878            .map_err(|e| Error::ChainError(format!("Failed to decode transaction: {}", e)))?;
879
880        if decoded.len() < 9 {
881            return Err(Error::ChainError(
882                "Invalid EIP-1559 transaction format".into(),
883            ));
884        }
885
886        // Reconstruct the transaction
887        let chain_id: u64 = decode_u64_from_bytes(&decoded[0])?;
888        let nonce: u64 = decode_u64_from_bytes(&decoded[1])?;
889        let max_priority_fee: u128 = decode_u128_from_bytes(&decoded[2])?;
890        let max_fee: u128 = decode_u128_from_bytes(&decoded[3])?;
891        let gas_limit: u64 = decode_u64_from_bytes(&decoded[4])?;
892        let to = Address::from_slice(&decoded[5]);
893        let value = U256::from_be_slice(&decoded[6]);
894        let data = Bytes::from(decoded[7].to_vec());
895
896        let tx = Eip1559Transaction {
897            chain_id,
898            nonce,
899            max_priority_fee_per_gas: max_priority_fee,
900            max_fee_per_gas: max_fee,
901            gas_limit,
902            to,
903            value,
904            data,
905            access_list: vec![], // Empty for now
906        };
907
908        Ok(tx.encode_signed(signature))
909    }
910
911    fn finalize_legacy_tx(
912        &self,
913        unsigned_tx: &UnsignedTx,
914        signature: &Signature,
915    ) -> Result<Vec<u8>> {
916        let decoded: Vec<alloy_rlp::Bytes> =
917            alloy_rlp::Decodable::decode(&mut &unsigned_tx.raw_tx[..])
918                .map_err(|e| Error::ChainError(format!("Failed to decode transaction: {}", e)))?;
919
920        if decoded.len() < 6 {
921            return Err(Error::ChainError(
922                "Invalid legacy transaction format".into(),
923            ));
924        }
925
926        let tx = LegacyTransaction {
927            nonce: decode_u64_from_bytes(&decoded[0])?,
928            gas_price: decode_u128_from_bytes(&decoded[1])?,
929            gas_limit: decode_u64_from_bytes(&decoded[2])?,
930            to: Address::from_slice(&decoded[3]),
931            value: U256::from_be_slice(&decoded[4]),
932            data: Bytes::from(decoded[5].to_vec()),
933        };
934
935        Ok(tx.encode_signed(signature, self.config.chain_id.0))
936    }
937}
938
939// ============================================================================
940// Helper Functions
941// ============================================================================
942
943fn parse_hex_u128(s: &str) -> Result<u128> {
944    let s = s.strip_prefix("0x").unwrap_or(s);
945    u128::from_str_radix(s, 16)
946        .map_err(|e| Error::ChainError(format!("Failed to parse hex: {}", e)))
947}
948
949fn parse_hex_u64(s: &str) -> Result<u64> {
950    let s = s.strip_prefix("0x").unwrap_or(s);
951    u64::from_str_radix(s, 16).map_err(|e| Error::ChainError(format!("Failed to parse hex: {}", e)))
952}
953
954fn decode_u64_from_bytes(bytes: &[u8]) -> Result<u64> {
955    if bytes.is_empty() {
956        return Ok(0);
957    }
958    if bytes.len() > 8 {
959        return Err(Error::ChainError("Value too large for u64".into()));
960    }
961    let mut buf = [0u8; 8];
962    buf[8 - bytes.len()..].copy_from_slice(bytes);
963    Ok(u64::from_be_bytes(buf))
964}
965
966fn decode_u128_from_bytes(bytes: &[u8]) -> Result<u128> {
967    if bytes.is_empty() {
968        return Ok(0);
969    }
970    if bytes.len() > 16 {
971        return Err(Error::ChainError("Value too large for u128".into()));
972    }
973    let mut buf = [0u8; 16];
974    buf[16 - bytes.len()..].copy_from_slice(bytes);
975    Ok(u128::from_be_bytes(buf))
976}
977
978fn median(values: &[u128]) -> Option<u128> {
979    if values.is_empty() {
980        return None;
981    }
982    let mut sorted = values.to_vec();
983    sorted.sort();
984    Some(sorted[sorted.len() / 2])
985}
986
987#[cfg(test)]
988mod tests {
989    use super::*;
990
991    #[test]
992    fn test_parse_value() {
993        let adapter = EvmAdapter::new(EvmConfig::ethereum_mainnet()).unwrap();
994
995        // Test decimal values
996        let value = adapter.parse_value("1.0").unwrap();
997        assert_eq!(value, U256::from(1_000_000_000_000_000_000u128));
998
999        let value = adapter.parse_value("0.5").unwrap();
1000        assert_eq!(value, U256::from(500_000_000_000_000_000u128));
1001
1002        let value = adapter.parse_value("1.5").unwrap();
1003        assert_eq!(value, U256::from(1_500_000_000_000_000_000u128));
1004    }
1005
1006    #[test]
1007    fn test_address_validation() {
1008        let adapter = EvmAdapter::new(EvmConfig::ethereum_mainnet()).unwrap();
1009
1010        assert!(adapter.is_valid_address("0x742d35Cc6634C0532925a3b844Bc9e7595f4e123"));
1011        assert!(!adapter.is_valid_address("0x742d35Cc")); // Too short
1012        assert!(!adapter.is_valid_address("742d35Cc6634C0532925a3b844Bc9e7595f4e123")); // No prefix
1013        assert!(!adapter.is_valid_address("0xGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG")); // Invalid hex
1014    }
1015
1016    #[test]
1017    fn test_derive_address() {
1018        let adapter = EvmAdapter::new(EvmConfig::ethereum_mainnet()).unwrap();
1019
1020        // Test with uncompressed public key (65 bytes)
1021        let pk = hex::decode("04e68acfc0253a10620dff706b0a1b1f1f5833ea3beb3bde2250d5f271f3563606672ebc45e0b7ea2e816ecb70ca03137b1c9476eec63d4632e990020b7b6fba39").unwrap();
1022        let address = adapter.derive_address(&pk).unwrap();
1023        assert!(address.starts_with("0x"));
1024        assert_eq!(address.len(), 42);
1025    }
1026
1027    #[test]
1028    fn test_explorer_urls() {
1029        let adapter = EvmAdapter::new(EvmConfig::ethereum_mainnet()).unwrap();
1030
1031        let tx_url = adapter.explorer_tx_url("0x123");
1032        assert_eq!(tx_url, Some("https://etherscan.io/tx/0x123".to_string()));
1033
1034        let addr_url = adapter.explorer_address_url("0x456");
1035        assert_eq!(
1036            addr_url,
1037            Some("https://etherscan.io/address/0x456".to_string())
1038        );
1039    }
1040
1041    #[test]
1042    fn test_chain_configs() {
1043        let mainnet = EvmConfig::ethereum_mainnet();
1044        assert_eq!(mainnet.chain_id.0, 1);
1045        assert_eq!(mainnet.symbol, "ETH");
1046        assert!(mainnet.eip1559_supported);
1047
1048        let bsc = EvmConfig::bsc();
1049        assert_eq!(bsc.chain_id.0, 56);
1050        assert_eq!(bsc.symbol, "BNB");
1051        assert!(!bsc.eip1559_supported);
1052    }
1053}