1#[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#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct EvmConfig {
43 pub chain_id: ChainId,
45 pub rpc_urls: Vec<String>,
47 pub explorer_url: Option<String>,
49 pub symbol: String,
51 pub decimals: u8,
53 pub eip1559_supported: bool,
55}
56
57impl EvmConfig {
58 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 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 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 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 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 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 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 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 pub fn with_explorer(mut self, url: impl Into<String>) -> Self {
178 self.explorer_url = Some(url.into());
179 self
180 }
181
182 pub fn with_eip1559(mut self, supported: bool) -> Self {
184 self.eip1559_supported = supported;
185 self
186 }
187}
188
189#[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#[derive(Debug, Clone, RlpEncodable)]
209struct AccessListItem {
210 address: Address,
211 storage_keys: Vec<alloy_primitives::B256>,
212}
213
214impl Eip1559Transaction {
215 fn signing_hash(&self) -> [u8; 32] {
217 let mut encoded = vec![0x02]; 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 fn encode_signed(&self, signature: &Signature) -> Vec<u8> {
229 let mut stream = alloy_rlp::BytesMut::new();
231
232 alloy_rlp::Header {
234 list: true,
235 payload_length: self.rlp_payload_length() + signature_rlp_length(signature),
236 }
237 .encode(&mut stream);
238
239 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 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 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#[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 fn signing_hash(&self, chain_id: u64) -> [u8; 32] {
304 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 fn encode_signed(&self, signature: &Signature, chain_id: u64) -> Vec<u8> {
335 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#[derive(Debug, Clone)]
377pub struct EvmAdapter {
378 config: EvmConfig,
379 rpc: RpcClient,
380}
381
382impl EvmAdapter {
383 pub fn new(config: EvmConfig) -> Result<Self> {
385 let rpc = RpcClient::new(config.rpc_urls.clone())?;
386 Ok(Self { config, rpc })
387 }
388
389 pub fn config(&self) -> &EvmConfig {
391 &self.config
392 }
393
394 fn parse_value(&self, value: &str) -> Result<U256> {
396 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 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 let value: u128 = value
434 .parse()
435 .map_err(|_| Error::InvalidConfig(format!("Invalid value: {}", value)))?;
436 Ok(U256::from(value))
437 }
438 }
439
440 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 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 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), median(&medium_tips).unwrap_or(2_000_000_000), median(&high_tips).unwrap_or(5_000_000_000), )
484 } else {
485 (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 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, max_priority_fee: 0,
526 estimated_wait_secs: Some(30),
527 },
528 high: GasPrice {
529 max_fee: price * 130 / 100, 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 let nonce = match params.nonce {
582 Some(n) => n,
583 None => self.get_nonce(¶ms.from).await?,
584 };
585
586 let to = Address::from_str(¶ms.to)
588 .map_err(|e| Error::InvalidConfig(format!("Invalid to address: {}", e)))?;
589
590 let value = self.parse_value(¶ms.value)?;
592
593 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 let gas_limit = match params.gas_limit {
603 Some(limit) => limit,
604 None => self.estimate_gas(¶ms).await?,
605 };
606
607 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 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 let pk_bytes = if public_key.len() == 33 {
698 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() } else if public_key.len() == 65 {
706 public_key[1..].to_vec() } 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 let mut hasher = Keccak::v256();
718 hasher.update(&pk_bytes);
719 let mut hash = [0u8; 32];
720 hasher.finalize(&mut hash);
721
722 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(¶ms.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 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 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
813 }
814 }
815
816 fn is_valid_address(&self, address: &str) -> bool {
817 if !address.starts_with("0x") || address.len() != 42 {
819 return false;
820 }
821 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 let raw_tx = if unsigned_tx.raw_tx.first() == Some(&0x02) {
846 self.finalize_eip1559_tx(unsigned_tx, signature)?
850 } else {
851 self.finalize_legacy_tx(unsigned_tx, signature)?
853 };
854
855 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 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 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![], };
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
939fn 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 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")); assert!(!adapter.is_valid_address("742d35Cc6634C0532925a3b844Bc9e7595f4e123")); assert!(!adapter.is_valid_address("0xGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG")); }
1015
1016 #[test]
1017 fn test_derive_address() {
1018 let adapter = EvmAdapter::new(EvmConfig::ethereum_mainnet()).unwrap();
1019
1020 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}