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 = total_gas.checked_div(tx_count).unwrap_or(0);
932
933 if min_gas == u64::MAX {
934 min_gas = 0;
935 }
936
937 let mut gas_by_function: Vec<GasByFunction> = function_gas
939 .into_iter()
940 .map(|(function, (call_count, total_gas_fn))| GasByFunction {
941 function,
942 call_count,
943 avg_gas: total_gas_fn.checked_div(call_count).unwrap_or(0),
944 total_gas: total_gas_fn,
945 })
946 .collect();
947 gas_by_function.sort_by_key(|g| std::cmp::Reverse(g.total_gas));
948
949 let total_gas_cost_formatted = format!("{} gas units", total_gas);
951
952 GasAnalysis {
953 avg_gas_used: avg_gas,
954 max_gas_used: max_gas,
955 min_gas_used: min_gas,
956 total_gas_cost_wei: total_gas.to_string(),
957 total_gas_cost_formatted,
958 tx_count,
959 gas_by_function,
960 failed_tx_count: failed_count,
961 wasted_gas,
962 }
963}
964
965#[cfg(test)]
970mod tests {
971 use super::*;
972
973 #[test]
974 fn test_balance_serialization() {
975 let balance = Balance {
976 raw: "1000000000000000000".to_string(),
977 formatted: "1.0".to_string(),
978 decimals: 18,
979 symbol: "ETH".to_string(),
980 usd_value: Some(3500.0),
981 };
982
983 let json = serde_json::to_string(&balance).unwrap();
984 assert!(json.contains("1000000000000000000"));
985 assert!(json.contains("1.0"));
986 assert!(json.contains("ETH"));
987 assert!(json.contains("3500"));
988
989 let deserialized: Balance = serde_json::from_str(&json).unwrap();
990 assert_eq!(deserialized.raw, balance.raw);
991 assert_eq!(deserialized.decimals, 18);
992 }
993
994 #[test]
995 fn test_balance_without_usd() {
996 let balance = Balance {
997 raw: "1000000000000000000".to_string(),
998 formatted: "1.0".to_string(),
999 decimals: 18,
1000 symbol: "ETH".to_string(),
1001 usd_value: None,
1002 };
1003
1004 let json = serde_json::to_string(&balance).unwrap();
1005 assert!(!json.contains("usd_value"));
1006 }
1007
1008 #[test]
1009 fn test_transaction_serialization() {
1010 let tx = Transaction {
1011 hash: "0xabc123".to_string(),
1012 block_number: Some(12345678),
1013 timestamp: Some(1700000000),
1014 from: "0xfrom".to_string(),
1015 to: Some("0xto".to_string()),
1016 value: "1.0".to_string(),
1017 gas_limit: 21000,
1018 gas_used: Some(21000),
1019 gas_price: "20000000000".to_string(),
1020 nonce: 42,
1021 input: "0x".to_string(),
1022 status: Some(true),
1023 };
1024
1025 let json = serde_json::to_string(&tx).unwrap();
1026 assert!(json.contains("0xabc123"));
1027 assert!(json.contains("12345678"));
1028 assert!(json.contains("0xfrom"));
1029 assert!(json.contains("0xto"));
1030
1031 let deserialized: Transaction = serde_json::from_str(&json).unwrap();
1032 assert_eq!(deserialized.hash, tx.hash);
1033 assert_eq!(deserialized.nonce, 42);
1034 }
1035
1036 #[test]
1037 fn test_pending_transaction_serialization() {
1038 let tx = Transaction {
1039 hash: "0xpending".to_string(),
1040 block_number: None,
1041 timestamp: None,
1042 from: "0xfrom".to_string(),
1043 to: Some("0xto".to_string()),
1044 value: "1.0".to_string(),
1045 gas_limit: 21000,
1046 gas_used: None,
1047 gas_price: "20000000000".to_string(),
1048 nonce: 0,
1049 input: "0x".to_string(),
1050 status: None,
1051 };
1052
1053 let json = serde_json::to_string(&tx).unwrap();
1054 assert!(json.contains("0xpending"));
1055 assert!(json.contains("null")); let deserialized: Transaction = serde_json::from_str(&json).unwrap();
1058 assert!(deserialized.block_number.is_none());
1059 assert!(deserialized.status.is_none());
1060 }
1061
1062 #[test]
1063 fn test_contract_creation_transaction() {
1064 let tx = Transaction {
1065 hash: "0xcreate".to_string(),
1066 block_number: Some(100),
1067 timestamp: Some(1700000000),
1068 from: "0xdeployer".to_string(),
1069 to: None, value: "0".to_string(),
1071 gas_limit: 1000000,
1072 gas_used: Some(500000),
1073 gas_price: "20000000000".to_string(),
1074 nonce: 0,
1075 input: "0x608060...".to_string(),
1076 status: Some(true),
1077 };
1078
1079 let json = serde_json::to_string(&tx).unwrap();
1080 assert!(json.contains("\"to\":null"));
1081 }
1082
1083 #[test]
1084 fn test_token_serialization() {
1085 let token = Token {
1086 contract_address: "0xtoken".to_string(),
1087 symbol: "USDC".to_string(),
1088 name: "USD Coin".to_string(),
1089 decimals: 6,
1090 };
1091
1092 let json = serde_json::to_string(&token).unwrap();
1093 assert!(json.contains("USDC"));
1094 assert!(json.contains("USD Coin"));
1095 assert!(json.contains("\"decimals\":6"));
1096
1097 let deserialized: Token = serde_json::from_str(&json).unwrap();
1098 assert_eq!(deserialized.decimals, 6);
1099 }
1100
1101 #[test]
1102 fn test_token_balance_serialization() {
1103 let token_balance = TokenBalance {
1104 token: Token {
1105 contract_address: "0xtoken".to_string(),
1106 symbol: "USDC".to_string(),
1107 name: "USD Coin".to_string(),
1108 decimals: 6,
1109 },
1110 balance: "1000000".to_string(),
1111 formatted_balance: "1.0".to_string(),
1112 usd_value: Some(1.0),
1113 };
1114
1115 let json = serde_json::to_string(&token_balance).unwrap();
1116 assert!(json.contains("USDC"));
1117 assert!(json.contains("1000000"));
1118 assert!(json.contains("1.0"));
1119 }
1120
1121 #[test]
1122 fn test_balance_debug() {
1123 let balance = Balance {
1124 raw: "1000".to_string(),
1125 formatted: "0.001".to_string(),
1126 decimals: 18,
1127 symbol: "ETH".to_string(),
1128 usd_value: None,
1129 };
1130
1131 let debug_str = format!("{:?}", balance);
1132 assert!(debug_str.contains("Balance"));
1133 assert!(debug_str.contains("1000"));
1134 }
1135
1136 #[test]
1137 fn test_transaction_debug() {
1138 let tx = Transaction {
1139 hash: "0xtest".to_string(),
1140 block_number: Some(1),
1141 timestamp: Some(0),
1142 from: "0x1".to_string(),
1143 to: Some("0x2".to_string()),
1144 value: "0".to_string(),
1145 gas_limit: 21000,
1146 gas_used: Some(21000),
1147 gas_price: "0".to_string(),
1148 nonce: 0,
1149 input: "0x".to_string(),
1150 status: Some(true),
1151 };
1152
1153 let debug_str = format!("{:?}", tx);
1154 assert!(debug_str.contains("Transaction"));
1155 assert!(debug_str.contains("0xtest"));
1156 }
1157
1158 #[test]
1163 fn test_infer_chain_from_address_evm() {
1164 assert_eq!(
1166 super::infer_chain_from_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2"),
1167 Some("ethereum")
1168 );
1169 assert_eq!(
1170 super::infer_chain_from_address("0x0000000000000000000000000000000000000000"),
1171 Some("ethereum")
1172 );
1173 assert_eq!(
1174 super::infer_chain_from_address("0xABCDEF1234567890abcdef1234567890ABCDEF12"),
1175 Some("ethereum")
1176 );
1177 }
1178
1179 #[test]
1180 fn test_infer_chain_from_address_tron() {
1181 assert_eq!(
1183 super::infer_chain_from_address("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf"),
1184 Some("tron")
1185 );
1186 assert_eq!(
1187 super::infer_chain_from_address("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"),
1188 Some("tron")
1189 );
1190 }
1191
1192 #[test]
1193 fn test_infer_chain_from_address_solana() {
1194 assert_eq!(
1196 super::infer_chain_from_address("DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy"),
1197 Some("solana")
1198 );
1199 assert_eq!(
1201 super::infer_chain_from_address("11111111111111111111111111111111"),
1202 Some("solana")
1203 );
1204 }
1205
1206 #[test]
1207 fn test_infer_chain_from_address_invalid() {
1208 assert_eq!(super::infer_chain_from_address("0x123"), None);
1210 assert_eq!(super::infer_chain_from_address("not_an_address"), None);
1212 assert_eq!(super::infer_chain_from_address(""), None);
1214 assert_eq!(super::infer_chain_from_address("0x123456"), None);
1216 assert_eq!(
1218 super::infer_chain_from_address("ADqSquXBgUCLYvYC4XZgrprLK589dkhSCf"),
1219 None
1220 );
1221 }
1222
1223 #[test]
1224 fn test_infer_chain_from_hash_evm() {
1225 assert_eq!(
1227 super::infer_chain_from_hash(
1228 "0xabc123def456789012345678901234567890123456789012345678901234abcd"
1229 ),
1230 Some("ethereum")
1231 );
1232 assert_eq!(
1233 super::infer_chain_from_hash(
1234 "0x0000000000000000000000000000000000000000000000000000000000000000"
1235 ),
1236 Some("ethereum")
1237 );
1238 }
1239
1240 #[test]
1241 fn test_infer_chain_from_hash_tron() {
1242 assert_eq!(
1244 super::infer_chain_from_hash(
1245 "abc123def456789012345678901234567890123456789012345678901234abcd"
1246 ),
1247 Some("tron")
1248 );
1249 assert_eq!(
1250 super::infer_chain_from_hash(
1251 "0000000000000000000000000000000000000000000000000000000000000000"
1252 ),
1253 Some("tron")
1254 );
1255 }
1256
1257 #[test]
1258 fn test_infer_chain_from_hash_solana() {
1259 let solana_sig = "5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW";
1262 assert_eq!(super::infer_chain_from_hash(solana_sig), Some("solana"));
1263 }
1264
1265 #[test]
1266 fn test_infer_chain_from_hash_invalid() {
1267 assert_eq!(super::infer_chain_from_hash("0x123"), None);
1269 assert_eq!(super::infer_chain_from_hash("not_a_hash"), None);
1271 assert_eq!(super::infer_chain_from_hash(""), None);
1273 assert_eq!(
1275 super::infer_chain_from_hash(
1276 "abc123gef456789012345678901234567890123456789012345678901234abcd"
1277 ),
1278 None
1279 );
1280 }
1281
1282 #[test]
1287 fn test_default_client_factory_create_dex_client() {
1288 let config = crate::config::ChainsConfig::default();
1289 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1290 let factory = DefaultClientFactory {
1291 chains_config: config,
1292 http,
1293 };
1294 let dex = factory.create_dex_client();
1295 let _ = format!("{:?}", std::mem::size_of_val(&dex));
1297 }
1298
1299 #[test]
1300 fn test_default_client_factory_create_ethereum_client() {
1301 let config = crate::config::ChainsConfig::default();
1302 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1303 let factory = DefaultClientFactory {
1304 chains_config: config,
1305 http,
1306 };
1307 let client = factory.create_chain_client("ethereum");
1309 assert!(client.is_ok());
1310 assert_eq!(client.unwrap().chain_name(), "ethereum");
1311 }
1312
1313 #[test]
1314 fn test_default_client_factory_create_polygon_client() {
1315 let config = crate::config::ChainsConfig::default();
1316 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1317 let factory = DefaultClientFactory {
1318 chains_config: config,
1319 http,
1320 };
1321 let client = factory.create_chain_client("polygon");
1322 assert!(client.is_ok());
1323 assert_eq!(client.unwrap().chain_name(), "polygon");
1324 }
1325
1326 #[test]
1327 fn test_default_client_factory_create_solana_client() {
1328 let config = crate::config::ChainsConfig::default();
1329 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1330 let factory = DefaultClientFactory {
1331 chains_config: config,
1332 http,
1333 };
1334 let client = factory.create_chain_client("solana");
1335 assert!(client.is_ok());
1336 assert_eq!(client.unwrap().chain_name(), "solana");
1337 }
1338
1339 #[test]
1340 fn test_default_client_factory_create_sol_alias() {
1341 let config = crate::config::ChainsConfig::default();
1342 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1343 let factory = DefaultClientFactory {
1344 chains_config: config,
1345 http,
1346 };
1347 let client = factory.create_chain_client("sol");
1348 assert!(client.is_ok());
1349 assert_eq!(client.unwrap().chain_name(), "solana");
1350 }
1351
1352 #[test]
1353 fn test_default_client_factory_create_tron_client() {
1354 let config = crate::config::ChainsConfig::default();
1355 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1356 let factory = DefaultClientFactory {
1357 chains_config: config,
1358 http,
1359 };
1360 let client = factory.create_chain_client("tron");
1361 assert!(client.is_ok());
1362 assert_eq!(client.unwrap().chain_name(), "tron");
1363 }
1364
1365 #[test]
1366 fn test_default_client_factory_create_trx_alias() {
1367 let config = crate::config::ChainsConfig::default();
1368 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1369 let factory = DefaultClientFactory {
1370 chains_config: config,
1371 http,
1372 };
1373 let client = factory.create_chain_client("trx");
1374 assert!(client.is_ok());
1375 assert_eq!(client.unwrap().chain_name(), "tron");
1376 }
1377
1378 #[test]
1379 fn test_default_client_factory_create_arbitrum_client() {
1380 let config = crate::config::ChainsConfig::default();
1381 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1382 let factory = DefaultClientFactory {
1383 chains_config: config,
1384 http,
1385 };
1386 let client = factory.create_chain_client("arbitrum");
1387 assert!(client.is_ok());
1388 assert_eq!(client.unwrap().chain_name(), "arbitrum");
1389 }
1390
1391 #[test]
1392 fn test_default_client_factory_create_optimism_client() {
1393 let config = crate::config::ChainsConfig::default();
1394 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1395 let factory = DefaultClientFactory {
1396 chains_config: config,
1397 http,
1398 };
1399 let client = factory.create_chain_client("optimism");
1400 assert!(client.is_ok());
1401 assert_eq!(client.unwrap().chain_name(), "optimism");
1402 }
1403
1404 #[test]
1405 fn test_default_client_factory_create_base_client() {
1406 let config = crate::config::ChainsConfig::default();
1407 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1408 let factory = DefaultClientFactory {
1409 chains_config: config,
1410 http,
1411 };
1412 let client = factory.create_chain_client("base");
1413 assert!(client.is_ok());
1414 assert_eq!(client.unwrap().chain_name(), "base");
1415 }
1416
1417 #[test]
1418 fn test_default_client_factory_create_unsupported_chain_returns_err() {
1419 let config = crate::config::ChainsConfig::default();
1420 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1421 let factory = DefaultClientFactory {
1422 chains_config: config,
1423 http,
1424 };
1425 let client = factory.create_chain_client("avalanche");
1426 match &client {
1427 Err(e) => assert!(e.to_string().contains("Unsupported")),
1428 Ok(_) => panic!("expected Err for unsupported chain"),
1429 }
1430 }
1431
1432 #[test]
1437 fn test_solana_client_new_with_http() {
1438 let config = crate::config::ChainsConfig::default();
1439 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1440 let client = SolanaClient::new_with_http(&config, http);
1441 assert!(client.is_ok());
1442 assert_eq!(client.unwrap().chain_name(), "solana");
1443 }
1444
1445 #[test]
1446 fn test_tron_client_new_with_http() {
1447 let config = crate::config::ChainsConfig::default();
1448 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1449 let client = TronClient::new_with_http(&config, http);
1450 assert!(client.is_ok());
1451 assert_eq!(client.unwrap().chain_name(), "tron");
1452 }
1453
1454 #[test]
1455 fn test_dex_client_new_with_http() {
1456 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1457 let client = DexClient::new_with_http(http);
1458 let _ = format!("{}", std::mem::size_of_val(&client));
1459 }
1460
1461 #[test]
1462 fn test_factory_shares_http_transport() {
1463 let config = crate::config::ChainsConfig::default();
1464 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1465 let weak = Arc::downgrade(&http);
1466 let factory = DefaultClientFactory {
1467 chains_config: config,
1468 http,
1469 };
1470 let _eth = factory.create_chain_client("ethereum").unwrap();
1475 let _sol = factory.create_chain_client("solana").unwrap();
1476 let _trx = factory.create_chain_client("tron").unwrap();
1477 let _dex = factory.create_dex_client();
1478 assert!(weak.upgrade().is_some());
1480 }
1481
1482 #[test]
1483 fn test_ethereum_client_new_with_http() {
1484 let config = crate::config::ChainsConfig::default();
1485 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1486 let client = EthereumClient::new_with_http(&config, http);
1487 assert!(client.is_ok());
1488 assert_eq!(client.unwrap().chain_name(), "ethereum");
1489 }
1490
1491 #[test]
1492 fn test_ethereum_client_for_chain_with_http() {
1493 let config = crate::config::ChainsConfig::default();
1494 let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1495 let client = EthereumClient::for_chain_with_http("polygon", &config, http);
1496 assert!(client.is_ok());
1497 assert_eq!(client.unwrap().chain_name(), "polygon");
1498 }
1499
1500 #[tokio::test]
1505 async fn test_chain_client_default_get_token_info() {
1506 use super::mocks::MockChainClient;
1507 let client = MockChainClient::new("ethereum", "ETH");
1509 let result = client.get_token_info("0xsometoken").await;
1510 assert!(result.is_err());
1511 }
1512
1513 #[tokio::test]
1514 async fn test_chain_client_default_get_token_holders() {
1515 use super::mocks::MockChainClient;
1516 let client = MockChainClient::new("ethereum", "ETH");
1517 let holders = client.get_token_holders("0xsometoken", 10).await.unwrap();
1518 assert!(holders.is_empty());
1519 }
1520
1521 #[tokio::test]
1522 async fn test_chain_client_default_get_token_holder_count() {
1523 use super::mocks::MockChainClient;
1524 let client = MockChainClient::new("ethereum", "ETH");
1525 let count = client.get_token_holder_count("0xsometoken").await.unwrap();
1526 assert_eq!(count, 0);
1527 }
1528
1529 #[tokio::test]
1530 async fn test_mock_client_factory_creates_chain_client() {
1531 use super::mocks::MockClientFactory;
1532 let factory = MockClientFactory::new();
1533 let client = factory.create_chain_client("anything").unwrap();
1534 assert_eq!(client.chain_name(), "ethereum"); }
1536
1537 #[tokio::test]
1538 async fn test_mock_client_factory_creates_dex_client() {
1539 use super::mocks::MockClientFactory;
1540 let factory = MockClientFactory::new();
1541 let dex = factory.create_dex_client();
1542 let price = dex.get_token_price("ethereum", "0xtest").await;
1543 assert_eq!(price, Some(1.0));
1544 }
1545
1546 #[tokio::test]
1547 async fn test_mock_chain_client_balance() {
1548 use super::mocks::MockChainClient;
1549 let client = MockChainClient::new("ethereum", "ETH");
1550 let balance = client.get_balance("0xtest").await.unwrap();
1551 assert_eq!(balance.formatted, "1.0");
1552 assert_eq!(balance.symbol, "ETH");
1553 assert_eq!(balance.usd_value, Some(2500.0));
1554 }
1555
1556 #[tokio::test]
1557 async fn test_mock_chain_client_transaction() {
1558 use super::mocks::MockChainClient;
1559 let client = MockChainClient::new("ethereum", "ETH");
1560 let tx = client.get_transaction("0xanyhash").await.unwrap();
1561 assert_eq!(tx.hash, "0xmocktx");
1562 assert_eq!(tx.nonce, 42);
1563 }
1564
1565 #[tokio::test]
1566 async fn test_mock_chain_client_block_number() {
1567 use super::mocks::MockChainClient;
1568 let client = MockChainClient::new("ethereum", "ETH");
1569 let block = client.get_block_number().await.unwrap();
1570 assert_eq!(block, 12345678);
1571 }
1572
1573 #[tokio::test]
1574 async fn test_mock_dex_source_data() {
1575 use super::mocks::MockDexSource;
1576 let dex = MockDexSource::new();
1577 let data = dex.get_token_data("ethereum", "0xtest").await.unwrap();
1578 assert_eq!(data.symbol, "MOCK");
1579 assert_eq!(data.price_usd, 1.0);
1580 }
1581
1582 #[tokio::test]
1583 async fn test_mock_dex_source_search() {
1584 use super::mocks::MockDexSource;
1585 let dex = MockDexSource::new();
1586 let results = dex.search_tokens("test", None).await.unwrap();
1587 assert!(results.is_empty());
1588 }
1589
1590 #[tokio::test]
1591 async fn test_mock_dex_source_native_price() {
1592 use super::mocks::MockDexSource;
1593 let dex = MockDexSource::new();
1594 let price = dex.get_native_token_price("ethereum").await;
1595 assert_eq!(price, Some(2500.0));
1596 }
1597
1598 struct MinimalChainClient;
1604
1605 #[async_trait::async_trait]
1606 impl ChainClient for MinimalChainClient {
1607 fn chain_name(&self) -> &str {
1608 "test"
1609 }
1610
1611 fn native_token_symbol(&self) -> &str {
1612 "TEST"
1613 }
1614
1615 async fn get_balance(&self, _address: &str) -> Result<Balance> {
1616 Ok(Balance {
1617 raw: "0".to_string(),
1618 formatted: "0".to_string(),
1619 decimals: 18,
1620 symbol: "TEST".to_string(),
1621 usd_value: None,
1622 })
1623 }
1624
1625 async fn get_transaction(&self, _hash: &str) -> Result<Transaction> {
1626 unimplemented!()
1627 }
1628
1629 async fn get_transactions(&self, _address: &str, _limit: u32) -> Result<Vec<Transaction>> {
1630 Ok(Vec::new())
1631 }
1632
1633 async fn get_block_number(&self) -> Result<u64> {
1634 Ok(0)
1635 }
1636
1637 async fn get_token_balances(&self, _address: &str) -> Result<Vec<TokenBalance>> {
1638 Ok(Vec::new())
1639 }
1640
1641 async fn enrich_balance_usd(&self, _balance: &mut Balance) {}
1642 }
1643
1644 #[tokio::test]
1645 async fn test_default_get_token_info() {
1646 let client = MinimalChainClient;
1647 let result = client.get_token_info("0xtest").await;
1648 assert!(result.is_err());
1649 assert!(result.unwrap_err().to_string().contains("not supported"));
1650 }
1651
1652 #[tokio::test]
1653 async fn test_default_get_token_holders() {
1654 let client = MinimalChainClient;
1655 let holders = client.get_token_holders("0xtest", 10).await.unwrap();
1656 assert!(holders.is_empty());
1657 }
1658
1659 #[tokio::test]
1660 async fn test_default_get_token_holder_count() {
1661 let client = MinimalChainClient;
1662 let count = client.get_token_holder_count("0xtest").await.unwrap();
1663 assert_eq!(count, 0);
1664 }
1665
1666 #[test]
1671 fn test_chain_metadata_ethereum() {
1672 let meta = chain_metadata("ethereum").unwrap();
1673 assert_eq!(meta.chain_id, "ethereum");
1674 assert_eq!(meta.native_symbol, "ETH");
1675 assert_eq!(meta.native_decimals, 18);
1676 assert_eq!(meta.explorer_token_base, "https://etherscan.io/token");
1677 }
1678
1679 #[test]
1680 fn test_chain_metadata_ethereum_alias() {
1681 let meta = chain_metadata("eth").unwrap();
1682 assert_eq!(meta.chain_id, "ethereum");
1683 assert_eq!(meta.native_symbol, "ETH");
1684 }
1685
1686 #[test]
1687 fn test_chain_metadata_polygon() {
1688 let meta = chain_metadata("polygon").unwrap();
1689 assert_eq!(meta.chain_id, "polygon");
1690 assert_eq!(meta.native_symbol, "MATIC");
1691 assert_eq!(meta.native_decimals, 18);
1692 assert_eq!(meta.explorer_token_base, "https://polygonscan.com/token");
1693 }
1694
1695 #[test]
1696 fn test_chain_metadata_bsc() {
1697 let meta = chain_metadata("bsc").unwrap();
1698 assert_eq!(meta.chain_id, "bsc");
1699 assert_eq!(meta.native_symbol, "BNB");
1700 assert_eq!(meta.native_decimals, 18);
1701 assert_eq!(meta.explorer_token_base, "https://bscscan.com/token");
1702 }
1703
1704 #[test]
1705 fn test_chain_metadata_solana() {
1706 let meta = chain_metadata("solana").unwrap();
1707 assert_eq!(meta.chain_id, "solana");
1708 assert_eq!(meta.native_symbol, "SOL");
1709 assert_eq!(meta.native_decimals, 9);
1710 assert_eq!(meta.explorer_token_base, "https://solscan.io/token");
1711 }
1712
1713 #[test]
1714 fn test_chain_metadata_solana_alias() {
1715 let meta = chain_metadata("sol").unwrap();
1716 assert_eq!(meta.chain_id, "solana");
1717 assert_eq!(meta.native_symbol, "SOL");
1718 }
1719
1720 #[test]
1721 fn test_chain_metadata_tron() {
1722 let meta = chain_metadata("tron").unwrap();
1723 assert_eq!(meta.chain_id, "tron");
1724 assert_eq!(meta.native_symbol, "TRX");
1725 assert_eq!(meta.native_decimals, 6);
1726 assert_eq!(meta.explorer_token_base, "https://tronscan.org/#/token20");
1727 }
1728
1729 #[test]
1730 fn test_chain_metadata_tron_alias() {
1731 let meta = chain_metadata("trx").unwrap();
1732 assert_eq!(meta.chain_id, "tron");
1733 assert_eq!(meta.native_symbol, "TRX");
1734 }
1735
1736 #[test]
1737 fn test_chain_metadata_arbitrum() {
1738 let meta = chain_metadata("arbitrum").unwrap();
1739 assert_eq!(meta.chain_id, "arbitrum");
1740 assert_eq!(meta.native_symbol, "ETH");
1741 assert_eq!(meta.native_decimals, 18);
1742 assert_eq!(meta.explorer_token_base, "https://arbiscan.io/token");
1743 }
1744
1745 #[test]
1746 fn test_chain_metadata_optimism() {
1747 let meta = chain_metadata("optimism").unwrap();
1748 assert_eq!(meta.chain_id, "optimism");
1749 assert_eq!(meta.native_symbol, "ETH");
1750 assert_eq!(meta.native_decimals, 18);
1751 assert_eq!(
1752 meta.explorer_token_base,
1753 "https://optimistic.etherscan.io/token"
1754 );
1755 }
1756
1757 #[test]
1758 fn test_chain_metadata_base() {
1759 let meta = chain_metadata("base").unwrap();
1760 assert_eq!(meta.chain_id, "base");
1761 assert_eq!(meta.native_symbol, "ETH");
1762 assert_eq!(meta.native_decimals, 18);
1763 assert_eq!(meta.explorer_token_base, "https://basescan.org/token");
1764 }
1765
1766 #[test]
1767 fn test_chain_metadata_case_insensitive() {
1768 let meta1 = chain_metadata("ETHEREUM").unwrap();
1769 let meta2 = chain_metadata("Ethereum").unwrap();
1770 let meta3 = chain_metadata("ethereum").unwrap();
1771 assert_eq!(meta1.chain_id, meta2.chain_id);
1772 assert_eq!(meta2.chain_id, meta3.chain_id);
1773 }
1774
1775 #[test]
1776 fn test_chain_metadata_unknown() {
1777 assert!(chain_metadata("bitcoin").is_none());
1778 assert!(chain_metadata("litecoin").is_none());
1779 assert!(chain_metadata("unknown").is_none());
1780 assert!(chain_metadata("").is_none());
1781 }
1782
1783 #[test]
1784 fn test_native_symbol_ethereum() {
1785 assert_eq!(native_symbol("ethereum"), "ETH");
1786 assert_eq!(native_symbol("eth"), "ETH");
1787 }
1788
1789 #[test]
1790 fn test_native_symbol_polygon() {
1791 assert_eq!(native_symbol("polygon"), "MATIC");
1792 }
1793
1794 #[test]
1795 fn test_native_symbol_bsc() {
1796 assert_eq!(native_symbol("bsc"), "BNB");
1797 }
1798
1799 #[test]
1800 fn test_native_symbol_solana() {
1801 assert_eq!(native_symbol("solana"), "SOL");
1802 assert_eq!(native_symbol("sol"), "SOL");
1803 }
1804
1805 #[test]
1806 fn test_native_symbol_tron() {
1807 assert_eq!(native_symbol("tron"), "TRX");
1808 assert_eq!(native_symbol("trx"), "TRX");
1809 }
1810
1811 #[test]
1812 fn test_native_symbol_arbitrum() {
1813 assert_eq!(native_symbol("arbitrum"), "ETH");
1814 }
1815
1816 #[test]
1817 fn test_native_symbol_optimism() {
1818 assert_eq!(native_symbol("optimism"), "ETH");
1819 }
1820
1821 #[test]
1822 fn test_native_symbol_base() {
1823 assert_eq!(native_symbol("base"), "ETH");
1824 }
1825
1826 #[test]
1827 fn test_native_symbol_unknown() {
1828 assert_eq!(native_symbol("unknown"), "???");
1829 assert_eq!(native_symbol("bitcoin"), "???");
1830 assert_eq!(native_symbol(""), "???");
1831 }
1832
1833 #[test]
1834 fn test_native_symbol_case_insensitive() {
1835 assert_eq!(native_symbol("ETHEREUM"), "ETH");
1836 assert_eq!(native_symbol("Ethereum"), "ETH");
1837 assert_eq!(native_symbol("ethereum"), "ETH");
1838 }
1839
1840 #[tokio::test]
1841 async fn test_chain_client_default_get_code() {
1842 let client = MinimalChainClient;
1843 let result = client.get_code("0x1234").await;
1844 assert!(result.is_err());
1845 let err_msg = result.unwrap_err().to_string();
1846 assert!(err_msg.contains("not supported"));
1847 }
1848
1849 fn tx(hash: &str, gas_used: Option<u64>, input: &str, status: Option<bool>) -> Transaction {
1854 Transaction {
1855 hash: hash.to_string(),
1856 block_number: Some(1),
1857 timestamp: Some(1700000000),
1858 from: "0xfrom".to_string(),
1859 to: Some("0xto".to_string()),
1860 value: "0".to_string(),
1861 gas_limit: 21000,
1862 gas_used,
1863 gas_price: "20000000000".to_string(),
1864 nonce: 0,
1865 input: input.to_string(),
1866 status,
1867 }
1868 }
1869
1870 #[test]
1871 fn test_analyze_gas_usage_empty_transactions() {
1872 let txs: Vec<Transaction> = vec![];
1873 let result = super::analyze_gas_usage(&txs);
1874 assert_eq!(result.avg_gas_used, 0);
1875 assert_eq!(result.max_gas_used, 0);
1876 assert_eq!(result.min_gas_used, 0);
1877 assert_eq!(result.tx_count, 0);
1878 assert_eq!(result.failed_tx_count, 0);
1879 assert_eq!(result.wasted_gas, 0);
1880 assert!(result.gas_by_function.is_empty());
1881 }
1882
1883 #[test]
1884 fn test_analyze_gas_usage_single_tx() {
1885 let txs = vec![tx("0x1", Some(100_000), "0x", Some(true))];
1886 let result = super::analyze_gas_usage(&txs);
1887 assert_eq!(result.avg_gas_used, 100_000);
1888 assert_eq!(result.max_gas_used, 100_000);
1889 assert_eq!(result.min_gas_used, 100_000);
1890 assert_eq!(result.tx_count, 1);
1891 }
1892
1893 #[test]
1894 fn test_analyze_gas_usage_multiple_txs() {
1895 let txs = vec![
1896 tx("0x1", Some(50_000), "0xa9059cbb", Some(true)),
1897 tx("0x2", Some(150_000), "0xa9059cbb", Some(true)),
1898 tx("0x3", Some(100_000), "0xa9059cbb", Some(true)),
1899 ];
1900 let result = super::analyze_gas_usage(&txs);
1901 assert_eq!(result.avg_gas_used, 100_000); assert_eq!(result.max_gas_used, 150_000);
1903 assert_eq!(result.min_gas_used, 50_000);
1904 assert_eq!(result.tx_count, 3);
1905 }
1906
1907 #[test]
1908 fn test_analyze_gas_usage_failed_tx() {
1909 let txs = vec![
1910 tx("0x1", Some(80_000), "0x", Some(true)),
1911 tx("0x2", Some(120_000), "0x", Some(false)),
1912 ];
1913 let result = super::analyze_gas_usage(&txs);
1914 assert_eq!(result.failed_tx_count, 1);
1915 assert_eq!(result.wasted_gas, 120_000);
1916 }
1917
1918 #[test]
1919 fn test_analyze_gas_usage_gas_by_function() {
1920 let txs = vec![
1922 tx("0x1", Some(100_000), "0xa9059cbb0000", Some(true)),
1923 tx("0x2", Some(200_000), "0xa9059cbb0000", Some(true)),
1924 tx("0x3", Some(50_000), "0x095ea7b30000", Some(true)),
1925 ];
1926 let result = super::analyze_gas_usage(&txs);
1927 assert_eq!(result.gas_by_function.len(), 2);
1928 let by_sel: std::collections::HashMap<_, _> = result
1929 .gas_by_function
1930 .iter()
1931 .map(|g| (g.function.as_str(), g))
1932 .collect();
1933 let transfer = by_sel.get("0xa9059cbb").unwrap();
1934 assert_eq!(transfer.call_count, 2);
1935 assert_eq!(transfer.total_gas, 300_000);
1936 assert_eq!(transfer.avg_gas, 150_000);
1937 let approve = by_sel.get("0x095ea7b3").unwrap();
1938 assert_eq!(approve.call_count, 1);
1939 assert_eq!(approve.total_gas, 50_000);
1940 }
1941
1942 #[test]
1943 fn test_analyze_gas_usage_input_0x_transfer() {
1944 let txs = vec![tx("0x1", Some(21_000), "0x", Some(true))];
1945 let result = super::analyze_gas_usage(&txs);
1946 assert_eq!(result.gas_by_function.len(), 1);
1947 assert_eq!(result.gas_by_function[0].function, "transfer()");
1948 }
1949
1950 #[test]
1951 fn test_analyze_gas_usage_input_empty_transfer() {
1952 let txs = vec![tx("0x1", Some(21_000), "", 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_gas_used_none() {
1960 let txs = vec![tx("0x1", None, "0x", Some(true))];
1961 let result = super::analyze_gas_usage(&txs);
1962 assert_eq!(result.avg_gas_used, 0);
1963 assert_eq!(result.max_gas_used, 0);
1964 assert_eq!(result.min_gas_used, 0);
1965 }
1966
1967 #[test]
1968 fn test_analyze_gas_usage_short_input_uses_full_input_as_selector() {
1969 let txs = vec![tx("0x1", Some(50_000), "0x1234567", Some(true))];
1970 let result = super::analyze_gas_usage(&txs);
1971 assert_eq!(result.gas_by_function.len(), 1);
1972 assert_eq!(result.gas_by_function[0].function, "0x1234567");
1973 }
1974}
1975
1976#[cfg(any(test, feature = "test-support"))]
1985pub mod mocks {
1986 use super::*;
1987 use crate::chains::dex::{DexDataSource, DexTokenData, TokenSearchResult};
1988 use async_trait::async_trait;
1989
1990 #[derive(Debug, Clone)]
1992 pub struct MockChainClient {
1993 pub chain: String,
1994 pub symbol: String,
1995 pub balance: Balance,
1996 pub transaction: Transaction,
1997 pub transactions: Vec<Transaction>,
1998 pub token_balances: Vec<TokenBalance>,
1999 pub block_number: u64,
2000 pub token_info: Option<Token>,
2001 pub token_holders: Vec<TokenHolder>,
2002 pub token_holder_count: u64,
2003 }
2004
2005 impl MockChainClient {
2006 pub fn new(chain: &str, symbol: &str) -> Self {
2008 Self {
2009 chain: chain.to_string(),
2010 symbol: symbol.to_string(),
2011 balance: Balance {
2012 raw: "1000000000000000000".to_string(),
2013 formatted: "1.0".to_string(),
2014 decimals: 18,
2015 symbol: symbol.to_string(),
2016 usd_value: Some(2500.0),
2017 },
2018 transaction: Transaction {
2019 hash: "0xmocktx".to_string(),
2020 block_number: Some(12345678),
2021 timestamp: Some(1700000000),
2022 from: "0xfrom".to_string(),
2023 to: Some("0xto".to_string()),
2024 value: "1.0".to_string(),
2025 gas_limit: 21000,
2026 gas_used: Some(21000),
2027 gas_price: "20000000000".to_string(),
2028 nonce: 42,
2029 input: "0x".to_string(),
2030 status: Some(true),
2031 },
2032 transactions: vec![],
2033 token_balances: vec![],
2034 block_number: 12345678,
2035 token_info: None,
2036 token_holders: vec![],
2037 token_holder_count: 0,
2038 }
2039 }
2040 }
2041
2042 #[async_trait]
2043 impl ChainClient for MockChainClient {
2044 fn chain_name(&self) -> &str {
2045 &self.chain
2046 }
2047
2048 fn native_token_symbol(&self) -> &str {
2049 &self.symbol
2050 }
2051
2052 async fn get_balance(&self, _address: &str) -> Result<Balance> {
2053 Ok(self.balance.clone())
2054 }
2055
2056 async fn enrich_balance_usd(&self, _balance: &mut Balance) {
2057 }
2059
2060 async fn get_transaction(&self, _hash: &str) -> Result<Transaction> {
2061 Ok(self.transaction.clone())
2062 }
2063
2064 async fn get_transactions(&self, _address: &str, _limit: u32) -> Result<Vec<Transaction>> {
2065 Ok(self.transactions.clone())
2066 }
2067
2068 async fn get_block_number(&self) -> Result<u64> {
2069 Ok(self.block_number)
2070 }
2071
2072 async fn get_token_balances(&self, _address: &str) -> Result<Vec<TokenBalance>> {
2073 Ok(self.token_balances.clone())
2074 }
2075
2076 async fn get_token_info(&self, _address: &str) -> Result<Token> {
2077 match &self.token_info {
2078 Some(t) => Ok(t.clone()),
2079 None => Err(crate::error::ScopeError::Chain(
2080 "Token info not available".to_string(),
2081 )),
2082 }
2083 }
2084
2085 async fn get_token_holders(&self, _address: &str, _limit: u32) -> Result<Vec<TokenHolder>> {
2086 Ok(self.token_holders.clone())
2087 }
2088
2089 async fn get_token_holder_count(&self, _address: &str) -> Result<u64> {
2090 Ok(self.token_holder_count)
2091 }
2092 }
2093
2094 #[derive(Debug, Clone)]
2096 pub struct MockDexSource {
2097 pub token_price: Option<f64>,
2098 pub native_price: Option<f64>,
2099 pub token_data: Option<DexTokenData>,
2100 pub search_results: Vec<TokenSearchResult>,
2101 }
2102
2103 impl Default for MockDexSource {
2104 fn default() -> Self {
2105 Self::new()
2106 }
2107 }
2108
2109 impl MockDexSource {
2110 pub fn new() -> Self {
2112 Self {
2113 token_price: Some(1.0),
2114 native_price: Some(2500.0),
2115 token_data: Some(DexTokenData {
2116 address: "0xmocktoken".to_string(),
2117 symbol: "MOCK".to_string(),
2118 name: "Mock Token".to_string(),
2119 price_usd: 1.0,
2120 price_change_24h: 5.0,
2121 price_change_6h: 2.0,
2122 price_change_1h: 0.5,
2123 price_change_5m: 0.1,
2124 volume_24h: 1_000_000.0,
2125 volume_6h: 250_000.0,
2126 volume_1h: 50_000.0,
2127 liquidity_usd: 5_000_000.0,
2128 market_cap: Some(100_000_000.0),
2129 fdv: Some(200_000_000.0),
2130 pairs: vec![],
2131 price_history: vec![],
2132 volume_history: vec![],
2133 total_buys_24h: 500,
2134 total_sells_24h: 450,
2135 total_buys_6h: 120,
2136 total_sells_6h: 110,
2137 total_buys_1h: 20,
2138 total_sells_1h: 18,
2139 earliest_pair_created_at: Some(1690000000),
2140 image_url: None,
2141 websites: vec![],
2142 socials: vec![],
2143 dexscreener_url: None,
2144 }),
2145 search_results: vec![],
2146 }
2147 }
2148 }
2149
2150 #[async_trait]
2151 impl DexDataSource for MockDexSource {
2152 async fn get_token_price(&self, _chain: &str, _address: &str) -> Option<f64> {
2153 self.token_price
2154 }
2155
2156 async fn get_native_token_price(&self, _chain: &str) -> Option<f64> {
2157 self.native_price
2158 }
2159
2160 async fn get_token_data(&self, _chain: &str, _address: &str) -> Result<DexTokenData> {
2161 match &self.token_data {
2162 Some(data) => Ok(data.clone()),
2163 None => Err(crate::error::ScopeError::NotFound(
2164 "No DEX data found".to_string(),
2165 )),
2166 }
2167 }
2168
2169 async fn search_tokens(
2170 &self,
2171 _query: &str,
2172 _chain: Option<&str>,
2173 ) -> Result<Vec<TokenSearchResult>> {
2174 Ok(self.search_results.clone())
2175 }
2176 }
2177
2178 pub struct MockClientFactory {
2180 pub mock_client: MockChainClient,
2181 pub mock_dex: MockDexSource,
2182 }
2183
2184 impl Default for MockClientFactory {
2185 fn default() -> Self {
2186 Self::new()
2187 }
2188 }
2189
2190 impl MockClientFactory {
2191 pub fn new() -> Self {
2193 Self {
2194 mock_client: MockChainClient::new("ethereum", "ETH"),
2195 mock_dex: MockDexSource::new(),
2196 }
2197 }
2198 }
2199
2200 impl ChainClientFactory for MockClientFactory {
2201 fn create_chain_client(&self, _chain: &str) -> Result<Box<dyn ChainClient>> {
2202 Ok(Box::new(self.mock_client.clone()))
2203 }
2204
2205 fn create_dex_client(&self) -> Box<dyn DexDataSource> {
2206 Box::new(self.mock_dex.clone())
2207 }
2208 }
2209}