1pub mod dex;
90pub mod ethereum;
91pub mod solana;
92pub mod tron;
93
94pub use dex::{DexClient, DexDataSource, TokenSearchResult};
95pub use ethereum::{ApiType, EthereumClient};
96pub use solana::{SolanaClient, validate_solana_address, validate_solana_signature};
97pub use tron::{TronClient, validate_tron_address, validate_tron_tx_hash};
98
99use crate::error::Result;
100use async_trait::async_trait;
101use serde::{Deserialize, Serialize};
102
103#[async_trait]
121pub trait ChainClient: Send + Sync {
122 fn chain_name(&self) -> &str;
124
125 fn native_token_symbol(&self) -> &str;
127
128 async fn get_balance(&self, address: &str) -> Result<Balance>;
138
139 async fn enrich_balance_usd(&self, balance: &mut Balance);
145
146 async fn get_transaction(&self, hash: &str) -> Result<Transaction>;
156
157 async fn get_transactions(&self, address: &str, limit: u32) -> Result<Vec<Transaction>>;
168
169 async fn get_block_number(&self) -> Result<u64>;
171
172 async fn get_token_balances(&self, address: &str) -> Result<Vec<TokenBalance>>;
177
178 async fn get_token_info(&self, _address: &str) -> Result<Token> {
183 Err(crate::error::ScopeError::Chain(
184 "Token info lookup not supported on this chain".to_string(),
185 ))
186 }
187
188 async fn get_token_holders(&self, _address: &str, _limit: u32) -> Result<Vec<TokenHolder>> {
193 Ok(Vec::new())
194 }
195
196 async fn get_token_holder_count(&self, _address: &str) -> Result<u64> {
201 Ok(0)
202 }
203}
204
205pub trait ChainClientFactory: Send + Sync {
221 fn create_chain_client(&self, chain: &str) -> Result<Box<dyn ChainClient>>;
227
228 fn create_dex_client(&self) -> Box<dyn DexDataSource>;
230}
231
232pub struct DefaultClientFactory {
234 pub chains_config: crate::config::ChainsConfig,
236}
237
238impl ChainClientFactory for DefaultClientFactory {
239 fn create_chain_client(&self, chain: &str) -> Result<Box<dyn ChainClient>> {
240 match chain.to_lowercase().as_str() {
241 "solana" | "sol" => Ok(Box::new(SolanaClient::new(&self.chains_config)?)),
242 "tron" | "trx" => Ok(Box::new(TronClient::new(&self.chains_config)?)),
243 _ => Ok(Box::new(EthereumClient::for_chain(
244 chain,
245 &self.chains_config,
246 )?)),
247 }
248 }
249
250 fn create_dex_client(&self) -> Box<dyn DexDataSource> {
251 Box::new(DexClient::new())
252 }
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct Balance {
258 pub raw: String,
260
261 pub formatted: String,
263
264 pub decimals: u8,
266
267 pub symbol: String,
269
270 #[serde(skip_serializing_if = "Option::is_none")]
272 pub usd_value: Option<f64>,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct Transaction {
278 pub hash: String,
280
281 pub block_number: Option<u64>,
283
284 pub timestamp: Option<u64>,
286
287 pub from: String,
289
290 pub to: Option<String>,
292
293 pub value: String,
295
296 pub gas_limit: u64,
298
299 pub gas_used: Option<u64>,
301
302 pub gas_price: String,
304
305 pub nonce: u64,
307
308 pub input: String,
310
311 pub status: Option<bool>,
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct Token {
318 pub contract_address: String,
320
321 pub symbol: String,
323
324 pub name: String,
326
327 pub decimals: u8,
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct TokenBalance {
334 pub token: Token,
336
337 pub balance: String,
339
340 pub formatted_balance: String,
342
343 #[serde(skip_serializing_if = "Option::is_none")]
345 pub usd_value: Option<f64>,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
354pub struct TokenHolder {
355 pub address: String,
357
358 pub balance: String,
360
361 pub formatted_balance: String,
363
364 pub percentage: f64,
366
367 pub rank: u32,
369}
370
371#[derive(Debug, Clone, Serialize, Deserialize)]
373pub struct PricePoint {
374 pub timestamp: i64,
376
377 pub price: f64,
379}
380
381#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct VolumePoint {
384 pub timestamp: i64,
386
387 pub volume: f64,
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct HolderCountPoint {
394 pub timestamp: i64,
396
397 pub count: u64,
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct DexPair {
404 pub dex_name: String,
406
407 pub pair_address: String,
409
410 pub base_token: String,
412
413 pub quote_token: String,
415
416 pub price_usd: f64,
418
419 pub volume_24h: f64,
421
422 pub liquidity_usd: f64,
424
425 pub price_change_24h: f64,
427
428 pub buys_24h: u64,
430
431 pub sells_24h: u64,
433
434 pub buys_6h: u64,
436
437 pub sells_6h: u64,
439
440 pub buys_1h: u64,
442
443 pub sells_1h: u64,
445
446 pub pair_created_at: Option<i64>,
448
449 pub url: Option<String>,
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct TokenAnalytics {
456 pub token: Token,
458
459 pub chain: String,
461
462 pub holders: Vec<TokenHolder>,
464
465 pub total_holders: u64,
467
468 pub volume_24h: f64,
470
471 pub volume_7d: f64,
473
474 pub price_usd: f64,
476
477 pub price_change_24h: f64,
479
480 pub price_change_7d: f64,
482
483 pub liquidity_usd: f64,
485
486 #[serde(skip_serializing_if = "Option::is_none")]
488 pub market_cap: Option<f64>,
489
490 #[serde(skip_serializing_if = "Option::is_none")]
492 pub fdv: Option<f64>,
493
494 #[serde(skip_serializing_if = "Option::is_none")]
496 pub total_supply: Option<String>,
497
498 #[serde(skip_serializing_if = "Option::is_none")]
500 pub circulating_supply: Option<String>,
501
502 pub price_history: Vec<PricePoint>,
504
505 pub volume_history: Vec<VolumePoint>,
507
508 pub holder_history: Vec<HolderCountPoint>,
510
511 pub dex_pairs: Vec<DexPair>,
513
514 pub fetched_at: i64,
516
517 #[serde(skip_serializing_if = "Option::is_none")]
519 pub top_10_concentration: Option<f64>,
520
521 #[serde(skip_serializing_if = "Option::is_none")]
523 pub top_50_concentration: Option<f64>,
524
525 #[serde(skip_serializing_if = "Option::is_none")]
527 pub top_100_concentration: Option<f64>,
528
529 pub price_change_6h: f64,
531
532 pub price_change_1h: f64,
534
535 pub total_buys_24h: u64,
537
538 pub total_sells_24h: u64,
540
541 pub total_buys_6h: u64,
543
544 pub total_sells_6h: u64,
546
547 pub total_buys_1h: u64,
549
550 pub total_sells_1h: u64,
552
553 #[serde(skip_serializing_if = "Option::is_none")]
555 pub token_age_hours: Option<f64>,
556
557 #[serde(skip_serializing_if = "Option::is_none")]
559 pub image_url: Option<String>,
560
561 pub websites: Vec<String>,
563
564 pub socials: Vec<TokenSocial>,
566
567 #[serde(skip_serializing_if = "Option::is_none")]
569 pub dexscreener_url: Option<String>,
570}
571
572#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
574pub struct TokenSocial {
575 pub platform: String,
577 pub url: String,
579}
580
581pub fn infer_chain_from_address(address: &str) -> Option<&'static str> {
607 if address.starts_with('T') && address.len() == 34 && bs58::decode(address).into_vec().is_ok() {
609 return Some("tron");
610 }
611
612 if address.starts_with("0x")
614 && address.len() == 42
615 && address[2..].chars().all(|c| c.is_ascii_hexdigit())
616 {
617 return Some("ethereum");
618 }
619
620 if address.len() >= 32
622 && address.len() <= 44
623 && let Ok(decoded) = bs58::decode(address).into_vec()
624 && decoded.len() == 32
625 {
626 return Some("solana");
627 }
628
629 None
630}
631
632pub fn infer_chain_from_hash(hash: &str) -> Option<&'static str> {
657 if hash.starts_with("0x")
659 && hash.len() == 66
660 && hash[2..].chars().all(|c| c.is_ascii_hexdigit())
661 {
662 return Some("ethereum");
663 }
664
665 if hash.len() == 64 && hash.chars().all(|c| c.is_ascii_hexdigit()) {
667 return Some("tron");
668 }
669
670 if hash.len() >= 80
672 && hash.len() <= 90
673 && let Ok(decoded) = bs58::decode(hash).into_vec()
674 && decoded.len() == 64
675 {
676 return Some("solana");
677 }
678
679 None
680}
681
682#[cfg(test)]
687mod tests {
688 use super::*;
689
690 #[test]
691 fn test_balance_serialization() {
692 let balance = Balance {
693 raw: "1000000000000000000".to_string(),
694 formatted: "1.0".to_string(),
695 decimals: 18,
696 symbol: "ETH".to_string(),
697 usd_value: Some(3500.0),
698 };
699
700 let json = serde_json::to_string(&balance).unwrap();
701 assert!(json.contains("1000000000000000000"));
702 assert!(json.contains("1.0"));
703 assert!(json.contains("ETH"));
704 assert!(json.contains("3500"));
705
706 let deserialized: Balance = serde_json::from_str(&json).unwrap();
707 assert_eq!(deserialized.raw, balance.raw);
708 assert_eq!(deserialized.decimals, 18);
709 }
710
711 #[test]
712 fn test_balance_without_usd() {
713 let balance = Balance {
714 raw: "1000000000000000000".to_string(),
715 formatted: "1.0".to_string(),
716 decimals: 18,
717 symbol: "ETH".to_string(),
718 usd_value: None,
719 };
720
721 let json = serde_json::to_string(&balance).unwrap();
722 assert!(!json.contains("usd_value"));
723 }
724
725 #[test]
726 fn test_transaction_serialization() {
727 let tx = Transaction {
728 hash: "0xabc123".to_string(),
729 block_number: Some(12345678),
730 timestamp: Some(1700000000),
731 from: "0xfrom".to_string(),
732 to: Some("0xto".to_string()),
733 value: "1.0".to_string(),
734 gas_limit: 21000,
735 gas_used: Some(21000),
736 gas_price: "20000000000".to_string(),
737 nonce: 42,
738 input: "0x".to_string(),
739 status: Some(true),
740 };
741
742 let json = serde_json::to_string(&tx).unwrap();
743 assert!(json.contains("0xabc123"));
744 assert!(json.contains("12345678"));
745 assert!(json.contains("0xfrom"));
746 assert!(json.contains("0xto"));
747
748 let deserialized: Transaction = serde_json::from_str(&json).unwrap();
749 assert_eq!(deserialized.hash, tx.hash);
750 assert_eq!(deserialized.nonce, 42);
751 }
752
753 #[test]
754 fn test_pending_transaction_serialization() {
755 let tx = Transaction {
756 hash: "0xpending".to_string(),
757 block_number: None,
758 timestamp: None,
759 from: "0xfrom".to_string(),
760 to: Some("0xto".to_string()),
761 value: "1.0".to_string(),
762 gas_limit: 21000,
763 gas_used: None,
764 gas_price: "20000000000".to_string(),
765 nonce: 0,
766 input: "0x".to_string(),
767 status: None,
768 };
769
770 let json = serde_json::to_string(&tx).unwrap();
771 assert!(json.contains("0xpending"));
772 assert!(json.contains("null")); let deserialized: Transaction = serde_json::from_str(&json).unwrap();
775 assert!(deserialized.block_number.is_none());
776 assert!(deserialized.status.is_none());
777 }
778
779 #[test]
780 fn test_contract_creation_transaction() {
781 let tx = Transaction {
782 hash: "0xcreate".to_string(),
783 block_number: Some(100),
784 timestamp: Some(1700000000),
785 from: "0xdeployer".to_string(),
786 to: None, value: "0".to_string(),
788 gas_limit: 1000000,
789 gas_used: Some(500000),
790 gas_price: "20000000000".to_string(),
791 nonce: 0,
792 input: "0x608060...".to_string(),
793 status: Some(true),
794 };
795
796 let json = serde_json::to_string(&tx).unwrap();
797 assert!(json.contains("\"to\":null"));
798 }
799
800 #[test]
801 fn test_token_serialization() {
802 let token = Token {
803 contract_address: "0xtoken".to_string(),
804 symbol: "USDC".to_string(),
805 name: "USD Coin".to_string(),
806 decimals: 6,
807 };
808
809 let json = serde_json::to_string(&token).unwrap();
810 assert!(json.contains("USDC"));
811 assert!(json.contains("USD Coin"));
812 assert!(json.contains("\"decimals\":6"));
813
814 let deserialized: Token = serde_json::from_str(&json).unwrap();
815 assert_eq!(deserialized.decimals, 6);
816 }
817
818 #[test]
819 fn test_token_balance_serialization() {
820 let token_balance = TokenBalance {
821 token: Token {
822 contract_address: "0xtoken".to_string(),
823 symbol: "USDC".to_string(),
824 name: "USD Coin".to_string(),
825 decimals: 6,
826 },
827 balance: "1000000".to_string(),
828 formatted_balance: "1.0".to_string(),
829 usd_value: Some(1.0),
830 };
831
832 let json = serde_json::to_string(&token_balance).unwrap();
833 assert!(json.contains("USDC"));
834 assert!(json.contains("1000000"));
835 assert!(json.contains("1.0"));
836 }
837
838 #[test]
839 fn test_balance_debug() {
840 let balance = Balance {
841 raw: "1000".to_string(),
842 formatted: "0.001".to_string(),
843 decimals: 18,
844 symbol: "ETH".to_string(),
845 usd_value: None,
846 };
847
848 let debug_str = format!("{:?}", balance);
849 assert!(debug_str.contains("Balance"));
850 assert!(debug_str.contains("1000"));
851 }
852
853 #[test]
854 fn test_transaction_debug() {
855 let tx = Transaction {
856 hash: "0xtest".to_string(),
857 block_number: Some(1),
858 timestamp: Some(0),
859 from: "0x1".to_string(),
860 to: Some("0x2".to_string()),
861 value: "0".to_string(),
862 gas_limit: 21000,
863 gas_used: Some(21000),
864 gas_price: "0".to_string(),
865 nonce: 0,
866 input: "0x".to_string(),
867 status: Some(true),
868 };
869
870 let debug_str = format!("{:?}", tx);
871 assert!(debug_str.contains("Transaction"));
872 assert!(debug_str.contains("0xtest"));
873 }
874
875 #[test]
880 fn test_infer_chain_from_address_evm() {
881 assert_eq!(
883 super::infer_chain_from_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2"),
884 Some("ethereum")
885 );
886 assert_eq!(
887 super::infer_chain_from_address("0x0000000000000000000000000000000000000000"),
888 Some("ethereum")
889 );
890 assert_eq!(
891 super::infer_chain_from_address("0xABCDEF1234567890abcdef1234567890ABCDEF12"),
892 Some("ethereum")
893 );
894 }
895
896 #[test]
897 fn test_infer_chain_from_address_tron() {
898 assert_eq!(
900 super::infer_chain_from_address("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf"),
901 Some("tron")
902 );
903 assert_eq!(
904 super::infer_chain_from_address("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"),
905 Some("tron")
906 );
907 }
908
909 #[test]
910 fn test_infer_chain_from_address_solana() {
911 assert_eq!(
913 super::infer_chain_from_address("DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy"),
914 Some("solana")
915 );
916 assert_eq!(
918 super::infer_chain_from_address("11111111111111111111111111111111"),
919 Some("solana")
920 );
921 }
922
923 #[test]
924 fn test_infer_chain_from_address_invalid() {
925 assert_eq!(super::infer_chain_from_address("0x123"), None);
927 assert_eq!(super::infer_chain_from_address("not_an_address"), None);
929 assert_eq!(super::infer_chain_from_address(""), None);
931 assert_eq!(super::infer_chain_from_address("0x123456"), None);
933 assert_eq!(
935 super::infer_chain_from_address("ADqSquXBgUCLYvYC4XZgrprLK589dkhSCf"),
936 None
937 );
938 }
939
940 #[test]
941 fn test_infer_chain_from_hash_evm() {
942 assert_eq!(
944 super::infer_chain_from_hash(
945 "0xabc123def456789012345678901234567890123456789012345678901234abcd"
946 ),
947 Some("ethereum")
948 );
949 assert_eq!(
950 super::infer_chain_from_hash(
951 "0x0000000000000000000000000000000000000000000000000000000000000000"
952 ),
953 Some("ethereum")
954 );
955 }
956
957 #[test]
958 fn test_infer_chain_from_hash_tron() {
959 assert_eq!(
961 super::infer_chain_from_hash(
962 "abc123def456789012345678901234567890123456789012345678901234abcd"
963 ),
964 Some("tron")
965 );
966 assert_eq!(
967 super::infer_chain_from_hash(
968 "0000000000000000000000000000000000000000000000000000000000000000"
969 ),
970 Some("tron")
971 );
972 }
973
974 #[test]
975 fn test_infer_chain_from_hash_solana() {
976 let solana_sig = "5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW";
979 assert_eq!(super::infer_chain_from_hash(solana_sig), Some("solana"));
980 }
981
982 #[test]
983 fn test_infer_chain_from_hash_invalid() {
984 assert_eq!(super::infer_chain_from_hash("0x123"), None);
986 assert_eq!(super::infer_chain_from_hash("not_a_hash"), None);
988 assert_eq!(super::infer_chain_from_hash(""), None);
990 assert_eq!(
992 super::infer_chain_from_hash(
993 "abc123gef456789012345678901234567890123456789012345678901234abcd"
994 ),
995 None
996 );
997 }
998
999 #[test]
1004 fn test_default_client_factory_create_dex_client() {
1005 let config = crate::config::ChainsConfig::default();
1006 let factory = DefaultClientFactory {
1007 chains_config: config,
1008 };
1009 let dex = factory.create_dex_client();
1010 let _ = format!("{:?}", std::mem::size_of_val(&dex));
1012 }
1013
1014 #[test]
1015 fn test_default_client_factory_create_ethereum_client() {
1016 let config = crate::config::ChainsConfig::default();
1017 let factory = DefaultClientFactory {
1018 chains_config: config,
1019 };
1020 let client = factory.create_chain_client("ethereum");
1022 assert!(client.is_ok());
1023 assert_eq!(client.unwrap().chain_name(), "ethereum");
1024 }
1025
1026 #[test]
1027 fn test_default_client_factory_create_polygon_client() {
1028 let config = crate::config::ChainsConfig::default();
1029 let factory = DefaultClientFactory {
1030 chains_config: config,
1031 };
1032 let client = factory.create_chain_client("polygon");
1033 assert!(client.is_ok());
1034 assert_eq!(client.unwrap().chain_name(), "polygon");
1035 }
1036
1037 #[test]
1038 fn test_default_client_factory_create_solana_client() {
1039 let config = crate::config::ChainsConfig::default();
1040 let factory = DefaultClientFactory {
1041 chains_config: config,
1042 };
1043 let client = factory.create_chain_client("solana");
1044 assert!(client.is_ok());
1045 assert_eq!(client.unwrap().chain_name(), "solana");
1046 }
1047
1048 #[test]
1049 fn test_default_client_factory_create_sol_alias() {
1050 let config = crate::config::ChainsConfig::default();
1051 let factory = DefaultClientFactory {
1052 chains_config: config,
1053 };
1054 let client = factory.create_chain_client("sol");
1055 assert!(client.is_ok());
1056 assert_eq!(client.unwrap().chain_name(), "solana");
1057 }
1058
1059 #[test]
1060 fn test_default_client_factory_create_tron_client() {
1061 let config = crate::config::ChainsConfig::default();
1062 let factory = DefaultClientFactory {
1063 chains_config: config,
1064 };
1065 let client = factory.create_chain_client("tron");
1066 assert!(client.is_ok());
1067 assert_eq!(client.unwrap().chain_name(), "tron");
1068 }
1069
1070 #[test]
1071 fn test_default_client_factory_create_trx_alias() {
1072 let config = crate::config::ChainsConfig::default();
1073 let factory = DefaultClientFactory {
1074 chains_config: config,
1075 };
1076 let client = factory.create_chain_client("trx");
1077 assert!(client.is_ok());
1078 assert_eq!(client.unwrap().chain_name(), "tron");
1079 }
1080
1081 #[tokio::test]
1086 async fn test_chain_client_default_get_token_info() {
1087 use super::mocks::MockChainClient;
1088 let client = MockChainClient::new("ethereum", "ETH");
1090 let result = client.get_token_info("0xsometoken").await;
1091 assert!(result.is_err());
1092 }
1093
1094 #[tokio::test]
1095 async fn test_chain_client_default_get_token_holders() {
1096 use super::mocks::MockChainClient;
1097 let client = MockChainClient::new("ethereum", "ETH");
1098 let holders = client.get_token_holders("0xsometoken", 10).await.unwrap();
1099 assert!(holders.is_empty());
1100 }
1101
1102 #[tokio::test]
1103 async fn test_chain_client_default_get_token_holder_count() {
1104 use super::mocks::MockChainClient;
1105 let client = MockChainClient::new("ethereum", "ETH");
1106 let count = client.get_token_holder_count("0xsometoken").await.unwrap();
1107 assert_eq!(count, 0);
1108 }
1109
1110 #[tokio::test]
1111 async fn test_mock_client_factory_creates_chain_client() {
1112 use super::mocks::MockClientFactory;
1113 let factory = MockClientFactory::new();
1114 let client = factory.create_chain_client("anything").unwrap();
1115 assert_eq!(client.chain_name(), "ethereum"); }
1117
1118 #[tokio::test]
1119 async fn test_mock_client_factory_creates_dex_client() {
1120 use super::mocks::MockClientFactory;
1121 let factory = MockClientFactory::new();
1122 let dex = factory.create_dex_client();
1123 let price = dex.get_token_price("ethereum", "0xtest").await;
1124 assert_eq!(price, Some(1.0));
1125 }
1126
1127 #[tokio::test]
1128 async fn test_mock_chain_client_balance() {
1129 use super::mocks::MockChainClient;
1130 let client = MockChainClient::new("ethereum", "ETH");
1131 let balance = client.get_balance("0xtest").await.unwrap();
1132 assert_eq!(balance.formatted, "1.0");
1133 assert_eq!(balance.symbol, "ETH");
1134 assert_eq!(balance.usd_value, Some(2500.0));
1135 }
1136
1137 #[tokio::test]
1138 async fn test_mock_chain_client_transaction() {
1139 use super::mocks::MockChainClient;
1140 let client = MockChainClient::new("ethereum", "ETH");
1141 let tx = client.get_transaction("0xanyhash").await.unwrap();
1142 assert_eq!(tx.hash, "0xmocktx");
1143 assert_eq!(tx.nonce, 42);
1144 }
1145
1146 #[tokio::test]
1147 async fn test_mock_chain_client_block_number() {
1148 use super::mocks::MockChainClient;
1149 let client = MockChainClient::new("ethereum", "ETH");
1150 let block = client.get_block_number().await.unwrap();
1151 assert_eq!(block, 12345678);
1152 }
1153
1154 #[tokio::test]
1155 async fn test_mock_dex_source_data() {
1156 use super::mocks::MockDexSource;
1157 let dex = MockDexSource::new();
1158 let data = dex.get_token_data("ethereum", "0xtest").await.unwrap();
1159 assert_eq!(data.symbol, "MOCK");
1160 assert_eq!(data.price_usd, 1.0);
1161 }
1162
1163 #[tokio::test]
1164 async fn test_mock_dex_source_search() {
1165 use super::mocks::MockDexSource;
1166 let dex = MockDexSource::new();
1167 let results = dex.search_tokens("test", None).await.unwrap();
1168 assert!(results.is_empty());
1169 }
1170
1171 #[tokio::test]
1172 async fn test_mock_dex_source_native_price() {
1173 use super::mocks::MockDexSource;
1174 let dex = MockDexSource::new();
1175 let price = dex.get_native_token_price("ethereum").await;
1176 assert_eq!(price, Some(2500.0));
1177 }
1178
1179 struct MinimalChainClient;
1185
1186 #[async_trait::async_trait]
1187 impl ChainClient for MinimalChainClient {
1188 fn chain_name(&self) -> &str {
1189 "test"
1190 }
1191
1192 fn native_token_symbol(&self) -> &str {
1193 "TEST"
1194 }
1195
1196 async fn get_balance(&self, _address: &str) -> Result<Balance> {
1197 Ok(Balance {
1198 raw: "0".to_string(),
1199 formatted: "0".to_string(),
1200 decimals: 18,
1201 symbol: "TEST".to_string(),
1202 usd_value: None,
1203 })
1204 }
1205
1206 async fn get_transaction(&self, _hash: &str) -> Result<Transaction> {
1207 unimplemented!()
1208 }
1209
1210 async fn get_transactions(&self, _address: &str, _limit: u32) -> Result<Vec<Transaction>> {
1211 Ok(Vec::new())
1212 }
1213
1214 async fn get_block_number(&self) -> Result<u64> {
1215 Ok(0)
1216 }
1217
1218 async fn get_token_balances(&self, _address: &str) -> Result<Vec<TokenBalance>> {
1219 Ok(Vec::new())
1220 }
1221
1222 async fn enrich_balance_usd(&self, _balance: &mut Balance) {}
1223 }
1224
1225 #[tokio::test]
1226 async fn test_default_get_token_info() {
1227 let client = MinimalChainClient;
1228 let result = client.get_token_info("0xtest").await;
1229 assert!(result.is_err());
1230 assert!(result.unwrap_err().to_string().contains("not supported"));
1231 }
1232
1233 #[tokio::test]
1234 async fn test_default_get_token_holders() {
1235 let client = MinimalChainClient;
1236 let holders = client.get_token_holders("0xtest", 10).await.unwrap();
1237 assert!(holders.is_empty());
1238 }
1239
1240 #[tokio::test]
1241 async fn test_default_get_token_holder_count() {
1242 let client = MinimalChainClient;
1243 let count = client.get_token_holder_count("0xtest").await.unwrap();
1244 assert_eq!(count, 0);
1245 }
1246}
1247
1248#[cfg(test)]
1257pub mod mocks {
1258 use super::*;
1259 use crate::chains::dex::{DexDataSource, DexTokenData, TokenSearchResult};
1260 use async_trait::async_trait;
1261
1262 #[derive(Debug, Clone)]
1264 pub struct MockChainClient {
1265 pub chain: String,
1266 pub symbol: String,
1267 pub balance: Balance,
1268 pub transaction: Transaction,
1269 pub transactions: Vec<Transaction>,
1270 pub token_balances: Vec<TokenBalance>,
1271 pub block_number: u64,
1272 pub token_info: Option<Token>,
1273 pub token_holders: Vec<TokenHolder>,
1274 pub token_holder_count: u64,
1275 }
1276
1277 impl MockChainClient {
1278 pub fn new(chain: &str, symbol: &str) -> Self {
1280 Self {
1281 chain: chain.to_string(),
1282 symbol: symbol.to_string(),
1283 balance: Balance {
1284 raw: "1000000000000000000".to_string(),
1285 formatted: "1.0".to_string(),
1286 decimals: 18,
1287 symbol: symbol.to_string(),
1288 usd_value: Some(2500.0),
1289 },
1290 transaction: Transaction {
1291 hash: "0xmocktx".to_string(),
1292 block_number: Some(12345678),
1293 timestamp: Some(1700000000),
1294 from: "0xfrom".to_string(),
1295 to: Some("0xto".to_string()),
1296 value: "1.0".to_string(),
1297 gas_limit: 21000,
1298 gas_used: Some(21000),
1299 gas_price: "20000000000".to_string(),
1300 nonce: 42,
1301 input: "0x".to_string(),
1302 status: Some(true),
1303 },
1304 transactions: vec![],
1305 token_balances: vec![],
1306 block_number: 12345678,
1307 token_info: None,
1308 token_holders: vec![],
1309 token_holder_count: 0,
1310 }
1311 }
1312 }
1313
1314 #[async_trait]
1315 impl ChainClient for MockChainClient {
1316 fn chain_name(&self) -> &str {
1317 &self.chain
1318 }
1319
1320 fn native_token_symbol(&self) -> &str {
1321 &self.symbol
1322 }
1323
1324 async fn get_balance(&self, _address: &str) -> Result<Balance> {
1325 Ok(self.balance.clone())
1326 }
1327
1328 async fn enrich_balance_usd(&self, _balance: &mut Balance) {
1329 }
1331
1332 async fn get_transaction(&self, _hash: &str) -> Result<Transaction> {
1333 Ok(self.transaction.clone())
1334 }
1335
1336 async fn get_transactions(&self, _address: &str, _limit: u32) -> Result<Vec<Transaction>> {
1337 Ok(self.transactions.clone())
1338 }
1339
1340 async fn get_block_number(&self) -> Result<u64> {
1341 Ok(self.block_number)
1342 }
1343
1344 async fn get_token_balances(&self, _address: &str) -> Result<Vec<TokenBalance>> {
1345 Ok(self.token_balances.clone())
1346 }
1347
1348 async fn get_token_info(&self, _address: &str) -> Result<Token> {
1349 match &self.token_info {
1350 Some(t) => Ok(t.clone()),
1351 None => Err(crate::error::ScopeError::Chain(
1352 "Token info not available".to_string(),
1353 )),
1354 }
1355 }
1356
1357 async fn get_token_holders(&self, _address: &str, _limit: u32) -> Result<Vec<TokenHolder>> {
1358 Ok(self.token_holders.clone())
1359 }
1360
1361 async fn get_token_holder_count(&self, _address: &str) -> Result<u64> {
1362 Ok(self.token_holder_count)
1363 }
1364 }
1365
1366 #[derive(Debug, Clone)]
1368 pub struct MockDexSource {
1369 pub token_price: Option<f64>,
1370 pub native_price: Option<f64>,
1371 pub token_data: Option<DexTokenData>,
1372 pub search_results: Vec<TokenSearchResult>,
1373 }
1374
1375 impl Default for MockDexSource {
1376 fn default() -> Self {
1377 Self::new()
1378 }
1379 }
1380
1381 impl MockDexSource {
1382 pub fn new() -> Self {
1384 Self {
1385 token_price: Some(1.0),
1386 native_price: Some(2500.0),
1387 token_data: Some(DexTokenData {
1388 address: "0xmocktoken".to_string(),
1389 symbol: "MOCK".to_string(),
1390 name: "Mock Token".to_string(),
1391 price_usd: 1.0,
1392 price_change_24h: 5.0,
1393 price_change_6h: 2.0,
1394 price_change_1h: 0.5,
1395 price_change_5m: 0.1,
1396 volume_24h: 1_000_000.0,
1397 volume_6h: 250_000.0,
1398 volume_1h: 50_000.0,
1399 liquidity_usd: 5_000_000.0,
1400 market_cap: Some(100_000_000.0),
1401 fdv: Some(200_000_000.0),
1402 pairs: vec![],
1403 price_history: vec![],
1404 volume_history: vec![],
1405 total_buys_24h: 500,
1406 total_sells_24h: 450,
1407 total_buys_6h: 120,
1408 total_sells_6h: 110,
1409 total_buys_1h: 20,
1410 total_sells_1h: 18,
1411 earliest_pair_created_at: Some(1690000000),
1412 image_url: None,
1413 websites: vec![],
1414 socials: vec![],
1415 dexscreener_url: None,
1416 }),
1417 search_results: vec![],
1418 }
1419 }
1420 }
1421
1422 #[async_trait]
1423 impl DexDataSource for MockDexSource {
1424 async fn get_token_price(&self, _chain: &str, _address: &str) -> Option<f64> {
1425 self.token_price
1426 }
1427
1428 async fn get_native_token_price(&self, _chain: &str) -> Option<f64> {
1429 self.native_price
1430 }
1431
1432 async fn get_token_data(&self, _chain: &str, _address: &str) -> Result<DexTokenData> {
1433 match &self.token_data {
1434 Some(data) => Ok(data.clone()),
1435 None => Err(crate::error::ScopeError::NotFound(
1436 "No DEX data found".to_string(),
1437 )),
1438 }
1439 }
1440
1441 async fn search_tokens(
1442 &self,
1443 _query: &str,
1444 _chain: Option<&str>,
1445 ) -> Result<Vec<TokenSearchResult>> {
1446 Ok(self.search_results.clone())
1447 }
1448 }
1449
1450 pub struct MockClientFactory {
1452 pub mock_client: MockChainClient,
1453 pub mock_dex: MockDexSource,
1454 }
1455
1456 impl Default for MockClientFactory {
1457 fn default() -> Self {
1458 Self::new()
1459 }
1460 }
1461
1462 impl MockClientFactory {
1463 pub fn new() -> Self {
1465 Self {
1466 mock_client: MockChainClient::new("ethereum", "ETH"),
1467 mock_dex: MockDexSource::new(),
1468 }
1469 }
1470 }
1471
1472 impl ChainClientFactory for MockClientFactory {
1473 fn create_chain_client(&self, _chain: &str) -> Result<Box<dyn ChainClient>> {
1474 Ok(Box::new(self.mock_client.clone()))
1475 }
1476
1477 fn create_dex_client(&self) -> Box<dyn DexDataSource> {
1478 Box::new(self.mock_dex.clone())
1479 }
1480 }
1481}