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 async_trait::async_trait;
100use serde::{Deserialize, Serialize};
101
102#[async_trait]
120pub trait ChainClient: Send + Sync {
121 fn chain_name(&self) -> &str;
123
124 fn native_token_symbol(&self) -> &str;
126
127 async fn get_balance(&self, address: &str) -> Result<Balance>;
137
138 async fn enrich_balance_usd(&self, balance: &mut Balance);
144
145 async fn get_transaction(&self, hash: &str) -> Result<Transaction>;
155
156 async fn get_transactions(&self, address: &str, limit: u32) -> Result<Vec<Transaction>>;
167
168 async fn get_block_number(&self) -> Result<u64>;
170
171 async fn get_token_balances(&self, address: &str) -> Result<Vec<TokenBalance>>;
176
177 async fn get_token_info(&self, _address: &str) -> Result<Token> {
182 Err(crate::error::ScopeError::Chain(
183 "Token info lookup not supported on this chain".to_string(),
184 ))
185 }
186
187 async fn get_token_holders(&self, _address: &str, _limit: u32) -> Result<Vec<TokenHolder>> {
192 Ok(Vec::new())
193 }
194
195 async fn get_token_holder_count(&self, _address: &str) -> Result<u64> {
200 Ok(0)
201 }
202
203 async fn get_code(&self, _address: &str) -> Result<String> {
207 Err(crate::error::ScopeError::Chain(
208 "Code lookup not supported on this chain".to_string(),
209 ))
210 }
211
212 async fn get_storage_at(&self, _address: &str, _slot: &str) -> Result<String> {
216 Err(crate::error::ScopeError::Chain(
217 "Storage lookup not supported on this chain".to_string(),
218 ))
219 }
220}
221
222pub trait ChainClientFactory: Send + Sync {
238 fn create_chain_client(&self, chain: &str) -> Result<Box<dyn ChainClient>>;
244
245 fn create_dex_client(&self) -> Box<dyn DexDataSource>;
247}
248
249pub struct DefaultClientFactory {
251 pub chains_config: crate::config::ChainsConfig,
253}
254
255impl ChainClientFactory for DefaultClientFactory {
256 fn create_chain_client(&self, chain: &str) -> Result<Box<dyn ChainClient>> {
257 match chain.to_lowercase().as_str() {
258 "solana" | "sol" => Ok(Box::new(SolanaClient::new(&self.chains_config)?)),
259 "tron" | "trx" => Ok(Box::new(TronClient::new(&self.chains_config)?)),
260 _ => Ok(Box::new(EthereumClient::for_chain(
261 chain,
262 &self.chains_config,
263 )?)),
264 }
265 }
266
267 fn create_dex_client(&self) -> Box<dyn DexDataSource> {
268 Box::new(DexClient::new())
269 }
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct Balance {
275 pub raw: String,
277
278 pub formatted: String,
280
281 pub decimals: u8,
283
284 pub symbol: String,
286
287 #[serde(skip_serializing_if = "Option::is_none")]
289 pub usd_value: Option<f64>,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct Transaction {
295 pub hash: String,
297
298 pub block_number: Option<u64>,
300
301 pub timestamp: Option<u64>,
303
304 pub from: String,
306
307 pub to: Option<String>,
309
310 pub value: String,
312
313 pub gas_limit: u64,
315
316 pub gas_used: Option<u64>,
318
319 pub gas_price: String,
321
322 pub nonce: u64,
324
325 pub input: String,
327
328 pub status: Option<bool>,
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct Token {
335 pub contract_address: String,
337
338 pub symbol: String,
340
341 pub name: String,
343
344 pub decimals: u8,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct TokenBalance {
351 pub token: Token,
353
354 pub balance: String,
356
357 pub formatted_balance: String,
359
360 #[serde(skip_serializing_if = "Option::is_none")]
362 pub usd_value: Option<f64>,
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize)]
371pub struct TokenHolder {
372 pub address: String,
374
375 pub balance: String,
377
378 pub formatted_balance: String,
380
381 pub percentage: f64,
383
384 pub rank: u32,
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct PricePoint {
391 pub timestamp: i64,
393
394 pub price: f64,
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize)]
400pub struct VolumePoint {
401 pub timestamp: i64,
403
404 pub volume: f64,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct HolderCountPoint {
411 pub timestamp: i64,
413
414 pub count: u64,
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize)]
420pub struct DexPair {
421 pub dex_name: String,
423
424 pub pair_address: String,
426
427 pub base_token: String,
429
430 pub quote_token: String,
432
433 pub price_usd: f64,
435
436 pub volume_24h: f64,
438
439 pub liquidity_usd: f64,
441
442 pub price_change_24h: f64,
444
445 pub buys_24h: u64,
447
448 pub sells_24h: u64,
450
451 pub buys_6h: u64,
453
454 pub sells_6h: u64,
456
457 pub buys_1h: u64,
459
460 pub sells_1h: u64,
462
463 pub pair_created_at: Option<i64>,
465
466 pub url: Option<String>,
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize)]
472pub struct TokenAnalytics {
473 pub token: Token,
475
476 pub chain: String,
478
479 pub holders: Vec<TokenHolder>,
481
482 pub total_holders: u64,
484
485 pub volume_24h: f64,
487
488 pub volume_7d: f64,
490
491 pub price_usd: f64,
493
494 pub price_change_24h: f64,
496
497 pub price_change_7d: f64,
499
500 pub liquidity_usd: f64,
502
503 #[serde(skip_serializing_if = "Option::is_none")]
505 pub market_cap: Option<f64>,
506
507 #[serde(skip_serializing_if = "Option::is_none")]
509 pub fdv: Option<f64>,
510
511 #[serde(skip_serializing_if = "Option::is_none")]
513 pub total_supply: Option<String>,
514
515 #[serde(skip_serializing_if = "Option::is_none")]
517 pub circulating_supply: Option<String>,
518
519 pub price_history: Vec<PricePoint>,
521
522 pub volume_history: Vec<VolumePoint>,
524
525 pub holder_history: Vec<HolderCountPoint>,
527
528 pub dex_pairs: Vec<DexPair>,
530
531 pub fetched_at: i64,
533
534 #[serde(skip_serializing_if = "Option::is_none")]
536 pub top_10_concentration: Option<f64>,
537
538 #[serde(skip_serializing_if = "Option::is_none")]
540 pub top_50_concentration: Option<f64>,
541
542 #[serde(skip_serializing_if = "Option::is_none")]
544 pub top_100_concentration: Option<f64>,
545
546 pub price_change_6h: f64,
548
549 pub price_change_1h: f64,
551
552 pub total_buys_24h: u64,
554
555 pub total_sells_24h: u64,
557
558 pub total_buys_6h: u64,
560
561 pub total_sells_6h: u64,
563
564 pub total_buys_1h: u64,
566
567 pub total_sells_1h: u64,
569
570 #[serde(skip_serializing_if = "Option::is_none")]
572 pub token_age_hours: Option<f64>,
573
574 #[serde(skip_serializing_if = "Option::is_none")]
576 pub image_url: Option<String>,
577
578 pub websites: Vec<String>,
580
581 pub socials: Vec<TokenSocial>,
583
584 #[serde(skip_serializing_if = "Option::is_none")]
586 pub dexscreener_url: Option<String>,
587}
588
589#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
591pub struct TokenSocial {
592 pub platform: String,
594 pub url: String,
596}
597
598#[derive(Debug, Clone, Serialize, Deserialize)]
604pub struct NftMetadata {
605 pub token_id: String,
607 pub name: Option<String>,
609 pub description: Option<String>,
611 pub image_url: Option<String>,
613 pub token_uri: Option<String>,
615 pub standard: String,
617 pub collection_name: Option<String>,
619 pub attributes: Vec<NftAttribute>,
621}
622
623#[derive(Debug, Clone, Serialize, Deserialize)]
625pub struct NftAttribute {
626 pub trait_type: String,
628 pub value: String,
630}
631
632#[derive(Debug, Clone, Serialize, Deserialize)]
638pub struct GasAnalysis {
639 pub avg_gas_used: u64,
641 pub max_gas_used: u64,
643 pub min_gas_used: u64,
645 pub total_gas_cost_wei: String,
647 pub total_gas_cost_formatted: String,
649 pub tx_count: u64,
651 pub gas_by_function: Vec<GasByFunction>,
653 pub failed_tx_count: u64,
655 pub wasted_gas: u64,
657}
658
659#[derive(Debug, Clone, Serialize, Deserialize)]
661pub struct GasByFunction {
662 pub function: String,
664 pub call_count: u64,
666 pub avg_gas: u64,
668 pub total_gas: u64,
670}
671
672#[derive(Debug, Clone)]
680pub struct ChainMetadata {
681 pub chain_id: &'static str,
683 pub native_symbol: &'static str,
685 pub native_decimals: u8,
687 pub explorer_token_base: &'static str,
689}
690
691pub fn chain_metadata(chain: &str) -> Option<ChainMetadata> {
695 match chain.to_lowercase().as_str() {
696 "ethereum" | "eth" => Some(ChainMetadata {
697 chain_id: "ethereum",
698 native_symbol: "ETH",
699 native_decimals: 18,
700 explorer_token_base: "https://etherscan.io/token",
701 }),
702 "polygon" => Some(ChainMetadata {
703 chain_id: "polygon",
704 native_symbol: "MATIC",
705 native_decimals: 18,
706 explorer_token_base: "https://polygonscan.com/token",
707 }),
708 "arbitrum" => Some(ChainMetadata {
709 chain_id: "arbitrum",
710 native_symbol: "ETH",
711 native_decimals: 18,
712 explorer_token_base: "https://arbiscan.io/token",
713 }),
714 "optimism" => Some(ChainMetadata {
715 chain_id: "optimism",
716 native_symbol: "ETH",
717 native_decimals: 18,
718 explorer_token_base: "https://optimistic.etherscan.io/token",
719 }),
720 "base" => Some(ChainMetadata {
721 chain_id: "base",
722 native_symbol: "ETH",
723 native_decimals: 18,
724 explorer_token_base: "https://basescan.org/token",
725 }),
726 "bsc" => Some(ChainMetadata {
727 chain_id: "bsc",
728 native_symbol: "BNB",
729 native_decimals: 18,
730 explorer_token_base: "https://bscscan.com/token",
731 }),
732 "solana" | "sol" => Some(ChainMetadata {
733 chain_id: "solana",
734 native_symbol: "SOL",
735 native_decimals: 9,
736 explorer_token_base: "https://solscan.io/token",
737 }),
738 "tron" | "trx" => Some(ChainMetadata {
739 chain_id: "tron",
740 native_symbol: "TRX",
741 native_decimals: 6,
742 explorer_token_base: "https://tronscan.org/#/token20",
743 }),
744 _ => None,
745 }
746}
747
748pub fn native_symbol(chain: &str) -> &'static str {
750 chain_metadata(chain)
751 .map(|m| m.native_symbol)
752 .unwrap_or("???")
753}
754
755pub fn infer_chain_from_address(address: &str) -> Option<&'static str> {
781 if address.starts_with('T') && address.len() == 34 && bs58::decode(address).into_vec().is_ok() {
783 return Some("tron");
784 }
785
786 if address.starts_with("0x")
788 && address.len() == 42
789 && address[2..].chars().all(|c| c.is_ascii_hexdigit())
790 {
791 return Some("ethereum");
792 }
793
794 if address.len() >= 32
796 && address.len() <= 44
797 && let Ok(decoded) = bs58::decode(address).into_vec()
798 && decoded.len() == 32
799 {
800 return Some("solana");
801 }
802
803 None
804}
805
806pub fn infer_chain_from_hash(hash: &str) -> Option<&'static str> {
831 if hash.starts_with("0x")
833 && hash.len() == 66
834 && hash[2..].chars().all(|c| c.is_ascii_hexdigit())
835 {
836 return Some("ethereum");
837 }
838
839 if hash.len() == 64 && hash.chars().all(|c| c.is_ascii_hexdigit()) {
841 return Some("tron");
842 }
843
844 if hash.len() >= 80
846 && hash.len() <= 90
847 && let Ok(decoded) = bs58::decode(hash).into_vec()
848 && decoded.len() == 64
849 {
850 return Some("solana");
851 }
852
853 None
854}
855
856pub fn analyze_gas_usage(transactions: &[Transaction]) -> GasAnalysis {
861 if transactions.is_empty() {
862 return GasAnalysis {
863 avg_gas_used: 0,
864 max_gas_used: 0,
865 min_gas_used: 0,
866 total_gas_cost_wei: "0".to_string(),
867 total_gas_cost_formatted: "0".to_string(),
868 tx_count: 0,
869 gas_by_function: vec![],
870 failed_tx_count: 0,
871 wasted_gas: 0,
872 };
873 }
874
875 let mut total_gas: u64 = 0;
876 let mut max_gas: u64 = 0;
877 let mut min_gas: u64 = u64::MAX;
878 let mut failed_count: u64 = 0;
879 let mut wasted_gas: u64 = 0;
880 let mut function_gas: std::collections::HashMap<String, (u64, u64)> =
881 std::collections::HashMap::new();
882
883 for tx in transactions {
884 let gas_used = tx.gas_used.unwrap_or(0);
885 total_gas += gas_used;
886 if gas_used > max_gas {
887 max_gas = gas_used;
888 }
889 if gas_used < min_gas {
890 min_gas = gas_used;
891 }
892
893 if tx.status == Some(false) {
895 failed_count += 1;
896 wasted_gas += gas_used;
897 }
898
899 let selector = if tx.input.len() >= 10 {
901 tx.input[..10].to_string()
902 } else if tx.input.is_empty() || tx.input == "0x" {
903 "transfer()".to_string()
904 } else {
905 tx.input.clone()
906 };
907
908 let entry = function_gas.entry(selector).or_insert((0, 0));
909 entry.0 += 1; entry.1 += gas_used; }
912
913 let tx_count = transactions.len() as u64;
914 let avg_gas = if tx_count > 0 {
915 total_gas / tx_count
916 } else {
917 0
918 };
919
920 if min_gas == u64::MAX {
921 min_gas = 0;
922 }
923
924 let mut gas_by_function: Vec<GasByFunction> = function_gas
926 .into_iter()
927 .map(|(function, (call_count, total_gas_fn))| GasByFunction {
928 function,
929 call_count,
930 avg_gas: if call_count > 0 {
931 total_gas_fn / call_count
932 } else {
933 0
934 },
935 total_gas: total_gas_fn,
936 })
937 .collect();
938 gas_by_function.sort_by(|a, b| b.total_gas.cmp(&a.total_gas));
939
940 let total_gas_cost_formatted = format!("{} gas units", total_gas);
942
943 GasAnalysis {
944 avg_gas_used: avg_gas,
945 max_gas_used: max_gas,
946 min_gas_used: min_gas,
947 total_gas_cost_wei: total_gas.to_string(),
948 total_gas_cost_formatted,
949 tx_count,
950 gas_by_function,
951 failed_tx_count: failed_count,
952 wasted_gas,
953 }
954}
955
956#[cfg(test)]
961mod tests {
962 use super::*;
963
964 #[test]
965 fn test_balance_serialization() {
966 let balance = Balance {
967 raw: "1000000000000000000".to_string(),
968 formatted: "1.0".to_string(),
969 decimals: 18,
970 symbol: "ETH".to_string(),
971 usd_value: Some(3500.0),
972 };
973
974 let json = serde_json::to_string(&balance).unwrap();
975 assert!(json.contains("1000000000000000000"));
976 assert!(json.contains("1.0"));
977 assert!(json.contains("ETH"));
978 assert!(json.contains("3500"));
979
980 let deserialized: Balance = serde_json::from_str(&json).unwrap();
981 assert_eq!(deserialized.raw, balance.raw);
982 assert_eq!(deserialized.decimals, 18);
983 }
984
985 #[test]
986 fn test_balance_without_usd() {
987 let balance = Balance {
988 raw: "1000000000000000000".to_string(),
989 formatted: "1.0".to_string(),
990 decimals: 18,
991 symbol: "ETH".to_string(),
992 usd_value: None,
993 };
994
995 let json = serde_json::to_string(&balance).unwrap();
996 assert!(!json.contains("usd_value"));
997 }
998
999 #[test]
1000 fn test_transaction_serialization() {
1001 let tx = Transaction {
1002 hash: "0xabc123".to_string(),
1003 block_number: Some(12345678),
1004 timestamp: Some(1700000000),
1005 from: "0xfrom".to_string(),
1006 to: Some("0xto".to_string()),
1007 value: "1.0".to_string(),
1008 gas_limit: 21000,
1009 gas_used: Some(21000),
1010 gas_price: "20000000000".to_string(),
1011 nonce: 42,
1012 input: "0x".to_string(),
1013 status: Some(true),
1014 };
1015
1016 let json = serde_json::to_string(&tx).unwrap();
1017 assert!(json.contains("0xabc123"));
1018 assert!(json.contains("12345678"));
1019 assert!(json.contains("0xfrom"));
1020 assert!(json.contains("0xto"));
1021
1022 let deserialized: Transaction = serde_json::from_str(&json).unwrap();
1023 assert_eq!(deserialized.hash, tx.hash);
1024 assert_eq!(deserialized.nonce, 42);
1025 }
1026
1027 #[test]
1028 fn test_pending_transaction_serialization() {
1029 let tx = Transaction {
1030 hash: "0xpending".to_string(),
1031 block_number: None,
1032 timestamp: None,
1033 from: "0xfrom".to_string(),
1034 to: Some("0xto".to_string()),
1035 value: "1.0".to_string(),
1036 gas_limit: 21000,
1037 gas_used: None,
1038 gas_price: "20000000000".to_string(),
1039 nonce: 0,
1040 input: "0x".to_string(),
1041 status: None,
1042 };
1043
1044 let json = serde_json::to_string(&tx).unwrap();
1045 assert!(json.contains("0xpending"));
1046 assert!(json.contains("null")); let deserialized: Transaction = serde_json::from_str(&json).unwrap();
1049 assert!(deserialized.block_number.is_none());
1050 assert!(deserialized.status.is_none());
1051 }
1052
1053 #[test]
1054 fn test_contract_creation_transaction() {
1055 let tx = Transaction {
1056 hash: "0xcreate".to_string(),
1057 block_number: Some(100),
1058 timestamp: Some(1700000000),
1059 from: "0xdeployer".to_string(),
1060 to: None, value: "0".to_string(),
1062 gas_limit: 1000000,
1063 gas_used: Some(500000),
1064 gas_price: "20000000000".to_string(),
1065 nonce: 0,
1066 input: "0x608060...".to_string(),
1067 status: Some(true),
1068 };
1069
1070 let json = serde_json::to_string(&tx).unwrap();
1071 assert!(json.contains("\"to\":null"));
1072 }
1073
1074 #[test]
1075 fn test_token_serialization() {
1076 let token = Token {
1077 contract_address: "0xtoken".to_string(),
1078 symbol: "USDC".to_string(),
1079 name: "USD Coin".to_string(),
1080 decimals: 6,
1081 };
1082
1083 let json = serde_json::to_string(&token).unwrap();
1084 assert!(json.contains("USDC"));
1085 assert!(json.contains("USD Coin"));
1086 assert!(json.contains("\"decimals\":6"));
1087
1088 let deserialized: Token = serde_json::from_str(&json).unwrap();
1089 assert_eq!(deserialized.decimals, 6);
1090 }
1091
1092 #[test]
1093 fn test_token_balance_serialization() {
1094 let token_balance = TokenBalance {
1095 token: Token {
1096 contract_address: "0xtoken".to_string(),
1097 symbol: "USDC".to_string(),
1098 name: "USD Coin".to_string(),
1099 decimals: 6,
1100 },
1101 balance: "1000000".to_string(),
1102 formatted_balance: "1.0".to_string(),
1103 usd_value: Some(1.0),
1104 };
1105
1106 let json = serde_json::to_string(&token_balance).unwrap();
1107 assert!(json.contains("USDC"));
1108 assert!(json.contains("1000000"));
1109 assert!(json.contains("1.0"));
1110 }
1111
1112 #[test]
1113 fn test_balance_debug() {
1114 let balance = Balance {
1115 raw: "1000".to_string(),
1116 formatted: "0.001".to_string(),
1117 decimals: 18,
1118 symbol: "ETH".to_string(),
1119 usd_value: None,
1120 };
1121
1122 let debug_str = format!("{:?}", balance);
1123 assert!(debug_str.contains("Balance"));
1124 assert!(debug_str.contains("1000"));
1125 }
1126
1127 #[test]
1128 fn test_transaction_debug() {
1129 let tx = Transaction {
1130 hash: "0xtest".to_string(),
1131 block_number: Some(1),
1132 timestamp: Some(0),
1133 from: "0x1".to_string(),
1134 to: Some("0x2".to_string()),
1135 value: "0".to_string(),
1136 gas_limit: 21000,
1137 gas_used: Some(21000),
1138 gas_price: "0".to_string(),
1139 nonce: 0,
1140 input: "0x".to_string(),
1141 status: Some(true),
1142 };
1143
1144 let debug_str = format!("{:?}", tx);
1145 assert!(debug_str.contains("Transaction"));
1146 assert!(debug_str.contains("0xtest"));
1147 }
1148
1149 #[test]
1154 fn test_infer_chain_from_address_evm() {
1155 assert_eq!(
1157 super::infer_chain_from_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2"),
1158 Some("ethereum")
1159 );
1160 assert_eq!(
1161 super::infer_chain_from_address("0x0000000000000000000000000000000000000000"),
1162 Some("ethereum")
1163 );
1164 assert_eq!(
1165 super::infer_chain_from_address("0xABCDEF1234567890abcdef1234567890ABCDEF12"),
1166 Some("ethereum")
1167 );
1168 }
1169
1170 #[test]
1171 fn test_infer_chain_from_address_tron() {
1172 assert_eq!(
1174 super::infer_chain_from_address("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf"),
1175 Some("tron")
1176 );
1177 assert_eq!(
1178 super::infer_chain_from_address("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"),
1179 Some("tron")
1180 );
1181 }
1182
1183 #[test]
1184 fn test_infer_chain_from_address_solana() {
1185 assert_eq!(
1187 super::infer_chain_from_address("DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy"),
1188 Some("solana")
1189 );
1190 assert_eq!(
1192 super::infer_chain_from_address("11111111111111111111111111111111"),
1193 Some("solana")
1194 );
1195 }
1196
1197 #[test]
1198 fn test_infer_chain_from_address_invalid() {
1199 assert_eq!(super::infer_chain_from_address("0x123"), None);
1201 assert_eq!(super::infer_chain_from_address("not_an_address"), None);
1203 assert_eq!(super::infer_chain_from_address(""), None);
1205 assert_eq!(super::infer_chain_from_address("0x123456"), None);
1207 assert_eq!(
1209 super::infer_chain_from_address("ADqSquXBgUCLYvYC4XZgrprLK589dkhSCf"),
1210 None
1211 );
1212 }
1213
1214 #[test]
1215 fn test_infer_chain_from_hash_evm() {
1216 assert_eq!(
1218 super::infer_chain_from_hash(
1219 "0xabc123def456789012345678901234567890123456789012345678901234abcd"
1220 ),
1221 Some("ethereum")
1222 );
1223 assert_eq!(
1224 super::infer_chain_from_hash(
1225 "0x0000000000000000000000000000000000000000000000000000000000000000"
1226 ),
1227 Some("ethereum")
1228 );
1229 }
1230
1231 #[test]
1232 fn test_infer_chain_from_hash_tron() {
1233 assert_eq!(
1235 super::infer_chain_from_hash(
1236 "abc123def456789012345678901234567890123456789012345678901234abcd"
1237 ),
1238 Some("tron")
1239 );
1240 assert_eq!(
1241 super::infer_chain_from_hash(
1242 "0000000000000000000000000000000000000000000000000000000000000000"
1243 ),
1244 Some("tron")
1245 );
1246 }
1247
1248 #[test]
1249 fn test_infer_chain_from_hash_solana() {
1250 let solana_sig = "5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW";
1253 assert_eq!(super::infer_chain_from_hash(solana_sig), Some("solana"));
1254 }
1255
1256 #[test]
1257 fn test_infer_chain_from_hash_invalid() {
1258 assert_eq!(super::infer_chain_from_hash("0x123"), None);
1260 assert_eq!(super::infer_chain_from_hash("not_a_hash"), None);
1262 assert_eq!(super::infer_chain_from_hash(""), None);
1264 assert_eq!(
1266 super::infer_chain_from_hash(
1267 "abc123gef456789012345678901234567890123456789012345678901234abcd"
1268 ),
1269 None
1270 );
1271 }
1272
1273 #[test]
1278 fn test_default_client_factory_create_dex_client() {
1279 let config = crate::config::ChainsConfig::default();
1280 let factory = DefaultClientFactory {
1281 chains_config: config,
1282 };
1283 let dex = factory.create_dex_client();
1284 let _ = format!("{:?}", std::mem::size_of_val(&dex));
1286 }
1287
1288 #[test]
1289 fn test_default_client_factory_create_ethereum_client() {
1290 let config = crate::config::ChainsConfig::default();
1291 let factory = DefaultClientFactory {
1292 chains_config: config,
1293 };
1294 let client = factory.create_chain_client("ethereum");
1296 assert!(client.is_ok());
1297 assert_eq!(client.unwrap().chain_name(), "ethereum");
1298 }
1299
1300 #[test]
1301 fn test_default_client_factory_create_polygon_client() {
1302 let config = crate::config::ChainsConfig::default();
1303 let factory = DefaultClientFactory {
1304 chains_config: config,
1305 };
1306 let client = factory.create_chain_client("polygon");
1307 assert!(client.is_ok());
1308 assert_eq!(client.unwrap().chain_name(), "polygon");
1309 }
1310
1311 #[test]
1312 fn test_default_client_factory_create_solana_client() {
1313 let config = crate::config::ChainsConfig::default();
1314 let factory = DefaultClientFactory {
1315 chains_config: config,
1316 };
1317 let client = factory.create_chain_client("solana");
1318 assert!(client.is_ok());
1319 assert_eq!(client.unwrap().chain_name(), "solana");
1320 }
1321
1322 #[test]
1323 fn test_default_client_factory_create_sol_alias() {
1324 let config = crate::config::ChainsConfig::default();
1325 let factory = DefaultClientFactory {
1326 chains_config: config,
1327 };
1328 let client = factory.create_chain_client("sol");
1329 assert!(client.is_ok());
1330 assert_eq!(client.unwrap().chain_name(), "solana");
1331 }
1332
1333 #[test]
1334 fn test_default_client_factory_create_tron_client() {
1335 let config = crate::config::ChainsConfig::default();
1336 let factory = DefaultClientFactory {
1337 chains_config: config,
1338 };
1339 let client = factory.create_chain_client("tron");
1340 assert!(client.is_ok());
1341 assert_eq!(client.unwrap().chain_name(), "tron");
1342 }
1343
1344 #[test]
1345 fn test_default_client_factory_create_trx_alias() {
1346 let config = crate::config::ChainsConfig::default();
1347 let factory = DefaultClientFactory {
1348 chains_config: config,
1349 };
1350 let client = factory.create_chain_client("trx");
1351 assert!(client.is_ok());
1352 assert_eq!(client.unwrap().chain_name(), "tron");
1353 }
1354
1355 #[test]
1356 fn test_default_client_factory_create_arbitrum_client() {
1357 let config = crate::config::ChainsConfig::default();
1358 let factory = DefaultClientFactory {
1359 chains_config: config,
1360 };
1361 let client = factory.create_chain_client("arbitrum");
1362 assert!(client.is_ok());
1363 assert_eq!(client.unwrap().chain_name(), "arbitrum");
1364 }
1365
1366 #[test]
1367 fn test_default_client_factory_create_optimism_client() {
1368 let config = crate::config::ChainsConfig::default();
1369 let factory = DefaultClientFactory {
1370 chains_config: config,
1371 };
1372 let client = factory.create_chain_client("optimism");
1373 assert!(client.is_ok());
1374 assert_eq!(client.unwrap().chain_name(), "optimism");
1375 }
1376
1377 #[test]
1378 fn test_default_client_factory_create_base_client() {
1379 let config = crate::config::ChainsConfig::default();
1380 let factory = DefaultClientFactory {
1381 chains_config: config,
1382 };
1383 let client = factory.create_chain_client("base");
1384 assert!(client.is_ok());
1385 assert_eq!(client.unwrap().chain_name(), "base");
1386 }
1387
1388 #[test]
1389 fn test_default_client_factory_create_unsupported_chain_returns_err() {
1390 let config = crate::config::ChainsConfig::default();
1391 let factory = DefaultClientFactory {
1392 chains_config: config,
1393 };
1394 let client = factory.create_chain_client("avalanche");
1395 match &client {
1396 Err(e) => assert!(e.to_string().contains("Unsupported")),
1397 Ok(_) => panic!("expected Err for unsupported chain"),
1398 }
1399 }
1400
1401 #[tokio::test]
1406 async fn test_chain_client_default_get_token_info() {
1407 use super::mocks::MockChainClient;
1408 let client = MockChainClient::new("ethereum", "ETH");
1410 let result = client.get_token_info("0xsometoken").await;
1411 assert!(result.is_err());
1412 }
1413
1414 #[tokio::test]
1415 async fn test_chain_client_default_get_token_holders() {
1416 use super::mocks::MockChainClient;
1417 let client = MockChainClient::new("ethereum", "ETH");
1418 let holders = client.get_token_holders("0xsometoken", 10).await.unwrap();
1419 assert!(holders.is_empty());
1420 }
1421
1422 #[tokio::test]
1423 async fn test_chain_client_default_get_token_holder_count() {
1424 use super::mocks::MockChainClient;
1425 let client = MockChainClient::new("ethereum", "ETH");
1426 let count = client.get_token_holder_count("0xsometoken").await.unwrap();
1427 assert_eq!(count, 0);
1428 }
1429
1430 #[tokio::test]
1431 async fn test_mock_client_factory_creates_chain_client() {
1432 use super::mocks::MockClientFactory;
1433 let factory = MockClientFactory::new();
1434 let client = factory.create_chain_client("anything").unwrap();
1435 assert_eq!(client.chain_name(), "ethereum"); }
1437
1438 #[tokio::test]
1439 async fn test_mock_client_factory_creates_dex_client() {
1440 use super::mocks::MockClientFactory;
1441 let factory = MockClientFactory::new();
1442 let dex = factory.create_dex_client();
1443 let price = dex.get_token_price("ethereum", "0xtest").await;
1444 assert_eq!(price, Some(1.0));
1445 }
1446
1447 #[tokio::test]
1448 async fn test_mock_chain_client_balance() {
1449 use super::mocks::MockChainClient;
1450 let client = MockChainClient::new("ethereum", "ETH");
1451 let balance = client.get_balance("0xtest").await.unwrap();
1452 assert_eq!(balance.formatted, "1.0");
1453 assert_eq!(balance.symbol, "ETH");
1454 assert_eq!(balance.usd_value, Some(2500.0));
1455 }
1456
1457 #[tokio::test]
1458 async fn test_mock_chain_client_transaction() {
1459 use super::mocks::MockChainClient;
1460 let client = MockChainClient::new("ethereum", "ETH");
1461 let tx = client.get_transaction("0xanyhash").await.unwrap();
1462 assert_eq!(tx.hash, "0xmocktx");
1463 assert_eq!(tx.nonce, 42);
1464 }
1465
1466 #[tokio::test]
1467 async fn test_mock_chain_client_block_number() {
1468 use super::mocks::MockChainClient;
1469 let client = MockChainClient::new("ethereum", "ETH");
1470 let block = client.get_block_number().await.unwrap();
1471 assert_eq!(block, 12345678);
1472 }
1473
1474 #[tokio::test]
1475 async fn test_mock_dex_source_data() {
1476 use super::mocks::MockDexSource;
1477 let dex = MockDexSource::new();
1478 let data = dex.get_token_data("ethereum", "0xtest").await.unwrap();
1479 assert_eq!(data.symbol, "MOCK");
1480 assert_eq!(data.price_usd, 1.0);
1481 }
1482
1483 #[tokio::test]
1484 async fn test_mock_dex_source_search() {
1485 use super::mocks::MockDexSource;
1486 let dex = MockDexSource::new();
1487 let results = dex.search_tokens("test", None).await.unwrap();
1488 assert!(results.is_empty());
1489 }
1490
1491 #[tokio::test]
1492 async fn test_mock_dex_source_native_price() {
1493 use super::mocks::MockDexSource;
1494 let dex = MockDexSource::new();
1495 let price = dex.get_native_token_price("ethereum").await;
1496 assert_eq!(price, Some(2500.0));
1497 }
1498
1499 struct MinimalChainClient;
1505
1506 #[async_trait::async_trait]
1507 impl ChainClient for MinimalChainClient {
1508 fn chain_name(&self) -> &str {
1509 "test"
1510 }
1511
1512 fn native_token_symbol(&self) -> &str {
1513 "TEST"
1514 }
1515
1516 async fn get_balance(&self, _address: &str) -> Result<Balance> {
1517 Ok(Balance {
1518 raw: "0".to_string(),
1519 formatted: "0".to_string(),
1520 decimals: 18,
1521 symbol: "TEST".to_string(),
1522 usd_value: None,
1523 })
1524 }
1525
1526 async fn get_transaction(&self, _hash: &str) -> Result<Transaction> {
1527 unimplemented!()
1528 }
1529
1530 async fn get_transactions(&self, _address: &str, _limit: u32) -> Result<Vec<Transaction>> {
1531 Ok(Vec::new())
1532 }
1533
1534 async fn get_block_number(&self) -> Result<u64> {
1535 Ok(0)
1536 }
1537
1538 async fn get_token_balances(&self, _address: &str) -> Result<Vec<TokenBalance>> {
1539 Ok(Vec::new())
1540 }
1541
1542 async fn enrich_balance_usd(&self, _balance: &mut Balance) {}
1543 }
1544
1545 #[tokio::test]
1546 async fn test_default_get_token_info() {
1547 let client = MinimalChainClient;
1548 let result = client.get_token_info("0xtest").await;
1549 assert!(result.is_err());
1550 assert!(result.unwrap_err().to_string().contains("not supported"));
1551 }
1552
1553 #[tokio::test]
1554 async fn test_default_get_token_holders() {
1555 let client = MinimalChainClient;
1556 let holders = client.get_token_holders("0xtest", 10).await.unwrap();
1557 assert!(holders.is_empty());
1558 }
1559
1560 #[tokio::test]
1561 async fn test_default_get_token_holder_count() {
1562 let client = MinimalChainClient;
1563 let count = client.get_token_holder_count("0xtest").await.unwrap();
1564 assert_eq!(count, 0);
1565 }
1566
1567 #[test]
1572 fn test_chain_metadata_ethereum() {
1573 let meta = chain_metadata("ethereum").unwrap();
1574 assert_eq!(meta.chain_id, "ethereum");
1575 assert_eq!(meta.native_symbol, "ETH");
1576 assert_eq!(meta.native_decimals, 18);
1577 assert_eq!(meta.explorer_token_base, "https://etherscan.io/token");
1578 }
1579
1580 #[test]
1581 fn test_chain_metadata_ethereum_alias() {
1582 let meta = chain_metadata("eth").unwrap();
1583 assert_eq!(meta.chain_id, "ethereum");
1584 assert_eq!(meta.native_symbol, "ETH");
1585 }
1586
1587 #[test]
1588 fn test_chain_metadata_polygon() {
1589 let meta = chain_metadata("polygon").unwrap();
1590 assert_eq!(meta.chain_id, "polygon");
1591 assert_eq!(meta.native_symbol, "MATIC");
1592 assert_eq!(meta.native_decimals, 18);
1593 assert_eq!(meta.explorer_token_base, "https://polygonscan.com/token");
1594 }
1595
1596 #[test]
1597 fn test_chain_metadata_bsc() {
1598 let meta = chain_metadata("bsc").unwrap();
1599 assert_eq!(meta.chain_id, "bsc");
1600 assert_eq!(meta.native_symbol, "BNB");
1601 assert_eq!(meta.native_decimals, 18);
1602 assert_eq!(meta.explorer_token_base, "https://bscscan.com/token");
1603 }
1604
1605 #[test]
1606 fn test_chain_metadata_solana() {
1607 let meta = chain_metadata("solana").unwrap();
1608 assert_eq!(meta.chain_id, "solana");
1609 assert_eq!(meta.native_symbol, "SOL");
1610 assert_eq!(meta.native_decimals, 9);
1611 assert_eq!(meta.explorer_token_base, "https://solscan.io/token");
1612 }
1613
1614 #[test]
1615 fn test_chain_metadata_solana_alias() {
1616 let meta = chain_metadata("sol").unwrap();
1617 assert_eq!(meta.chain_id, "solana");
1618 assert_eq!(meta.native_symbol, "SOL");
1619 }
1620
1621 #[test]
1622 fn test_chain_metadata_tron() {
1623 let meta = chain_metadata("tron").unwrap();
1624 assert_eq!(meta.chain_id, "tron");
1625 assert_eq!(meta.native_symbol, "TRX");
1626 assert_eq!(meta.native_decimals, 6);
1627 assert_eq!(meta.explorer_token_base, "https://tronscan.org/#/token20");
1628 }
1629
1630 #[test]
1631 fn test_chain_metadata_tron_alias() {
1632 let meta = chain_metadata("trx").unwrap();
1633 assert_eq!(meta.chain_id, "tron");
1634 assert_eq!(meta.native_symbol, "TRX");
1635 }
1636
1637 #[test]
1638 fn test_chain_metadata_arbitrum() {
1639 let meta = chain_metadata("arbitrum").unwrap();
1640 assert_eq!(meta.chain_id, "arbitrum");
1641 assert_eq!(meta.native_symbol, "ETH");
1642 assert_eq!(meta.native_decimals, 18);
1643 assert_eq!(meta.explorer_token_base, "https://arbiscan.io/token");
1644 }
1645
1646 #[test]
1647 fn test_chain_metadata_optimism() {
1648 let meta = chain_metadata("optimism").unwrap();
1649 assert_eq!(meta.chain_id, "optimism");
1650 assert_eq!(meta.native_symbol, "ETH");
1651 assert_eq!(meta.native_decimals, 18);
1652 assert_eq!(
1653 meta.explorer_token_base,
1654 "https://optimistic.etherscan.io/token"
1655 );
1656 }
1657
1658 #[test]
1659 fn test_chain_metadata_base() {
1660 let meta = chain_metadata("base").unwrap();
1661 assert_eq!(meta.chain_id, "base");
1662 assert_eq!(meta.native_symbol, "ETH");
1663 assert_eq!(meta.native_decimals, 18);
1664 assert_eq!(meta.explorer_token_base, "https://basescan.org/token");
1665 }
1666
1667 #[test]
1668 fn test_chain_metadata_case_insensitive() {
1669 let meta1 = chain_metadata("ETHEREUM").unwrap();
1670 let meta2 = chain_metadata("Ethereum").unwrap();
1671 let meta3 = chain_metadata("ethereum").unwrap();
1672 assert_eq!(meta1.chain_id, meta2.chain_id);
1673 assert_eq!(meta2.chain_id, meta3.chain_id);
1674 }
1675
1676 #[test]
1677 fn test_chain_metadata_unknown() {
1678 assert!(chain_metadata("bitcoin").is_none());
1679 assert!(chain_metadata("litecoin").is_none());
1680 assert!(chain_metadata("unknown").is_none());
1681 assert!(chain_metadata("").is_none());
1682 }
1683
1684 #[test]
1685 fn test_native_symbol_ethereum() {
1686 assert_eq!(native_symbol("ethereum"), "ETH");
1687 assert_eq!(native_symbol("eth"), "ETH");
1688 }
1689
1690 #[test]
1691 fn test_native_symbol_polygon() {
1692 assert_eq!(native_symbol("polygon"), "MATIC");
1693 }
1694
1695 #[test]
1696 fn test_native_symbol_bsc() {
1697 assert_eq!(native_symbol("bsc"), "BNB");
1698 }
1699
1700 #[test]
1701 fn test_native_symbol_solana() {
1702 assert_eq!(native_symbol("solana"), "SOL");
1703 assert_eq!(native_symbol("sol"), "SOL");
1704 }
1705
1706 #[test]
1707 fn test_native_symbol_tron() {
1708 assert_eq!(native_symbol("tron"), "TRX");
1709 assert_eq!(native_symbol("trx"), "TRX");
1710 }
1711
1712 #[test]
1713 fn test_native_symbol_arbitrum() {
1714 assert_eq!(native_symbol("arbitrum"), "ETH");
1715 }
1716
1717 #[test]
1718 fn test_native_symbol_optimism() {
1719 assert_eq!(native_symbol("optimism"), "ETH");
1720 }
1721
1722 #[test]
1723 fn test_native_symbol_base() {
1724 assert_eq!(native_symbol("base"), "ETH");
1725 }
1726
1727 #[test]
1728 fn test_native_symbol_unknown() {
1729 assert_eq!(native_symbol("unknown"), "???");
1730 assert_eq!(native_symbol("bitcoin"), "???");
1731 assert_eq!(native_symbol(""), "???");
1732 }
1733
1734 #[test]
1735 fn test_native_symbol_case_insensitive() {
1736 assert_eq!(native_symbol("ETHEREUM"), "ETH");
1737 assert_eq!(native_symbol("Ethereum"), "ETH");
1738 assert_eq!(native_symbol("ethereum"), "ETH");
1739 }
1740
1741 #[tokio::test]
1742 async fn test_chain_client_default_get_code() {
1743 let client = MinimalChainClient;
1744 let result = client.get_code("0x1234").await;
1745 assert!(result.is_err());
1746 let err_msg = result.unwrap_err().to_string();
1747 assert!(err_msg.contains("not supported"));
1748 }
1749
1750 fn tx(hash: &str, gas_used: Option<u64>, input: &str, status: Option<bool>) -> Transaction {
1755 Transaction {
1756 hash: hash.to_string(),
1757 block_number: Some(1),
1758 timestamp: Some(1700000000),
1759 from: "0xfrom".to_string(),
1760 to: Some("0xto".to_string()),
1761 value: "0".to_string(),
1762 gas_limit: 21000,
1763 gas_used,
1764 gas_price: "20000000000".to_string(),
1765 nonce: 0,
1766 input: input.to_string(),
1767 status,
1768 }
1769 }
1770
1771 #[test]
1772 fn test_analyze_gas_usage_empty_transactions() {
1773 let txs: Vec<Transaction> = vec![];
1774 let result = super::analyze_gas_usage(&txs);
1775 assert_eq!(result.avg_gas_used, 0);
1776 assert_eq!(result.max_gas_used, 0);
1777 assert_eq!(result.min_gas_used, 0);
1778 assert_eq!(result.tx_count, 0);
1779 assert_eq!(result.failed_tx_count, 0);
1780 assert_eq!(result.wasted_gas, 0);
1781 assert!(result.gas_by_function.is_empty());
1782 }
1783
1784 #[test]
1785 fn test_analyze_gas_usage_single_tx() {
1786 let txs = vec![tx("0x1", Some(100_000), "0x", Some(true))];
1787 let result = super::analyze_gas_usage(&txs);
1788 assert_eq!(result.avg_gas_used, 100_000);
1789 assert_eq!(result.max_gas_used, 100_000);
1790 assert_eq!(result.min_gas_used, 100_000);
1791 assert_eq!(result.tx_count, 1);
1792 }
1793
1794 #[test]
1795 fn test_analyze_gas_usage_multiple_txs() {
1796 let txs = vec![
1797 tx("0x1", Some(50_000), "0xa9059cbb", Some(true)),
1798 tx("0x2", Some(150_000), "0xa9059cbb", Some(true)),
1799 tx("0x3", Some(100_000), "0xa9059cbb", Some(true)),
1800 ];
1801 let result = super::analyze_gas_usage(&txs);
1802 assert_eq!(result.avg_gas_used, 100_000); assert_eq!(result.max_gas_used, 150_000);
1804 assert_eq!(result.min_gas_used, 50_000);
1805 assert_eq!(result.tx_count, 3);
1806 }
1807
1808 #[test]
1809 fn test_analyze_gas_usage_failed_tx() {
1810 let txs = vec![
1811 tx("0x1", Some(80_000), "0x", Some(true)),
1812 tx("0x2", Some(120_000), "0x", Some(false)),
1813 ];
1814 let result = super::analyze_gas_usage(&txs);
1815 assert_eq!(result.failed_tx_count, 1);
1816 assert_eq!(result.wasted_gas, 120_000);
1817 }
1818
1819 #[test]
1820 fn test_analyze_gas_usage_gas_by_function() {
1821 let txs = vec![
1823 tx("0x1", Some(100_000), "0xa9059cbb0000", Some(true)),
1824 tx("0x2", Some(200_000), "0xa9059cbb0000", Some(true)),
1825 tx("0x3", Some(50_000), "0x095ea7b30000", Some(true)),
1826 ];
1827 let result = super::analyze_gas_usage(&txs);
1828 assert_eq!(result.gas_by_function.len(), 2);
1829 let by_sel: std::collections::HashMap<_, _> = result
1830 .gas_by_function
1831 .iter()
1832 .map(|g| (g.function.as_str(), g))
1833 .collect();
1834 let transfer = by_sel.get("0xa9059cbb").unwrap();
1835 assert_eq!(transfer.call_count, 2);
1836 assert_eq!(transfer.total_gas, 300_000);
1837 assert_eq!(transfer.avg_gas, 150_000);
1838 let approve = by_sel.get("0x095ea7b3").unwrap();
1839 assert_eq!(approve.call_count, 1);
1840 assert_eq!(approve.total_gas, 50_000);
1841 }
1842
1843 #[test]
1844 fn test_analyze_gas_usage_input_0x_transfer() {
1845 let txs = vec![tx("0x1", Some(21_000), "0x", Some(true))];
1846 let result = super::analyze_gas_usage(&txs);
1847 assert_eq!(result.gas_by_function.len(), 1);
1848 assert_eq!(result.gas_by_function[0].function, "transfer()");
1849 }
1850
1851 #[test]
1852 fn test_analyze_gas_usage_input_empty_transfer() {
1853 let txs = vec![tx("0x1", Some(21_000), "", Some(true))];
1854 let result = super::analyze_gas_usage(&txs);
1855 assert_eq!(result.gas_by_function.len(), 1);
1856 assert_eq!(result.gas_by_function[0].function, "transfer()");
1857 }
1858
1859 #[test]
1860 fn test_analyze_gas_usage_gas_used_none() {
1861 let txs = vec![tx("0x1", None, "0x", Some(true))];
1862 let result = super::analyze_gas_usage(&txs);
1863 assert_eq!(result.avg_gas_used, 0);
1864 assert_eq!(result.max_gas_used, 0);
1865 assert_eq!(result.min_gas_used, 0);
1866 }
1867
1868 #[test]
1869 fn test_analyze_gas_usage_short_input_uses_full_input_as_selector() {
1870 let txs = vec![tx("0x1", Some(50_000), "0x1234567", Some(true))];
1871 let result = super::analyze_gas_usage(&txs);
1872 assert_eq!(result.gas_by_function.len(), 1);
1873 assert_eq!(result.gas_by_function[0].function, "0x1234567");
1874 }
1875}
1876
1877#[cfg(test)]
1886pub mod mocks {
1887 use super::*;
1888 use crate::chains::dex::{DexDataSource, DexTokenData, TokenSearchResult};
1889 use async_trait::async_trait;
1890
1891 #[derive(Debug, Clone)]
1893 pub struct MockChainClient {
1894 pub chain: String,
1895 pub symbol: String,
1896 pub balance: Balance,
1897 pub transaction: Transaction,
1898 pub transactions: Vec<Transaction>,
1899 pub token_balances: Vec<TokenBalance>,
1900 pub block_number: u64,
1901 pub token_info: Option<Token>,
1902 pub token_holders: Vec<TokenHolder>,
1903 pub token_holder_count: u64,
1904 }
1905
1906 impl MockChainClient {
1907 pub fn new(chain: &str, symbol: &str) -> Self {
1909 Self {
1910 chain: chain.to_string(),
1911 symbol: symbol.to_string(),
1912 balance: Balance {
1913 raw: "1000000000000000000".to_string(),
1914 formatted: "1.0".to_string(),
1915 decimals: 18,
1916 symbol: symbol.to_string(),
1917 usd_value: Some(2500.0),
1918 },
1919 transaction: Transaction {
1920 hash: "0xmocktx".to_string(),
1921 block_number: Some(12345678),
1922 timestamp: Some(1700000000),
1923 from: "0xfrom".to_string(),
1924 to: Some("0xto".to_string()),
1925 value: "1.0".to_string(),
1926 gas_limit: 21000,
1927 gas_used: Some(21000),
1928 gas_price: "20000000000".to_string(),
1929 nonce: 42,
1930 input: "0x".to_string(),
1931 status: Some(true),
1932 },
1933 transactions: vec![],
1934 token_balances: vec![],
1935 block_number: 12345678,
1936 token_info: None,
1937 token_holders: vec![],
1938 token_holder_count: 0,
1939 }
1940 }
1941 }
1942
1943 #[async_trait]
1944 impl ChainClient for MockChainClient {
1945 fn chain_name(&self) -> &str {
1946 &self.chain
1947 }
1948
1949 fn native_token_symbol(&self) -> &str {
1950 &self.symbol
1951 }
1952
1953 async fn get_balance(&self, _address: &str) -> Result<Balance> {
1954 Ok(self.balance.clone())
1955 }
1956
1957 async fn enrich_balance_usd(&self, _balance: &mut Balance) {
1958 }
1960
1961 async fn get_transaction(&self, _hash: &str) -> Result<Transaction> {
1962 Ok(self.transaction.clone())
1963 }
1964
1965 async fn get_transactions(&self, _address: &str, _limit: u32) -> Result<Vec<Transaction>> {
1966 Ok(self.transactions.clone())
1967 }
1968
1969 async fn get_block_number(&self) -> Result<u64> {
1970 Ok(self.block_number)
1971 }
1972
1973 async fn get_token_balances(&self, _address: &str) -> Result<Vec<TokenBalance>> {
1974 Ok(self.token_balances.clone())
1975 }
1976
1977 async fn get_token_info(&self, _address: &str) -> Result<Token> {
1978 match &self.token_info {
1979 Some(t) => Ok(t.clone()),
1980 None => Err(crate::error::ScopeError::Chain(
1981 "Token info not available".to_string(),
1982 )),
1983 }
1984 }
1985
1986 async fn get_token_holders(&self, _address: &str, _limit: u32) -> Result<Vec<TokenHolder>> {
1987 Ok(self.token_holders.clone())
1988 }
1989
1990 async fn get_token_holder_count(&self, _address: &str) -> Result<u64> {
1991 Ok(self.token_holder_count)
1992 }
1993 }
1994
1995 #[derive(Debug, Clone)]
1997 pub struct MockDexSource {
1998 pub token_price: Option<f64>,
1999 pub native_price: Option<f64>,
2000 pub token_data: Option<DexTokenData>,
2001 pub search_results: Vec<TokenSearchResult>,
2002 }
2003
2004 impl Default for MockDexSource {
2005 fn default() -> Self {
2006 Self::new()
2007 }
2008 }
2009
2010 impl MockDexSource {
2011 pub fn new() -> Self {
2013 Self {
2014 token_price: Some(1.0),
2015 native_price: Some(2500.0),
2016 token_data: Some(DexTokenData {
2017 address: "0xmocktoken".to_string(),
2018 symbol: "MOCK".to_string(),
2019 name: "Mock Token".to_string(),
2020 price_usd: 1.0,
2021 price_change_24h: 5.0,
2022 price_change_6h: 2.0,
2023 price_change_1h: 0.5,
2024 price_change_5m: 0.1,
2025 volume_24h: 1_000_000.0,
2026 volume_6h: 250_000.0,
2027 volume_1h: 50_000.0,
2028 liquidity_usd: 5_000_000.0,
2029 market_cap: Some(100_000_000.0),
2030 fdv: Some(200_000_000.0),
2031 pairs: vec![],
2032 price_history: vec![],
2033 volume_history: vec![],
2034 total_buys_24h: 500,
2035 total_sells_24h: 450,
2036 total_buys_6h: 120,
2037 total_sells_6h: 110,
2038 total_buys_1h: 20,
2039 total_sells_1h: 18,
2040 earliest_pair_created_at: Some(1690000000),
2041 image_url: None,
2042 websites: vec![],
2043 socials: vec![],
2044 dexscreener_url: None,
2045 }),
2046 search_results: vec![],
2047 }
2048 }
2049 }
2050
2051 #[async_trait]
2052 impl DexDataSource for MockDexSource {
2053 async fn get_token_price(&self, _chain: &str, _address: &str) -> Option<f64> {
2054 self.token_price
2055 }
2056
2057 async fn get_native_token_price(&self, _chain: &str) -> Option<f64> {
2058 self.native_price
2059 }
2060
2061 async fn get_token_data(&self, _chain: &str, _address: &str) -> Result<DexTokenData> {
2062 match &self.token_data {
2063 Some(data) => Ok(data.clone()),
2064 None => Err(crate::error::ScopeError::NotFound(
2065 "No DEX data found".to_string(),
2066 )),
2067 }
2068 }
2069
2070 async fn search_tokens(
2071 &self,
2072 _query: &str,
2073 _chain: Option<&str>,
2074 ) -> Result<Vec<TokenSearchResult>> {
2075 Ok(self.search_results.clone())
2076 }
2077 }
2078
2079 pub struct MockClientFactory {
2081 pub mock_client: MockChainClient,
2082 pub mock_dex: MockDexSource,
2083 }
2084
2085 impl Default for MockClientFactory {
2086 fn default() -> Self {
2087 Self::new()
2088 }
2089 }
2090
2091 impl MockClientFactory {
2092 pub fn new() -> Self {
2094 Self {
2095 mock_client: MockChainClient::new("ethereum", "ETH"),
2096 mock_dex: MockDexSource::new(),
2097 }
2098 }
2099 }
2100
2101 impl ChainClientFactory for MockClientFactory {
2102 fn create_chain_client(&self, _chain: &str) -> Result<Box<dyn ChainClient>> {
2103 Ok(Box::new(self.mock_client.clone()))
2104 }
2105
2106 fn create_dex_client(&self) -> Box<dyn DexDataSource> {
2107 Box::new(self.mock_dex.clone())
2108 }
2109 }
2110}