1pub mod dex;
89pub mod ethereum;
90pub mod solana;
91pub mod tron;
92
93pub use dex::{DexClient, DexDataSource, DiscoverToken, TokenSearchResult};
94pub use ethereum::{ApiType, EthereumClient};
95pub use solana::{SolanaClient, validate_solana_address, validate_solana_signature};
96pub use tron::{TronClient, validate_tron_address, validate_tron_tx_hash};
97
98use crate::error::Result;
99use crate::http::HttpClient;
100use async_trait::async_trait;
101use serde::{Deserialize, Serialize};
102use std::sync::Arc;
103
104#[async_trait]
122pub trait ChainClient: Send + Sync {
123 fn chain_name(&self) -> &str;
125
126 fn native_token_symbol(&self) -> &str;
128
129 async fn get_balance(&self, address: &str) -> Result<Balance>;
139
140 async fn enrich_balance_usd(&self, balance: &mut Balance);
146
147 async fn get_transaction(&self, hash: &str) -> Result<Transaction>;
157
158 async fn get_transactions(&self, address: &str, limit: u32) -> Result<Vec<Transaction>>;
169
170 async fn get_block_number(&self) -> Result<u64>;
172
173 async fn get_token_balances(&self, address: &str) -> Result<Vec<TokenBalance>>;
178
179 async fn get_token_info(&self, _address: &str) -> Result<Token> {
184 Err(crate::error::ScopeError::Chain(
185 "Token info lookup not supported on this chain".to_string(),
186 ))
187 }
188
189 async fn get_token_holders(&self, _address: &str, _limit: u32) -> Result<Vec<TokenHolder>> {
194 Ok(Vec::new())
195 }
196
197 async fn get_token_holder_count(&self, _address: &str) -> Result<u64> {
202 Ok(0)
203 }
204
205 async fn get_code(&self, _address: &str) -> Result<String> {
209 Err(crate::error::ScopeError::Chain(
210 "Code lookup not supported on this chain".to_string(),
211 ))
212 }
213
214 async fn get_storage_at(&self, _address: &str, _slot: &str) -> Result<String> {
218 Err(crate::error::ScopeError::Chain(
219 "Storage lookup not supported on this chain".to_string(),
220 ))
221 }
222}
223
224pub trait ChainClientFactory: Send + Sync {
243 fn create_chain_client(&self, chain: &str) -> Result<Box<dyn ChainClient>>;
249
250 fn create_dex_client(&self) -> Box<dyn DexDataSource>;
252}
253
254pub struct DefaultClientFactory {
259 pub chains_config: crate::config::ChainsConfig,
261 pub http: Arc<dyn HttpClient>,
263}
264
265impl ChainClientFactory for DefaultClientFactory {
266 fn create_chain_client(&self, chain: &str) -> Result<Box<dyn ChainClient>> {
267 match chain.to_lowercase().as_str() {
268 "solana" | "sol" => Ok(Box::new(SolanaClient::new_with_http(
269 &self.chains_config,
270 self.http.clone(),
271 )?)),
272 "tron" | "trx" => Ok(Box::new(TronClient::new_with_http(
273 &self.chains_config,
274 self.http.clone(),
275 )?)),
276 _ => Ok(Box::new(EthereumClient::for_chain_with_http(
277 chain,
278 &self.chains_config,
279 self.http.clone(),
280 )?)),
281 }
282 }
283
284 fn create_dex_client(&self) -> Box<dyn DexDataSource> {
285 Box::new(DexClient::new_with_http(self.http.clone()))
286 }
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct Balance {
292 pub raw: String,
294
295 pub formatted: String,
297
298 pub decimals: u8,
300
301 pub symbol: String,
303
304 #[serde(skip_serializing_if = "Option::is_none")]
306 pub usd_value: Option<f64>,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct Transaction {
312 pub hash: String,
314
315 pub block_number: Option<u64>,
317
318 pub timestamp: Option<u64>,
320
321 pub from: String,
323
324 pub to: Option<String>,
326
327 pub value: String,
329
330 pub gas_limit: u64,
332
333 pub gas_used: Option<u64>,
335
336 pub gas_price: String,
338
339 pub nonce: u64,
341
342 pub input: String,
344
345 pub status: Option<bool>,
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct Token {
352 pub contract_address: String,
354
355 pub symbol: String,
357
358 pub name: String,
360
361 pub decimals: u8,
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize)]
367pub struct TokenBalance {
368 pub token: Token,
370
371 pub balance: String,
373
374 pub formatted_balance: String,
376
377 #[serde(skip_serializing_if = "Option::is_none")]
379 pub usd_value: Option<f64>,
380}
381
382#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct TokenHolder {
389 pub address: String,
391
392 pub balance: String,
394
395 pub formatted_balance: String,
397
398 pub percentage: f64,
400
401 pub rank: u32,
403}
404
405#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct PricePoint {
408 pub timestamp: i64,
410
411 pub price: f64,
413}
414
415#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct VolumePoint {
418 pub timestamp: i64,
420
421 pub volume: f64,
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct HolderCountPoint {
428 pub timestamp: i64,
430
431 pub count: u64,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize)]
437pub struct DexPair {
438 pub dex_name: String,
440
441 pub pair_address: String,
443
444 pub base_token: String,
446
447 pub quote_token: String,
449
450 pub price_usd: f64,
452
453 pub volume_24h: f64,
455
456 pub liquidity_usd: f64,
458
459 pub price_change_24h: f64,
461
462 pub buys_24h: u64,
464
465 pub sells_24h: u64,
467
468 pub buys_6h: u64,
470
471 pub sells_6h: u64,
473
474 pub buys_1h: u64,
476
477 pub sells_1h: u64,
479
480 pub pair_created_at: Option<i64>,
482
483 pub url: Option<String>,
485}
486
487#[derive(Debug, Clone, Serialize, Deserialize)]
489pub struct TokenAnalytics {
490 pub token: Token,
492
493 pub chain: String,
495
496 pub holders: Vec<TokenHolder>,
498
499 pub total_holders: u64,
501
502 pub volume_24h: f64,
504
505 pub volume_7d: f64,
507
508 pub price_usd: f64,
510
511 pub price_change_24h: f64,
513
514 pub price_change_7d: f64,
516
517 pub liquidity_usd: f64,
519
520 #[serde(skip_serializing_if = "Option::is_none")]
522 pub market_cap: Option<f64>,
523
524 #[serde(skip_serializing_if = "Option::is_none")]
526 pub fdv: Option<f64>,
527
528 #[serde(skip_serializing_if = "Option::is_none")]
530 pub total_supply: Option<String>,
531
532 #[serde(skip_serializing_if = "Option::is_none")]
534 pub circulating_supply: Option<String>,
535
536 pub price_history: Vec<PricePoint>,
538
539 pub volume_history: Vec<VolumePoint>,
541
542 pub holder_history: Vec<HolderCountPoint>,
544
545 pub dex_pairs: Vec<DexPair>,
547
548 pub fetched_at: i64,
550
551 #[serde(skip_serializing_if = "Option::is_none")]
553 pub top_10_concentration: Option<f64>,
554
555 #[serde(skip_serializing_if = "Option::is_none")]
557 pub top_50_concentration: Option<f64>,
558
559 #[serde(skip_serializing_if = "Option::is_none")]
561 pub top_100_concentration: Option<f64>,
562
563 pub price_change_6h: f64,
565
566 pub price_change_1h: f64,
568
569 pub total_buys_24h: u64,
571
572 pub total_sells_24h: u64,
574
575 pub total_buys_6h: u64,
577
578 pub total_sells_6h: u64,
580
581 pub total_buys_1h: u64,
583
584 pub total_sells_1h: u64,
586
587 #[serde(skip_serializing_if = "Option::is_none")]
589 pub token_age_hours: Option<f64>,
590
591 #[serde(skip_serializing_if = "Option::is_none")]
593 pub image_url: Option<String>,
594
595 pub websites: Vec<String>,
597
598 pub socials: Vec<TokenSocial>,
600
601 #[serde(skip_serializing_if = "Option::is_none")]
603 pub dexscreener_url: Option<String>,
604}
605
606#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
608pub struct TokenSocial {
609 pub platform: String,
611 pub url: String,
613}
614
615#[derive(Debug, Clone, Serialize, Deserialize)]
621pub struct NftMetadata {
622 pub token_id: String,
624 pub name: Option<String>,
626 pub description: Option<String>,
628 pub image_url: Option<String>,
630 pub token_uri: Option<String>,
632 pub standard: String,
634 pub collection_name: Option<String>,
636 pub attributes: Vec<NftAttribute>,
638}
639
640#[derive(Debug, Clone, Serialize, Deserialize)]
642pub struct NftAttribute {
643 pub trait_type: String,
645 pub value: String,
647}
648
649#[derive(Debug, Clone, Serialize, Deserialize)]
655pub struct GasAnalysis {
656 pub avg_gas_used: u64,
658 pub max_gas_used: u64,
660 pub min_gas_used: u64,
662 pub total_gas_cost_wei: String,
664 pub total_gas_cost_formatted: String,
666 pub tx_count: u64,
668 pub gas_by_function: Vec<GasByFunction>,
670 pub failed_tx_count: u64,
672 pub wasted_gas: u64,
674}
675
676#[derive(Debug, Clone, Serialize, Deserialize)]
678pub struct GasByFunction {
679 pub function: String,
681 pub call_count: u64,
683 pub avg_gas: u64,
685 pub total_gas: u64,
687}
688
689#[derive(Debug, Clone)]
697pub struct ChainMetadata {
698 pub chain_id: &'static str,
700 pub native_symbol: &'static str,
702 pub native_decimals: u8,
704 pub explorer_token_base: &'static str,
706}
707
708pub fn chain_metadata(chain: &str) -> Option<ChainMetadata> {
712 match chain.to_lowercase().as_str() {
713 "ethereum" | "eth" => Some(ChainMetadata {
714 chain_id: "ethereum",
715 native_symbol: "ETH",
716 native_decimals: 18,
717 explorer_token_base: "https://etherscan.io/token",
718 }),
719 "polygon" => Some(ChainMetadata {
720 chain_id: "polygon",
721 native_symbol: "MATIC",
722 native_decimals: 18,
723 explorer_token_base: "https://polygonscan.com/token",
724 }),
725 "arbitrum" => Some(ChainMetadata {
726 chain_id: "arbitrum",
727 native_symbol: "ETH",
728 native_decimals: 18,
729 explorer_token_base: "https://arbiscan.io/token",
730 }),
731 "optimism" => Some(ChainMetadata {
732 chain_id: "optimism",
733 native_symbol: "ETH",
734 native_decimals: 18,
735 explorer_token_base: "https://optimistic.etherscan.io/token",
736 }),
737 "base" => Some(ChainMetadata {
738 chain_id: "base",
739 native_symbol: "ETH",
740 native_decimals: 18,
741 explorer_token_base: "https://basescan.org/token",
742 }),
743 "bsc" => Some(ChainMetadata {
744 chain_id: "bsc",
745 native_symbol: "BNB",
746 native_decimals: 18,
747 explorer_token_base: "https://bscscan.com/token",
748 }),
749 "solana" | "sol" => Some(ChainMetadata {
750 chain_id: "solana",
751 native_symbol: "SOL",
752 native_decimals: 9,
753 explorer_token_base: "https://solscan.io/token",
754 }),
755 "tron" | "trx" => Some(ChainMetadata {
756 chain_id: "tron",
757 native_symbol: "TRX",
758 native_decimals: 6,
759 explorer_token_base: "https://tronscan.org/#/token20",
760 }),
761 _ => None,
762 }
763}
764
765pub fn native_symbol(chain: &str) -> &'static str {
767 chain_metadata(chain)
768 .map(|m| m.native_symbol)
769 .unwrap_or("???")
770}
771
772pub fn infer_chain_from_address(address: &str) -> Option<&'static str> {
798 if address.starts_with('T') && address.len() == 34 && bs58::decode(address).into_vec().is_ok() {
800 return Some("tron");
801 }
802
803 if address.starts_with("0x")
805 && address.len() == 42
806 && address[2..].chars().all(|c| c.is_ascii_hexdigit())
807 {
808 return Some("ethereum");
809 }
810
811 if address.len() >= 32
813 && address.len() <= 44
814 && let Ok(decoded) = bs58::decode(address).into_vec()
815 && decoded.len() == 32
816 {
817 return Some("solana");
818 }
819
820 None
821}
822
823pub fn infer_chain_from_hash(hash: &str) -> Option<&'static str> {
848 if hash.starts_with("0x")
850 && hash.len() == 66
851 && hash[2..].chars().all(|c| c.is_ascii_hexdigit())
852 {
853 return Some("ethereum");
854 }
855
856 if hash.len() == 64 && hash.chars().all(|c| c.is_ascii_hexdigit()) {
858 return Some("tron");
859 }
860
861 if hash.len() >= 80
863 && hash.len() <= 90
864 && let Ok(decoded) = bs58::decode(hash).into_vec()
865 && decoded.len() == 64
866 {
867 return Some("solana");
868 }
869
870 None
871}
872
873pub fn analyze_gas_usage(transactions: &[Transaction]) -> GasAnalysis {
878 if transactions.is_empty() {
879 return GasAnalysis {
880 avg_gas_used: 0,
881 max_gas_used: 0,
882 min_gas_used: 0,
883 total_gas_cost_wei: "0".to_string(),
884 total_gas_cost_formatted: "0".to_string(),
885 tx_count: 0,
886 gas_by_function: vec![],
887 failed_tx_count: 0,
888 wasted_gas: 0,
889 };
890 }
891
892 let mut total_gas: u64 = 0;
893 let mut max_gas: u64 = 0;
894 let mut min_gas: u64 = u64::MAX;
895 let mut failed_count: u64 = 0;
896 let mut wasted_gas: u64 = 0;
897 let mut function_gas: std::collections::HashMap<String, (u64, u64)> =
898 std::collections::HashMap::new();
899
900 for tx in transactions {
901 let gas_used = tx.gas_used.unwrap_or(0);
902 total_gas += gas_used;
903 if gas_used > max_gas {
904 max_gas = gas_used;
905 }
906 if gas_used < min_gas {
907 min_gas = gas_used;
908 }
909
910 if tx.status == Some(false) {
912 failed_count += 1;
913 wasted_gas += gas_used;
914 }
915
916 let selector = if tx.input.len() >= 10 {
918 tx.input[..10].to_string()
919 } else if tx.input.is_empty() || tx.input == "0x" {
920 "transfer()".to_string()
921 } else {
922 tx.input.clone()
923 };
924
925 let entry = function_gas.entry(selector).or_insert((0, 0));
926 entry.0 += 1; entry.1 += gas_used; }
929
930 let tx_count = transactions.len() as u64;
931 let avg_gas = if tx_count > 0 {
932 total_gas / tx_count
933 } else {
934 0
935 };
936
937 if min_gas == u64::MAX {
938 min_gas = 0;
939 }
940
941 let mut gas_by_function: Vec<GasByFunction> = function_gas
943 .into_iter()
944 .map(|(function, (call_count, total_gas_fn))| GasByFunction {
945 function,
946 call_count,
947 avg_gas: if call_count > 0 {
948 total_gas_fn / call_count
949 } else {
950 0
951 },
952 total_gas: total_gas_fn,
953 })
954 .collect();
955 gas_by_function.sort_by(|a, b| b.total_gas.cmp(&a.total_gas));
956
957 let total_gas_cost_formatted = format!("{} gas units", total_gas);
959
960 GasAnalysis {
961 avg_gas_used: avg_gas,
962 max_gas_used: max_gas,
963 min_gas_used: min_gas,
964 total_gas_cost_wei: total_gas.to_string(),
965 total_gas_cost_formatted,
966 tx_count,
967 gas_by_function,
968 failed_tx_count: failed_count,
969 wasted_gas,
970 }
971}
972
973#[cfg(test)]
978mod tests {
979 use super::*;
980
981 #[test]
982 fn test_balance_serialization() {
983 let balance = Balance {
984 raw: "1000000000000000000".to_string(),
985 formatted: "1.0".to_string(),
986 decimals: 18,
987 symbol: "ETH".to_string(),
988 usd_value: Some(3500.0),
989 };
990
991 let json = serde_json::to_string(&balance).unwrap();
992 assert!(json.contains("1000000000000000000"));
993 assert!(json.contains("1.0"));
994 assert!(json.contains("ETH"));
995 assert!(json.contains("3500"));
996
997 let deserialized: Balance = serde_json::from_str(&json).unwrap();
998 assert_eq!(deserialized.raw, balance.raw);
999 assert_eq!(deserialized.decimals, 18);
1000 }
1001
1002 #[test]
1003 fn test_balance_without_usd() {
1004 let balance = Balance {
1005 raw: "1000000000000000000".to_string(),
1006 formatted: "1.0".to_string(),
1007 decimals: 18,
1008 symbol: "ETH".to_string(),
1009 usd_value: None,
1010 };
1011
1012 let json = serde_json::to_string(&balance).unwrap();
1013 assert!(!json.contains("usd_value"));
1014 }
1015
1016 #[test]
1017 fn test_transaction_serialization() {
1018 let tx = Transaction {
1019 hash: "0xabc123".to_string(),
1020 block_number: Some(12345678),
1021 timestamp: Some(1700000000),
1022 from: "0xfrom".to_string(),
1023 to: Some("0xto".to_string()),
1024 value: "1.0".to_string(),
1025 gas_limit: 21000,
1026 gas_used: Some(21000),
1027 gas_price: "20000000000".to_string(),
1028 nonce: 42,
1029 input: "0x".to_string(),
1030 status: Some(true),
1031 };
1032
1033 let json = serde_json::to_string(&tx).unwrap();
1034 assert!(json.contains("0xabc123"));
1035 assert!(json.contains("12345678"));
1036 assert!(json.contains("0xfrom"));
1037 assert!(json.contains("0xto"));
1038
1039 let deserialized: Transaction = serde_json::from_str(&json).unwrap();
1040 assert_eq!(deserialized.hash, tx.hash);
1041 assert_eq!(deserialized.nonce, 42);
1042 }
1043
1044 #[test]
1045 fn test_pending_transaction_serialization() {
1046 let tx = Transaction {
1047 hash: "0xpending".to_string(),
1048 block_number: None,
1049 timestamp: None,
1050 from: "0xfrom".to_string(),
1051 to: Some("0xto".to_string()),
1052 value: "1.0".to_string(),
1053 gas_limit: 21000,
1054 gas_used: None,
1055 gas_price: "20000000000".to_string(),
1056 nonce: 0,
1057 input: "0x".to_string(),
1058 status: None,
1059 };
1060
1061 let json = serde_json::to_string(&tx).unwrap();
1062 assert!(json.contains("0xpending"));
1063 assert!(json.contains("null")); let deserialized: Transaction = serde_json::from_str(&json).unwrap();
1066 assert!(deserialized.block_number.is_none());
1067 assert!(deserialized.status.is_none());
1068 }
1069
1070 #[test]
1071 fn test_contract_creation_transaction() {
1072 let tx = Transaction {
1073 hash: "0xcreate".to_string(),
1074 block_number: Some(100),
1075 timestamp: Some(1700000000),
1076 from: "0xdeployer".to_string(),
1077 to: None, value: "0".to_string(),
1079 gas_limit: 1000000,
1080 gas_used: Some(500000),
1081 gas_price: "20000000000".to_string(),
1082 nonce: 0,
1083 input: "0x608060...".to_string(),
1084 status: Some(true),
1085 };
1086
1087 let json = serde_json::to_string(&tx).unwrap();
1088 assert!(json.contains("\"to\":null"));
1089 }
1090
1091 #[test]
1092 fn test_token_serialization() {
1093 let token = Token {
1094 contract_address: "0xtoken".to_string(),
1095 symbol: "USDC".to_string(),
1096 name: "USD Coin".to_string(),
1097 decimals: 6,
1098 };
1099
1100 let json = serde_json::to_string(&token).unwrap();
1101 assert!(json.contains("USDC"));
1102 assert!(json.contains("USD Coin"));
1103 assert!(json.contains("\"decimals\":6"));
1104
1105 let deserialized: Token = serde_json::from_str(&json).unwrap();
1106 assert_eq!(deserialized.decimals, 6);
1107 }
1108
1109 #[test]
1110 fn test_token_balance_serialization() {
1111 let token_balance = TokenBalance {
1112 token: Token {
1113 contract_address: "0xtoken".to_string(),
1114 symbol: "USDC".to_string(),
1115 name: "USD Coin".to_string(),
1116 decimals: 6,
1117 },
1118 balance: "1000000".to_string(),
1119 formatted_balance: "1.0".to_string(),
1120 usd_value: Some(1.0),
1121 };
1122
1123 let json = serde_json::to_string(&token_balance).unwrap();
1124 assert!(json.contains("USDC"));
1125 assert!(json.contains("1000000"));
1126 assert!(json.contains("1.0"));
1127 }
1128
1129 #[test]
1130 fn test_balance_debug() {
1131 let balance = Balance {
1132 raw: "1000".to_string(),
1133 formatted: "0.001".to_string(),
1134 decimals: 18,
1135 symbol: "ETH".to_string(),
1136 usd_value: None,
1137 };
1138
1139 let debug_str = format!("{:?}", balance);
1140 assert!(debug_str.contains("Balance"));
1141 assert!(debug_str.contains("1000"));
1142 }
1143
1144 #[test]
1145 fn test_transaction_debug() {
1146 let tx = Transaction {
1147 hash: "0xtest".to_string(),
1148 block_number: Some(1),
1149 timestamp: Some(0),
1150 from: "0x1".to_string(),
1151 to: Some("0x2".to_string()),
1152 value: "0".to_string(),
1153 gas_limit: 21000,
1154 gas_used: Some(21000),
1155 gas_price: "0".to_string(),
1156 nonce: 0,
1157 input: "0x".to_string(),
1158 status: Some(true),
1159 };
1160
1161 let debug_str = format!("{:?}", tx);
1162 assert!(debug_str.contains("Transaction"));
1163 assert!(debug_str.contains("0xtest"));
1164 }
1165
1166 #[test]
1171 fn test_infer_chain_from_address_evm() {
1172 assert_eq!(
1174 super::infer_chain_from_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2"),
1175 Some("ethereum")
1176 );
1177 assert_eq!(
1178 super::infer_chain_from_address("0x0000000000000000000000000000000000000000"),
1179 Some("ethereum")
1180 );
1181 assert_eq!(
1182 super::infer_chain_from_address("0xABCDEF1234567890abcdef1234567890ABCDEF12"),
1183 Some("ethereum")
1184 );
1185 }
1186
1187 #[test]
1188 fn test_infer_chain_from_address_tron() {
1189 assert_eq!(
1191 super::infer_chain_from_address("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf"),
1192 Some("tron")
1193 );
1194 assert_eq!(
1195 super::infer_chain_from_address("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"),
1196 Some("tron")
1197 );
1198 }
1199
1200 #[test]
1201 fn test_infer_chain_from_address_solana() {
1202 assert_eq!(
1204 super::infer_chain_from_address("DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy"),
1205 Some("solana")
1206 );
1207 assert_eq!(
1209 super::infer_chain_from_address("11111111111111111111111111111111"),
1210 Some("solana")
1211 );
1212 }
1213
1214 #[test]
1215 fn test_infer_chain_from_address_invalid() {
1216 assert_eq!(super::infer_chain_from_address("0x123"), None);
1218 assert_eq!(super::infer_chain_from_address("not_an_address"), None);
1220 assert_eq!(super::infer_chain_from_address(""), None);
1222 assert_eq!(super::infer_chain_from_address("0x123456"), None);
1224 assert_eq!(
1226 super::infer_chain_from_address("ADqSquXBgUCLYvYC4XZgrprLK589dkhSCf"),
1227 None
1228 );
1229 }
1230
1231 #[test]
1232 fn test_infer_chain_from_hash_evm() {
1233 assert_eq!(
1235 super::infer_chain_from_hash(
1236 "0xabc123def456789012345678901234567890123456789012345678901234abcd"
1237 ),
1238 Some("ethereum")
1239 );
1240 assert_eq!(
1241 super::infer_chain_from_hash(
1242 "0x0000000000000000000000000000000000000000000000000000000000000000"
1243 ),
1244 Some("ethereum")
1245 );
1246 }
1247
1248 #[test]
1249 fn test_infer_chain_from_hash_tron() {
1250 assert_eq!(
1252 super::infer_chain_from_hash(
1253 "abc123def456789012345678901234567890123456789012345678901234abcd"
1254 ),
1255 Some("tron")
1256 );
1257 assert_eq!(
1258 super::infer_chain_from_hash(
1259 "0000000000000000000000000000000000000000000000000000000000000000"
1260 ),
1261 Some("tron")
1262 );
1263 }
1264
1265 #[test]
1266 fn test_infer_chain_from_hash_solana() {
1267 let solana_sig = "5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW";
1270 assert_eq!(super::infer_chain_from_hash(solana_sig), Some("solana"));
1271 }
1272
1273 #[test]
1274 fn test_infer_chain_from_hash_invalid() {
1275 assert_eq!(super::infer_chain_from_hash("0x123"), None);
1277 assert_eq!(super::infer_chain_from_hash("not_a_hash"), None);
1279 assert_eq!(super::infer_chain_from_hash(""), None);
1281 assert_eq!(
1283 super::infer_chain_from_hash(
1284 "abc123gef456789012345678901234567890123456789012345678901234abcd"
1285 ),
1286 None
1287 );
1288 }
1289
1290 #[test]
1295 fn test_default_client_factory_create_dex_client() {
1296 let config = crate::config::ChainsConfig::default();
1297 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1298 let factory = DefaultClientFactory {
1299 chains_config: config,
1300 http,
1301 };
1302 let dex = factory.create_dex_client();
1303 let _ = format!("{:?}", std::mem::size_of_val(&dex));
1305 }
1306
1307 #[test]
1308 fn test_default_client_factory_create_ethereum_client() {
1309 let config = crate::config::ChainsConfig::default();
1310 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1311 let factory = DefaultClientFactory {
1312 chains_config: config,
1313 http,
1314 };
1315 let client = factory.create_chain_client("ethereum");
1317 assert!(client.is_ok());
1318 assert_eq!(client.unwrap().chain_name(), "ethereum");
1319 }
1320
1321 #[test]
1322 fn test_default_client_factory_create_polygon_client() {
1323 let config = crate::config::ChainsConfig::default();
1324 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1325 let factory = DefaultClientFactory {
1326 chains_config: config,
1327 http,
1328 };
1329 let client = factory.create_chain_client("polygon");
1330 assert!(client.is_ok());
1331 assert_eq!(client.unwrap().chain_name(), "polygon");
1332 }
1333
1334 #[test]
1335 fn test_default_client_factory_create_solana_client() {
1336 let config = crate::config::ChainsConfig::default();
1337 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1338 let factory = DefaultClientFactory {
1339 chains_config: config,
1340 http,
1341 };
1342 let client = factory.create_chain_client("solana");
1343 assert!(client.is_ok());
1344 assert_eq!(client.unwrap().chain_name(), "solana");
1345 }
1346
1347 #[test]
1348 fn test_default_client_factory_create_sol_alias() {
1349 let config = crate::config::ChainsConfig::default();
1350 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1351 let factory = DefaultClientFactory {
1352 chains_config: config,
1353 http,
1354 };
1355 let client = factory.create_chain_client("sol");
1356 assert!(client.is_ok());
1357 assert_eq!(client.unwrap().chain_name(), "solana");
1358 }
1359
1360 #[test]
1361 fn test_default_client_factory_create_tron_client() {
1362 let config = crate::config::ChainsConfig::default();
1363 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1364 let factory = DefaultClientFactory {
1365 chains_config: config,
1366 http,
1367 };
1368 let client = factory.create_chain_client("tron");
1369 assert!(client.is_ok());
1370 assert_eq!(client.unwrap().chain_name(), "tron");
1371 }
1372
1373 #[test]
1374 fn test_default_client_factory_create_trx_alias() {
1375 let config = crate::config::ChainsConfig::default();
1376 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1377 let factory = DefaultClientFactory {
1378 chains_config: config,
1379 http,
1380 };
1381 let client = factory.create_chain_client("trx");
1382 assert!(client.is_ok());
1383 assert_eq!(client.unwrap().chain_name(), "tron");
1384 }
1385
1386 #[test]
1387 fn test_default_client_factory_create_arbitrum_client() {
1388 let config = crate::config::ChainsConfig::default();
1389 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1390 let factory = DefaultClientFactory {
1391 chains_config: config,
1392 http,
1393 };
1394 let client = factory.create_chain_client("arbitrum");
1395 assert!(client.is_ok());
1396 assert_eq!(client.unwrap().chain_name(), "arbitrum");
1397 }
1398
1399 #[test]
1400 fn test_default_client_factory_create_optimism_client() {
1401 let config = crate::config::ChainsConfig::default();
1402 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1403 let factory = DefaultClientFactory {
1404 chains_config: config,
1405 http,
1406 };
1407 let client = factory.create_chain_client("optimism");
1408 assert!(client.is_ok());
1409 assert_eq!(client.unwrap().chain_name(), "optimism");
1410 }
1411
1412 #[test]
1413 fn test_default_client_factory_create_base_client() {
1414 let config = crate::config::ChainsConfig::default();
1415 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1416 let factory = DefaultClientFactory {
1417 chains_config: config,
1418 http,
1419 };
1420 let client = factory.create_chain_client("base");
1421 assert!(client.is_ok());
1422 assert_eq!(client.unwrap().chain_name(), "base");
1423 }
1424
1425 #[test]
1426 fn test_default_client_factory_create_unsupported_chain_returns_err() {
1427 let config = crate::config::ChainsConfig::default();
1428 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1429 let factory = DefaultClientFactory {
1430 chains_config: config,
1431 http,
1432 };
1433 let client = factory.create_chain_client("avalanche");
1434 match &client {
1435 Err(e) => assert!(e.to_string().contains("Unsupported")),
1436 Ok(_) => panic!("expected Err for unsupported chain"),
1437 }
1438 }
1439
1440 #[test]
1445 fn test_solana_client_new_with_http() {
1446 let config = crate::config::ChainsConfig::default();
1447 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1448 let client = SolanaClient::new_with_http(&config, http);
1449 assert!(client.is_ok());
1450 assert_eq!(client.unwrap().chain_name(), "solana");
1451 }
1452
1453 #[test]
1454 fn test_tron_client_new_with_http() {
1455 let config = crate::config::ChainsConfig::default();
1456 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1457 let client = TronClient::new_with_http(&config, http);
1458 assert!(client.is_ok());
1459 assert_eq!(client.unwrap().chain_name(), "tron");
1460 }
1461
1462 #[test]
1463 fn test_dex_client_new_with_http() {
1464 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1465 let client = DexClient::new_with_http(http);
1466 let _ = format!("{}", std::mem::size_of_val(&client));
1467 }
1468
1469 #[test]
1470 fn test_factory_shares_http_transport() {
1471 let config = crate::config::ChainsConfig::default();
1472 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1473 let weak = Arc::downgrade(&http);
1474 let factory = DefaultClientFactory {
1475 chains_config: config,
1476 http,
1477 };
1478 let _eth = factory.create_chain_client("ethereum").unwrap();
1483 let _sol = factory.create_chain_client("solana").unwrap();
1484 let _trx = factory.create_chain_client("tron").unwrap();
1485 let _dex = factory.create_dex_client();
1486 assert!(weak.upgrade().is_some());
1488 }
1489
1490 #[test]
1491 fn test_ethereum_client_new_with_http() {
1492 let config = crate::config::ChainsConfig::default();
1493 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1494 let client = EthereumClient::new_with_http(&config, http);
1495 assert!(client.is_ok());
1496 assert_eq!(client.unwrap().chain_name(), "ethereum");
1497 }
1498
1499 #[test]
1500 fn test_ethereum_client_for_chain_with_http() {
1501 let config = crate::config::ChainsConfig::default();
1502 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1503 let client = EthereumClient::for_chain_with_http("polygon", &config, http);
1504 assert!(client.is_ok());
1505 assert_eq!(client.unwrap().chain_name(), "polygon");
1506 }
1507
1508 #[tokio::test]
1513 async fn test_chain_client_default_get_token_info() {
1514 use super::mocks::MockChainClient;
1515 let client = MockChainClient::new("ethereum", "ETH");
1517 let result = client.get_token_info("0xsometoken").await;
1518 assert!(result.is_err());
1519 }
1520
1521 #[tokio::test]
1522 async fn test_chain_client_default_get_token_holders() {
1523 use super::mocks::MockChainClient;
1524 let client = MockChainClient::new("ethereum", "ETH");
1525 let holders = client.get_token_holders("0xsometoken", 10).await.unwrap();
1526 assert!(holders.is_empty());
1527 }
1528
1529 #[tokio::test]
1530 async fn test_chain_client_default_get_token_holder_count() {
1531 use super::mocks::MockChainClient;
1532 let client = MockChainClient::new("ethereum", "ETH");
1533 let count = client.get_token_holder_count("0xsometoken").await.unwrap();
1534 assert_eq!(count, 0);
1535 }
1536
1537 #[tokio::test]
1538 async fn test_mock_client_factory_creates_chain_client() {
1539 use super::mocks::MockClientFactory;
1540 let factory = MockClientFactory::new();
1541 let client = factory.create_chain_client("anything").unwrap();
1542 assert_eq!(client.chain_name(), "ethereum"); }
1544
1545 #[tokio::test]
1546 async fn test_mock_client_factory_creates_dex_client() {
1547 use super::mocks::MockClientFactory;
1548 let factory = MockClientFactory::new();
1549 let dex = factory.create_dex_client();
1550 let price = dex.get_token_price("ethereum", "0xtest").await;
1551 assert_eq!(price, Some(1.0));
1552 }
1553
1554 #[tokio::test]
1555 async fn test_mock_chain_client_balance() {
1556 use super::mocks::MockChainClient;
1557 let client = MockChainClient::new("ethereum", "ETH");
1558 let balance = client.get_balance("0xtest").await.unwrap();
1559 assert_eq!(balance.formatted, "1.0");
1560 assert_eq!(balance.symbol, "ETH");
1561 assert_eq!(balance.usd_value, Some(2500.0));
1562 }
1563
1564 #[tokio::test]
1565 async fn test_mock_chain_client_transaction() {
1566 use super::mocks::MockChainClient;
1567 let client = MockChainClient::new("ethereum", "ETH");
1568 let tx = client.get_transaction("0xanyhash").await.unwrap();
1569 assert_eq!(tx.hash, "0xmocktx");
1570 assert_eq!(tx.nonce, 42);
1571 }
1572
1573 #[tokio::test]
1574 async fn test_mock_chain_client_block_number() {
1575 use super::mocks::MockChainClient;
1576 let client = MockChainClient::new("ethereum", "ETH");
1577 let block = client.get_block_number().await.unwrap();
1578 assert_eq!(block, 12345678);
1579 }
1580
1581 #[tokio::test]
1582 async fn test_mock_dex_source_data() {
1583 use super::mocks::MockDexSource;
1584 let dex = MockDexSource::new();
1585 let data = dex.get_token_data("ethereum", "0xtest").await.unwrap();
1586 assert_eq!(data.symbol, "MOCK");
1587 assert_eq!(data.price_usd, 1.0);
1588 }
1589
1590 #[tokio::test]
1591 async fn test_mock_dex_source_search() {
1592 use super::mocks::MockDexSource;
1593 let dex = MockDexSource::new();
1594 let results = dex.search_tokens("test", None).await.unwrap();
1595 assert!(results.is_empty());
1596 }
1597
1598 #[tokio::test]
1599 async fn test_mock_dex_source_native_price() {
1600 use super::mocks::MockDexSource;
1601 let dex = MockDexSource::new();
1602 let price = dex.get_native_token_price("ethereum").await;
1603 assert_eq!(price, Some(2500.0));
1604 }
1605
1606 struct MinimalChainClient;
1612
1613 #[async_trait::async_trait]
1614 impl ChainClient for MinimalChainClient {
1615 fn chain_name(&self) -> &str {
1616 "test"
1617 }
1618
1619 fn native_token_symbol(&self) -> &str {
1620 "TEST"
1621 }
1622
1623 async fn get_balance(&self, _address: &str) -> Result<Balance> {
1624 Ok(Balance {
1625 raw: "0".to_string(),
1626 formatted: "0".to_string(),
1627 decimals: 18,
1628 symbol: "TEST".to_string(),
1629 usd_value: None,
1630 })
1631 }
1632
1633 async fn get_transaction(&self, _hash: &str) -> Result<Transaction> {
1634 unimplemented!()
1635 }
1636
1637 async fn get_transactions(&self, _address: &str, _limit: u32) -> Result<Vec<Transaction>> {
1638 Ok(Vec::new())
1639 }
1640
1641 async fn get_block_number(&self) -> Result<u64> {
1642 Ok(0)
1643 }
1644
1645 async fn get_token_balances(&self, _address: &str) -> Result<Vec<TokenBalance>> {
1646 Ok(Vec::new())
1647 }
1648
1649 async fn enrich_balance_usd(&self, _balance: &mut Balance) {}
1650 }
1651
1652 #[tokio::test]
1653 async fn test_default_get_token_info() {
1654 let client = MinimalChainClient;
1655 let result = client.get_token_info("0xtest").await;
1656 assert!(result.is_err());
1657 assert!(result.unwrap_err().to_string().contains("not supported"));
1658 }
1659
1660 #[tokio::test]
1661 async fn test_default_get_token_holders() {
1662 let client = MinimalChainClient;
1663 let holders = client.get_token_holders("0xtest", 10).await.unwrap();
1664 assert!(holders.is_empty());
1665 }
1666
1667 #[tokio::test]
1668 async fn test_default_get_token_holder_count() {
1669 let client = MinimalChainClient;
1670 let count = client.get_token_holder_count("0xtest").await.unwrap();
1671 assert_eq!(count, 0);
1672 }
1673
1674 #[test]
1679 fn test_chain_metadata_ethereum() {
1680 let meta = chain_metadata("ethereum").unwrap();
1681 assert_eq!(meta.chain_id, "ethereum");
1682 assert_eq!(meta.native_symbol, "ETH");
1683 assert_eq!(meta.native_decimals, 18);
1684 assert_eq!(meta.explorer_token_base, "https://etherscan.io/token");
1685 }
1686
1687 #[test]
1688 fn test_chain_metadata_ethereum_alias() {
1689 let meta = chain_metadata("eth").unwrap();
1690 assert_eq!(meta.chain_id, "ethereum");
1691 assert_eq!(meta.native_symbol, "ETH");
1692 }
1693
1694 #[test]
1695 fn test_chain_metadata_polygon() {
1696 let meta = chain_metadata("polygon").unwrap();
1697 assert_eq!(meta.chain_id, "polygon");
1698 assert_eq!(meta.native_symbol, "MATIC");
1699 assert_eq!(meta.native_decimals, 18);
1700 assert_eq!(meta.explorer_token_base, "https://polygonscan.com/token");
1701 }
1702
1703 #[test]
1704 fn test_chain_metadata_bsc() {
1705 let meta = chain_metadata("bsc").unwrap();
1706 assert_eq!(meta.chain_id, "bsc");
1707 assert_eq!(meta.native_symbol, "BNB");
1708 assert_eq!(meta.native_decimals, 18);
1709 assert_eq!(meta.explorer_token_base, "https://bscscan.com/token");
1710 }
1711
1712 #[test]
1713 fn test_chain_metadata_solana() {
1714 let meta = chain_metadata("solana").unwrap();
1715 assert_eq!(meta.chain_id, "solana");
1716 assert_eq!(meta.native_symbol, "SOL");
1717 assert_eq!(meta.native_decimals, 9);
1718 assert_eq!(meta.explorer_token_base, "https://solscan.io/token");
1719 }
1720
1721 #[test]
1722 fn test_chain_metadata_solana_alias() {
1723 let meta = chain_metadata("sol").unwrap();
1724 assert_eq!(meta.chain_id, "solana");
1725 assert_eq!(meta.native_symbol, "SOL");
1726 }
1727
1728 #[test]
1729 fn test_chain_metadata_tron() {
1730 let meta = chain_metadata("tron").unwrap();
1731 assert_eq!(meta.chain_id, "tron");
1732 assert_eq!(meta.native_symbol, "TRX");
1733 assert_eq!(meta.native_decimals, 6);
1734 assert_eq!(meta.explorer_token_base, "https://tronscan.org/#/token20");
1735 }
1736
1737 #[test]
1738 fn test_chain_metadata_tron_alias() {
1739 let meta = chain_metadata("trx").unwrap();
1740 assert_eq!(meta.chain_id, "tron");
1741 assert_eq!(meta.native_symbol, "TRX");
1742 }
1743
1744 #[test]
1745 fn test_chain_metadata_arbitrum() {
1746 let meta = chain_metadata("arbitrum").unwrap();
1747 assert_eq!(meta.chain_id, "arbitrum");
1748 assert_eq!(meta.native_symbol, "ETH");
1749 assert_eq!(meta.native_decimals, 18);
1750 assert_eq!(meta.explorer_token_base, "https://arbiscan.io/token");
1751 }
1752
1753 #[test]
1754 fn test_chain_metadata_optimism() {
1755 let meta = chain_metadata("optimism").unwrap();
1756 assert_eq!(meta.chain_id, "optimism");
1757 assert_eq!(meta.native_symbol, "ETH");
1758 assert_eq!(meta.native_decimals, 18);
1759 assert_eq!(
1760 meta.explorer_token_base,
1761 "https://optimistic.etherscan.io/token"
1762 );
1763 }
1764
1765 #[test]
1766 fn test_chain_metadata_base() {
1767 let meta = chain_metadata("base").unwrap();
1768 assert_eq!(meta.chain_id, "base");
1769 assert_eq!(meta.native_symbol, "ETH");
1770 assert_eq!(meta.native_decimals, 18);
1771 assert_eq!(meta.explorer_token_base, "https://basescan.org/token");
1772 }
1773
1774 #[test]
1775 fn test_chain_metadata_case_insensitive() {
1776 let meta1 = chain_metadata("ETHEREUM").unwrap();
1777 let meta2 = chain_metadata("Ethereum").unwrap();
1778 let meta3 = chain_metadata("ethereum").unwrap();
1779 assert_eq!(meta1.chain_id, meta2.chain_id);
1780 assert_eq!(meta2.chain_id, meta3.chain_id);
1781 }
1782
1783 #[test]
1784 fn test_chain_metadata_unknown() {
1785 assert!(chain_metadata("bitcoin").is_none());
1786 assert!(chain_metadata("litecoin").is_none());
1787 assert!(chain_metadata("unknown").is_none());
1788 assert!(chain_metadata("").is_none());
1789 }
1790
1791 #[test]
1792 fn test_native_symbol_ethereum() {
1793 assert_eq!(native_symbol("ethereum"), "ETH");
1794 assert_eq!(native_symbol("eth"), "ETH");
1795 }
1796
1797 #[test]
1798 fn test_native_symbol_polygon() {
1799 assert_eq!(native_symbol("polygon"), "MATIC");
1800 }
1801
1802 #[test]
1803 fn test_native_symbol_bsc() {
1804 assert_eq!(native_symbol("bsc"), "BNB");
1805 }
1806
1807 #[test]
1808 fn test_native_symbol_solana() {
1809 assert_eq!(native_symbol("solana"), "SOL");
1810 assert_eq!(native_symbol("sol"), "SOL");
1811 }
1812
1813 #[test]
1814 fn test_native_symbol_tron() {
1815 assert_eq!(native_symbol("tron"), "TRX");
1816 assert_eq!(native_symbol("trx"), "TRX");
1817 }
1818
1819 #[test]
1820 fn test_native_symbol_arbitrum() {
1821 assert_eq!(native_symbol("arbitrum"), "ETH");
1822 }
1823
1824 #[test]
1825 fn test_native_symbol_optimism() {
1826 assert_eq!(native_symbol("optimism"), "ETH");
1827 }
1828
1829 #[test]
1830 fn test_native_symbol_base() {
1831 assert_eq!(native_symbol("base"), "ETH");
1832 }
1833
1834 #[test]
1835 fn test_native_symbol_unknown() {
1836 assert_eq!(native_symbol("unknown"), "???");
1837 assert_eq!(native_symbol("bitcoin"), "???");
1838 assert_eq!(native_symbol(""), "???");
1839 }
1840
1841 #[test]
1842 fn test_native_symbol_case_insensitive() {
1843 assert_eq!(native_symbol("ETHEREUM"), "ETH");
1844 assert_eq!(native_symbol("Ethereum"), "ETH");
1845 assert_eq!(native_symbol("ethereum"), "ETH");
1846 }
1847
1848 #[tokio::test]
1849 async fn test_chain_client_default_get_code() {
1850 let client = MinimalChainClient;
1851 let result = client.get_code("0x1234").await;
1852 assert!(result.is_err());
1853 let err_msg = result.unwrap_err().to_string();
1854 assert!(err_msg.contains("not supported"));
1855 }
1856
1857 fn tx(hash: &str, gas_used: Option<u64>, input: &str, status: Option<bool>) -> Transaction {
1862 Transaction {
1863 hash: hash.to_string(),
1864 block_number: Some(1),
1865 timestamp: Some(1700000000),
1866 from: "0xfrom".to_string(),
1867 to: Some("0xto".to_string()),
1868 value: "0".to_string(),
1869 gas_limit: 21000,
1870 gas_used,
1871 gas_price: "20000000000".to_string(),
1872 nonce: 0,
1873 input: input.to_string(),
1874 status,
1875 }
1876 }
1877
1878 #[test]
1879 fn test_analyze_gas_usage_empty_transactions() {
1880 let txs: Vec<Transaction> = vec![];
1881 let result = super::analyze_gas_usage(&txs);
1882 assert_eq!(result.avg_gas_used, 0);
1883 assert_eq!(result.max_gas_used, 0);
1884 assert_eq!(result.min_gas_used, 0);
1885 assert_eq!(result.tx_count, 0);
1886 assert_eq!(result.failed_tx_count, 0);
1887 assert_eq!(result.wasted_gas, 0);
1888 assert!(result.gas_by_function.is_empty());
1889 }
1890
1891 #[test]
1892 fn test_analyze_gas_usage_single_tx() {
1893 let txs = vec![tx("0x1", Some(100_000), "0x", Some(true))];
1894 let result = super::analyze_gas_usage(&txs);
1895 assert_eq!(result.avg_gas_used, 100_000);
1896 assert_eq!(result.max_gas_used, 100_000);
1897 assert_eq!(result.min_gas_used, 100_000);
1898 assert_eq!(result.tx_count, 1);
1899 }
1900
1901 #[test]
1902 fn test_analyze_gas_usage_multiple_txs() {
1903 let txs = vec![
1904 tx("0x1", Some(50_000), "0xa9059cbb", Some(true)),
1905 tx("0x2", Some(150_000), "0xa9059cbb", Some(true)),
1906 tx("0x3", Some(100_000), "0xa9059cbb", Some(true)),
1907 ];
1908 let result = super::analyze_gas_usage(&txs);
1909 assert_eq!(result.avg_gas_used, 100_000); assert_eq!(result.max_gas_used, 150_000);
1911 assert_eq!(result.min_gas_used, 50_000);
1912 assert_eq!(result.tx_count, 3);
1913 }
1914
1915 #[test]
1916 fn test_analyze_gas_usage_failed_tx() {
1917 let txs = vec![
1918 tx("0x1", Some(80_000), "0x", Some(true)),
1919 tx("0x2", Some(120_000), "0x", Some(false)),
1920 ];
1921 let result = super::analyze_gas_usage(&txs);
1922 assert_eq!(result.failed_tx_count, 1);
1923 assert_eq!(result.wasted_gas, 120_000);
1924 }
1925
1926 #[test]
1927 fn test_analyze_gas_usage_gas_by_function() {
1928 let txs = vec![
1930 tx("0x1", Some(100_000), "0xa9059cbb0000", Some(true)),
1931 tx("0x2", Some(200_000), "0xa9059cbb0000", Some(true)),
1932 tx("0x3", Some(50_000), "0x095ea7b30000", Some(true)),
1933 ];
1934 let result = super::analyze_gas_usage(&txs);
1935 assert_eq!(result.gas_by_function.len(), 2);
1936 let by_sel: std::collections::HashMap<_, _> = result
1937 .gas_by_function
1938 .iter()
1939 .map(|g| (g.function.as_str(), g))
1940 .collect();
1941 let transfer = by_sel.get("0xa9059cbb").unwrap();
1942 assert_eq!(transfer.call_count, 2);
1943 assert_eq!(transfer.total_gas, 300_000);
1944 assert_eq!(transfer.avg_gas, 150_000);
1945 let approve = by_sel.get("0x095ea7b3").unwrap();
1946 assert_eq!(approve.call_count, 1);
1947 assert_eq!(approve.total_gas, 50_000);
1948 }
1949
1950 #[test]
1951 fn test_analyze_gas_usage_input_0x_transfer() {
1952 let txs = vec![tx("0x1", Some(21_000), "0x", Some(true))];
1953 let result = super::analyze_gas_usage(&txs);
1954 assert_eq!(result.gas_by_function.len(), 1);
1955 assert_eq!(result.gas_by_function[0].function, "transfer()");
1956 }
1957
1958 #[test]
1959 fn test_analyze_gas_usage_input_empty_transfer() {
1960 let txs = vec![tx("0x1", Some(21_000), "", Some(true))];
1961 let result = super::analyze_gas_usage(&txs);
1962 assert_eq!(result.gas_by_function.len(), 1);
1963 assert_eq!(result.gas_by_function[0].function, "transfer()");
1964 }
1965
1966 #[test]
1967 fn test_analyze_gas_usage_gas_used_none() {
1968 let txs = vec![tx("0x1", None, "0x", Some(true))];
1969 let result = super::analyze_gas_usage(&txs);
1970 assert_eq!(result.avg_gas_used, 0);
1971 assert_eq!(result.max_gas_used, 0);
1972 assert_eq!(result.min_gas_used, 0);
1973 }
1974
1975 #[test]
1976 fn test_analyze_gas_usage_short_input_uses_full_input_as_selector() {
1977 let txs = vec![tx("0x1", Some(50_000), "0x1234567", Some(true))];
1978 let result = super::analyze_gas_usage(&txs);
1979 assert_eq!(result.gas_by_function.len(), 1);
1980 assert_eq!(result.gas_by_function[0].function, "0x1234567");
1981 }
1982}
1983
1984#[cfg(test)]
1993pub mod mocks {
1994 use super::*;
1995 use crate::chains::dex::{DexDataSource, DexTokenData, TokenSearchResult};
1996 use async_trait::async_trait;
1997
1998 #[derive(Debug, Clone)]
2000 pub struct MockChainClient {
2001 pub chain: String,
2002 pub symbol: String,
2003 pub balance: Balance,
2004 pub transaction: Transaction,
2005 pub transactions: Vec<Transaction>,
2006 pub token_balances: Vec<TokenBalance>,
2007 pub block_number: u64,
2008 pub token_info: Option<Token>,
2009 pub token_holders: Vec<TokenHolder>,
2010 pub token_holder_count: u64,
2011 }
2012
2013 impl MockChainClient {
2014 pub fn new(chain: &str, symbol: &str) -> Self {
2016 Self {
2017 chain: chain.to_string(),
2018 symbol: symbol.to_string(),
2019 balance: Balance {
2020 raw: "1000000000000000000".to_string(),
2021 formatted: "1.0".to_string(),
2022 decimals: 18,
2023 symbol: symbol.to_string(),
2024 usd_value: Some(2500.0),
2025 },
2026 transaction: Transaction {
2027 hash: "0xmocktx".to_string(),
2028 block_number: Some(12345678),
2029 timestamp: Some(1700000000),
2030 from: "0xfrom".to_string(),
2031 to: Some("0xto".to_string()),
2032 value: "1.0".to_string(),
2033 gas_limit: 21000,
2034 gas_used: Some(21000),
2035 gas_price: "20000000000".to_string(),
2036 nonce: 42,
2037 input: "0x".to_string(),
2038 status: Some(true),
2039 },
2040 transactions: vec![],
2041 token_balances: vec![],
2042 block_number: 12345678,
2043 token_info: None,
2044 token_holders: vec![],
2045 token_holder_count: 0,
2046 }
2047 }
2048 }
2049
2050 #[async_trait]
2051 impl ChainClient for MockChainClient {
2052 fn chain_name(&self) -> &str {
2053 &self.chain
2054 }
2055
2056 fn native_token_symbol(&self) -> &str {
2057 &self.symbol
2058 }
2059
2060 async fn get_balance(&self, _address: &str) -> Result<Balance> {
2061 Ok(self.balance.clone())
2062 }
2063
2064 async fn enrich_balance_usd(&self, _balance: &mut Balance) {
2065 }
2067
2068 async fn get_transaction(&self, _hash: &str) -> Result<Transaction> {
2069 Ok(self.transaction.clone())
2070 }
2071
2072 async fn get_transactions(&self, _address: &str, _limit: u32) -> Result<Vec<Transaction>> {
2073 Ok(self.transactions.clone())
2074 }
2075
2076 async fn get_block_number(&self) -> Result<u64> {
2077 Ok(self.block_number)
2078 }
2079
2080 async fn get_token_balances(&self, _address: &str) -> Result<Vec<TokenBalance>> {
2081 Ok(self.token_balances.clone())
2082 }
2083
2084 async fn get_token_info(&self, _address: &str) -> Result<Token> {
2085 match &self.token_info {
2086 Some(t) => Ok(t.clone()),
2087 None => Err(crate::error::ScopeError::Chain(
2088 "Token info not available".to_string(),
2089 )),
2090 }
2091 }
2092
2093 async fn get_token_holders(&self, _address: &str, _limit: u32) -> Result<Vec<TokenHolder>> {
2094 Ok(self.token_holders.clone())
2095 }
2096
2097 async fn get_token_holder_count(&self, _address: &str) -> Result<u64> {
2098 Ok(self.token_holder_count)
2099 }
2100 }
2101
2102 #[derive(Debug, Clone)]
2104 pub struct MockDexSource {
2105 pub token_price: Option<f64>,
2106 pub native_price: Option<f64>,
2107 pub token_data: Option<DexTokenData>,
2108 pub search_results: Vec<TokenSearchResult>,
2109 }
2110
2111 impl Default for MockDexSource {
2112 fn default() -> Self {
2113 Self::new()
2114 }
2115 }
2116
2117 impl MockDexSource {
2118 pub fn new() -> Self {
2120 Self {
2121 token_price: Some(1.0),
2122 native_price: Some(2500.0),
2123 token_data: Some(DexTokenData {
2124 address: "0xmocktoken".to_string(),
2125 symbol: "MOCK".to_string(),
2126 name: "Mock Token".to_string(),
2127 price_usd: 1.0,
2128 price_change_24h: 5.0,
2129 price_change_6h: 2.0,
2130 price_change_1h: 0.5,
2131 price_change_5m: 0.1,
2132 volume_24h: 1_000_000.0,
2133 volume_6h: 250_000.0,
2134 volume_1h: 50_000.0,
2135 liquidity_usd: 5_000_000.0,
2136 market_cap: Some(100_000_000.0),
2137 fdv: Some(200_000_000.0),
2138 pairs: vec![],
2139 price_history: vec![],
2140 volume_history: vec![],
2141 total_buys_24h: 500,
2142 total_sells_24h: 450,
2143 total_buys_6h: 120,
2144 total_sells_6h: 110,
2145 total_buys_1h: 20,
2146 total_sells_1h: 18,
2147 earliest_pair_created_at: Some(1690000000),
2148 image_url: None,
2149 websites: vec![],
2150 socials: vec![],
2151 dexscreener_url: None,
2152 }),
2153 search_results: vec![],
2154 }
2155 }
2156 }
2157
2158 #[async_trait]
2159 impl DexDataSource for MockDexSource {
2160 async fn get_token_price(&self, _chain: &str, _address: &str) -> Option<f64> {
2161 self.token_price
2162 }
2163
2164 async fn get_native_token_price(&self, _chain: &str) -> Option<f64> {
2165 self.native_price
2166 }
2167
2168 async fn get_token_data(&self, _chain: &str, _address: &str) -> Result<DexTokenData> {
2169 match &self.token_data {
2170 Some(data) => Ok(data.clone()),
2171 None => Err(crate::error::ScopeError::NotFound(
2172 "No DEX data found".to_string(),
2173 )),
2174 }
2175 }
2176
2177 async fn search_tokens(
2178 &self,
2179 _query: &str,
2180 _chain: Option<&str>,
2181 ) -> Result<Vec<TokenSearchResult>> {
2182 Ok(self.search_results.clone())
2183 }
2184 }
2185
2186 pub struct MockClientFactory {
2188 pub mock_client: MockChainClient,
2189 pub mock_dex: MockDexSource,
2190 }
2191
2192 impl Default for MockClientFactory {
2193 fn default() -> Self {
2194 Self::new()
2195 }
2196 }
2197
2198 impl MockClientFactory {
2199 pub fn new() -> Self {
2201 Self {
2202 mock_client: MockChainClient::new("ethereum", "ETH"),
2203 mock_dex: MockDexSource::new(),
2204 }
2205 }
2206 }
2207
2208 impl ChainClientFactory for MockClientFactory {
2209 fn create_chain_client(&self, _chain: &str) -> Result<Box<dyn ChainClient>> {
2210 Ok(Box::new(self.mock_client.clone()))
2211 }
2212
2213 fn create_dex_client(&self) -> Box<dyn DexDataSource> {
2214 Box::new(self.mock_dex.clone())
2215 }
2216 }
2217}