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
204pub trait ChainClientFactory: Send + Sync {
220 fn create_chain_client(&self, chain: &str) -> Result<Box<dyn ChainClient>>;
226
227 fn create_dex_client(&self) -> Box<dyn DexDataSource>;
229}
230
231pub struct DefaultClientFactory {
233 pub chains_config: crate::config::ChainsConfig,
235}
236
237impl ChainClientFactory for DefaultClientFactory {
238 fn create_chain_client(&self, chain: &str) -> Result<Box<dyn ChainClient>> {
239 match chain.to_lowercase().as_str() {
240 "solana" | "sol" => Ok(Box::new(SolanaClient::new(&self.chains_config)?)),
241 "tron" | "trx" => Ok(Box::new(TronClient::new(&self.chains_config)?)),
242 _ => Ok(Box::new(EthereumClient::for_chain(
243 chain,
244 &self.chains_config,
245 )?)),
246 }
247 }
248
249 fn create_dex_client(&self) -> Box<dyn DexDataSource> {
250 Box::new(DexClient::new())
251 }
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct Balance {
257 pub raw: String,
259
260 pub formatted: String,
262
263 pub decimals: u8,
265
266 pub symbol: String,
268
269 #[serde(skip_serializing_if = "Option::is_none")]
271 pub usd_value: Option<f64>,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct Transaction {
277 pub hash: String,
279
280 pub block_number: Option<u64>,
282
283 pub timestamp: Option<u64>,
285
286 pub from: String,
288
289 pub to: Option<String>,
291
292 pub value: String,
294
295 pub gas_limit: u64,
297
298 pub gas_used: Option<u64>,
300
301 pub gas_price: String,
303
304 pub nonce: u64,
306
307 pub input: String,
309
310 pub status: Option<bool>,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct Token {
317 pub contract_address: String,
319
320 pub symbol: String,
322
323 pub name: String,
325
326 pub decimals: u8,
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct TokenBalance {
333 pub token: Token,
335
336 pub balance: String,
338
339 pub formatted_balance: String,
341
342 #[serde(skip_serializing_if = "Option::is_none")]
344 pub usd_value: Option<f64>,
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct TokenHolder {
354 pub address: String,
356
357 pub balance: String,
359
360 pub formatted_balance: String,
362
363 pub percentage: f64,
365
366 pub rank: u32,
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct PricePoint {
373 pub timestamp: i64,
375
376 pub price: f64,
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct VolumePoint {
383 pub timestamp: i64,
385
386 pub volume: f64,
388}
389
390#[derive(Debug, Clone, Serialize, Deserialize)]
392pub struct HolderCountPoint {
393 pub timestamp: i64,
395
396 pub count: u64,
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct DexPair {
403 pub dex_name: String,
405
406 pub pair_address: String,
408
409 pub base_token: String,
411
412 pub quote_token: String,
414
415 pub price_usd: f64,
417
418 pub volume_24h: f64,
420
421 pub liquidity_usd: f64,
423
424 pub price_change_24h: f64,
426
427 pub buys_24h: u64,
429
430 pub sells_24h: u64,
432
433 pub buys_6h: u64,
435
436 pub sells_6h: u64,
438
439 pub buys_1h: u64,
441
442 pub sells_1h: u64,
444
445 pub pair_created_at: Option<i64>,
447
448 pub url: Option<String>,
450}
451
452#[derive(Debug, Clone, Serialize, Deserialize)]
454pub struct TokenAnalytics {
455 pub token: Token,
457
458 pub chain: String,
460
461 pub holders: Vec<TokenHolder>,
463
464 pub total_holders: u64,
466
467 pub volume_24h: f64,
469
470 pub volume_7d: f64,
472
473 pub price_usd: f64,
475
476 pub price_change_24h: f64,
478
479 pub price_change_7d: f64,
481
482 pub liquidity_usd: f64,
484
485 #[serde(skip_serializing_if = "Option::is_none")]
487 pub market_cap: Option<f64>,
488
489 #[serde(skip_serializing_if = "Option::is_none")]
491 pub fdv: Option<f64>,
492
493 #[serde(skip_serializing_if = "Option::is_none")]
495 pub total_supply: Option<String>,
496
497 #[serde(skip_serializing_if = "Option::is_none")]
499 pub circulating_supply: Option<String>,
500
501 pub price_history: Vec<PricePoint>,
503
504 pub volume_history: Vec<VolumePoint>,
506
507 pub holder_history: Vec<HolderCountPoint>,
509
510 pub dex_pairs: Vec<DexPair>,
512
513 pub fetched_at: i64,
515
516 #[serde(skip_serializing_if = "Option::is_none")]
518 pub top_10_concentration: Option<f64>,
519
520 #[serde(skip_serializing_if = "Option::is_none")]
522 pub top_50_concentration: Option<f64>,
523
524 #[serde(skip_serializing_if = "Option::is_none")]
526 pub top_100_concentration: Option<f64>,
527
528 pub price_change_6h: f64,
530
531 pub price_change_1h: f64,
533
534 pub total_buys_24h: u64,
536
537 pub total_sells_24h: u64,
539
540 pub total_buys_6h: u64,
542
543 pub total_sells_6h: u64,
545
546 pub total_buys_1h: u64,
548
549 pub total_sells_1h: u64,
551
552 #[serde(skip_serializing_if = "Option::is_none")]
554 pub token_age_hours: Option<f64>,
555
556 #[serde(skip_serializing_if = "Option::is_none")]
558 pub image_url: Option<String>,
559
560 pub websites: Vec<String>,
562
563 pub socials: Vec<TokenSocial>,
565
566 #[serde(skip_serializing_if = "Option::is_none")]
568 pub dexscreener_url: Option<String>,
569}
570
571#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
573pub struct TokenSocial {
574 pub platform: String,
576 pub url: String,
578}
579
580pub fn infer_chain_from_address(address: &str) -> Option<&'static str> {
606 if address.starts_with('T') && address.len() == 34 && bs58::decode(address).into_vec().is_ok() {
608 return Some("tron");
609 }
610
611 if address.starts_with("0x")
613 && address.len() == 42
614 && address[2..].chars().all(|c| c.is_ascii_hexdigit())
615 {
616 return Some("ethereum");
617 }
618
619 if address.len() >= 32
621 && address.len() <= 44
622 && let Ok(decoded) = bs58::decode(address).into_vec()
623 && decoded.len() == 32
624 {
625 return Some("solana");
626 }
627
628 None
629}
630
631pub fn infer_chain_from_hash(hash: &str) -> Option<&'static str> {
656 if hash.starts_with("0x")
658 && hash.len() == 66
659 && hash[2..].chars().all(|c| c.is_ascii_hexdigit())
660 {
661 return Some("ethereum");
662 }
663
664 if hash.len() == 64 && hash.chars().all(|c| c.is_ascii_hexdigit()) {
666 return Some("tron");
667 }
668
669 if hash.len() >= 80
671 && hash.len() <= 90
672 && let Ok(decoded) = bs58::decode(hash).into_vec()
673 && decoded.len() == 64
674 {
675 return Some("solana");
676 }
677
678 None
679}
680
681#[cfg(test)]
686mod tests {
687 use super::*;
688
689 #[test]
690 fn test_balance_serialization() {
691 let balance = Balance {
692 raw: "1000000000000000000".to_string(),
693 formatted: "1.0".to_string(),
694 decimals: 18,
695 symbol: "ETH".to_string(),
696 usd_value: Some(3500.0),
697 };
698
699 let json = serde_json::to_string(&balance).unwrap();
700 assert!(json.contains("1000000000000000000"));
701 assert!(json.contains("1.0"));
702 assert!(json.contains("ETH"));
703 assert!(json.contains("3500"));
704
705 let deserialized: Balance = serde_json::from_str(&json).unwrap();
706 assert_eq!(deserialized.raw, balance.raw);
707 assert_eq!(deserialized.decimals, 18);
708 }
709
710 #[test]
711 fn test_balance_without_usd() {
712 let balance = Balance {
713 raw: "1000000000000000000".to_string(),
714 formatted: "1.0".to_string(),
715 decimals: 18,
716 symbol: "ETH".to_string(),
717 usd_value: None,
718 };
719
720 let json = serde_json::to_string(&balance).unwrap();
721 assert!(!json.contains("usd_value"));
722 }
723
724 #[test]
725 fn test_transaction_serialization() {
726 let tx = Transaction {
727 hash: "0xabc123".to_string(),
728 block_number: Some(12345678),
729 timestamp: Some(1700000000),
730 from: "0xfrom".to_string(),
731 to: Some("0xto".to_string()),
732 value: "1.0".to_string(),
733 gas_limit: 21000,
734 gas_used: Some(21000),
735 gas_price: "20000000000".to_string(),
736 nonce: 42,
737 input: "0x".to_string(),
738 status: Some(true),
739 };
740
741 let json = serde_json::to_string(&tx).unwrap();
742 assert!(json.contains("0xabc123"));
743 assert!(json.contains("12345678"));
744 assert!(json.contains("0xfrom"));
745 assert!(json.contains("0xto"));
746
747 let deserialized: Transaction = serde_json::from_str(&json).unwrap();
748 assert_eq!(deserialized.hash, tx.hash);
749 assert_eq!(deserialized.nonce, 42);
750 }
751
752 #[test]
753 fn test_pending_transaction_serialization() {
754 let tx = Transaction {
755 hash: "0xpending".to_string(),
756 block_number: None,
757 timestamp: None,
758 from: "0xfrom".to_string(),
759 to: Some("0xto".to_string()),
760 value: "1.0".to_string(),
761 gas_limit: 21000,
762 gas_used: None,
763 gas_price: "20000000000".to_string(),
764 nonce: 0,
765 input: "0x".to_string(),
766 status: None,
767 };
768
769 let json = serde_json::to_string(&tx).unwrap();
770 assert!(json.contains("0xpending"));
771 assert!(json.contains("null")); let deserialized: Transaction = serde_json::from_str(&json).unwrap();
774 assert!(deserialized.block_number.is_none());
775 assert!(deserialized.status.is_none());
776 }
777
778 #[test]
779 fn test_contract_creation_transaction() {
780 let tx = Transaction {
781 hash: "0xcreate".to_string(),
782 block_number: Some(100),
783 timestamp: Some(1700000000),
784 from: "0xdeployer".to_string(),
785 to: None, value: "0".to_string(),
787 gas_limit: 1000000,
788 gas_used: Some(500000),
789 gas_price: "20000000000".to_string(),
790 nonce: 0,
791 input: "0x608060...".to_string(),
792 status: Some(true),
793 };
794
795 let json = serde_json::to_string(&tx).unwrap();
796 assert!(json.contains("\"to\":null"));
797 }
798
799 #[test]
800 fn test_token_serialization() {
801 let token = Token {
802 contract_address: "0xtoken".to_string(),
803 symbol: "USDC".to_string(),
804 name: "USD Coin".to_string(),
805 decimals: 6,
806 };
807
808 let json = serde_json::to_string(&token).unwrap();
809 assert!(json.contains("USDC"));
810 assert!(json.contains("USD Coin"));
811 assert!(json.contains("\"decimals\":6"));
812
813 let deserialized: Token = serde_json::from_str(&json).unwrap();
814 assert_eq!(deserialized.decimals, 6);
815 }
816
817 #[test]
818 fn test_token_balance_serialization() {
819 let token_balance = TokenBalance {
820 token: Token {
821 contract_address: "0xtoken".to_string(),
822 symbol: "USDC".to_string(),
823 name: "USD Coin".to_string(),
824 decimals: 6,
825 },
826 balance: "1000000".to_string(),
827 formatted_balance: "1.0".to_string(),
828 usd_value: Some(1.0),
829 };
830
831 let json = serde_json::to_string(&token_balance).unwrap();
832 assert!(json.contains("USDC"));
833 assert!(json.contains("1000000"));
834 assert!(json.contains("1.0"));
835 }
836
837 #[test]
838 fn test_balance_debug() {
839 let balance = Balance {
840 raw: "1000".to_string(),
841 formatted: "0.001".to_string(),
842 decimals: 18,
843 symbol: "ETH".to_string(),
844 usd_value: None,
845 };
846
847 let debug_str = format!("{:?}", balance);
848 assert!(debug_str.contains("Balance"));
849 assert!(debug_str.contains("1000"));
850 }
851
852 #[test]
853 fn test_transaction_debug() {
854 let tx = Transaction {
855 hash: "0xtest".to_string(),
856 block_number: Some(1),
857 timestamp: Some(0),
858 from: "0x1".to_string(),
859 to: Some("0x2".to_string()),
860 value: "0".to_string(),
861 gas_limit: 21000,
862 gas_used: Some(21000),
863 gas_price: "0".to_string(),
864 nonce: 0,
865 input: "0x".to_string(),
866 status: Some(true),
867 };
868
869 let debug_str = format!("{:?}", tx);
870 assert!(debug_str.contains("Transaction"));
871 assert!(debug_str.contains("0xtest"));
872 }
873
874 #[test]
879 fn test_infer_chain_from_address_evm() {
880 assert_eq!(
882 super::infer_chain_from_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2"),
883 Some("ethereum")
884 );
885 assert_eq!(
886 super::infer_chain_from_address("0x0000000000000000000000000000000000000000"),
887 Some("ethereum")
888 );
889 assert_eq!(
890 super::infer_chain_from_address("0xABCDEF1234567890abcdef1234567890ABCDEF12"),
891 Some("ethereum")
892 );
893 }
894
895 #[test]
896 fn test_infer_chain_from_address_tron() {
897 assert_eq!(
899 super::infer_chain_from_address("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf"),
900 Some("tron")
901 );
902 assert_eq!(
903 super::infer_chain_from_address("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"),
904 Some("tron")
905 );
906 }
907
908 #[test]
909 fn test_infer_chain_from_address_solana() {
910 assert_eq!(
912 super::infer_chain_from_address("DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy"),
913 Some("solana")
914 );
915 assert_eq!(
917 super::infer_chain_from_address("11111111111111111111111111111111"),
918 Some("solana")
919 );
920 }
921
922 #[test]
923 fn test_infer_chain_from_address_invalid() {
924 assert_eq!(super::infer_chain_from_address("0x123"), None);
926 assert_eq!(super::infer_chain_from_address("not_an_address"), None);
928 assert_eq!(super::infer_chain_from_address(""), None);
930 assert_eq!(super::infer_chain_from_address("0x123456"), None);
932 assert_eq!(
934 super::infer_chain_from_address("ADqSquXBgUCLYvYC4XZgrprLK589dkhSCf"),
935 None
936 );
937 }
938
939 #[test]
940 fn test_infer_chain_from_hash_evm() {
941 assert_eq!(
943 super::infer_chain_from_hash(
944 "0xabc123def456789012345678901234567890123456789012345678901234abcd"
945 ),
946 Some("ethereum")
947 );
948 assert_eq!(
949 super::infer_chain_from_hash(
950 "0x0000000000000000000000000000000000000000000000000000000000000000"
951 ),
952 Some("ethereum")
953 );
954 }
955
956 #[test]
957 fn test_infer_chain_from_hash_tron() {
958 assert_eq!(
960 super::infer_chain_from_hash(
961 "abc123def456789012345678901234567890123456789012345678901234abcd"
962 ),
963 Some("tron")
964 );
965 assert_eq!(
966 super::infer_chain_from_hash(
967 "0000000000000000000000000000000000000000000000000000000000000000"
968 ),
969 Some("tron")
970 );
971 }
972
973 #[test]
974 fn test_infer_chain_from_hash_solana() {
975 let solana_sig = "5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW";
978 assert_eq!(super::infer_chain_from_hash(solana_sig), Some("solana"));
979 }
980
981 #[test]
982 fn test_infer_chain_from_hash_invalid() {
983 assert_eq!(super::infer_chain_from_hash("0x123"), None);
985 assert_eq!(super::infer_chain_from_hash("not_a_hash"), None);
987 assert_eq!(super::infer_chain_from_hash(""), None);
989 assert_eq!(
991 super::infer_chain_from_hash(
992 "abc123gef456789012345678901234567890123456789012345678901234abcd"
993 ),
994 None
995 );
996 }
997
998 #[test]
1003 fn test_default_client_factory_create_dex_client() {
1004 let config = crate::config::ChainsConfig::default();
1005 let factory = DefaultClientFactory {
1006 chains_config: config,
1007 };
1008 let dex = factory.create_dex_client();
1009 let _ = format!("{:?}", std::mem::size_of_val(&dex));
1011 }
1012
1013 #[test]
1014 fn test_default_client_factory_create_ethereum_client() {
1015 let config = crate::config::ChainsConfig::default();
1016 let factory = DefaultClientFactory {
1017 chains_config: config,
1018 };
1019 let client = factory.create_chain_client("ethereum");
1021 assert!(client.is_ok());
1022 assert_eq!(client.unwrap().chain_name(), "ethereum");
1023 }
1024
1025 #[test]
1026 fn test_default_client_factory_create_polygon_client() {
1027 let config = crate::config::ChainsConfig::default();
1028 let factory = DefaultClientFactory {
1029 chains_config: config,
1030 };
1031 let client = factory.create_chain_client("polygon");
1032 assert!(client.is_ok());
1033 assert_eq!(client.unwrap().chain_name(), "polygon");
1034 }
1035
1036 #[test]
1037 fn test_default_client_factory_create_solana_client() {
1038 let config = crate::config::ChainsConfig::default();
1039 let factory = DefaultClientFactory {
1040 chains_config: config,
1041 };
1042 let client = factory.create_chain_client("solana");
1043 assert!(client.is_ok());
1044 assert_eq!(client.unwrap().chain_name(), "solana");
1045 }
1046
1047 #[test]
1048 fn test_default_client_factory_create_sol_alias() {
1049 let config = crate::config::ChainsConfig::default();
1050 let factory = DefaultClientFactory {
1051 chains_config: config,
1052 };
1053 let client = factory.create_chain_client("sol");
1054 assert!(client.is_ok());
1055 assert_eq!(client.unwrap().chain_name(), "solana");
1056 }
1057
1058 #[test]
1059 fn test_default_client_factory_create_tron_client() {
1060 let config = crate::config::ChainsConfig::default();
1061 let factory = DefaultClientFactory {
1062 chains_config: config,
1063 };
1064 let client = factory.create_chain_client("tron");
1065 assert!(client.is_ok());
1066 assert_eq!(client.unwrap().chain_name(), "tron");
1067 }
1068
1069 #[test]
1070 fn test_default_client_factory_create_trx_alias() {
1071 let config = crate::config::ChainsConfig::default();
1072 let factory = DefaultClientFactory {
1073 chains_config: config,
1074 };
1075 let client = factory.create_chain_client("trx");
1076 assert!(client.is_ok());
1077 assert_eq!(client.unwrap().chain_name(), "tron");
1078 }
1079
1080 #[tokio::test]
1085 async fn test_chain_client_default_get_token_info() {
1086 use super::mocks::MockChainClient;
1087 let client = MockChainClient::new("ethereum", "ETH");
1089 let result = client.get_token_info("0xsometoken").await;
1090 assert!(result.is_err());
1091 }
1092
1093 #[tokio::test]
1094 async fn test_chain_client_default_get_token_holders() {
1095 use super::mocks::MockChainClient;
1096 let client = MockChainClient::new("ethereum", "ETH");
1097 let holders = client.get_token_holders("0xsometoken", 10).await.unwrap();
1098 assert!(holders.is_empty());
1099 }
1100
1101 #[tokio::test]
1102 async fn test_chain_client_default_get_token_holder_count() {
1103 use super::mocks::MockChainClient;
1104 let client = MockChainClient::new("ethereum", "ETH");
1105 let count = client.get_token_holder_count("0xsometoken").await.unwrap();
1106 assert_eq!(count, 0);
1107 }
1108
1109 #[tokio::test]
1110 async fn test_mock_client_factory_creates_chain_client() {
1111 use super::mocks::MockClientFactory;
1112 let factory = MockClientFactory::new();
1113 let client = factory.create_chain_client("anything").unwrap();
1114 assert_eq!(client.chain_name(), "ethereum"); }
1116
1117 #[tokio::test]
1118 async fn test_mock_client_factory_creates_dex_client() {
1119 use super::mocks::MockClientFactory;
1120 let factory = MockClientFactory::new();
1121 let dex = factory.create_dex_client();
1122 let price = dex.get_token_price("ethereum", "0xtest").await;
1123 assert_eq!(price, Some(1.0));
1124 }
1125
1126 #[tokio::test]
1127 async fn test_mock_chain_client_balance() {
1128 use super::mocks::MockChainClient;
1129 let client = MockChainClient::new("ethereum", "ETH");
1130 let balance = client.get_balance("0xtest").await.unwrap();
1131 assert_eq!(balance.formatted, "1.0");
1132 assert_eq!(balance.symbol, "ETH");
1133 assert_eq!(balance.usd_value, Some(2500.0));
1134 }
1135
1136 #[tokio::test]
1137 async fn test_mock_chain_client_transaction() {
1138 use super::mocks::MockChainClient;
1139 let client = MockChainClient::new("ethereum", "ETH");
1140 let tx = client.get_transaction("0xanyhash").await.unwrap();
1141 assert_eq!(tx.hash, "0xmocktx");
1142 assert_eq!(tx.nonce, 42);
1143 }
1144
1145 #[tokio::test]
1146 async fn test_mock_chain_client_block_number() {
1147 use super::mocks::MockChainClient;
1148 let client = MockChainClient::new("ethereum", "ETH");
1149 let block = client.get_block_number().await.unwrap();
1150 assert_eq!(block, 12345678);
1151 }
1152
1153 #[tokio::test]
1154 async fn test_mock_dex_source_data() {
1155 use super::mocks::MockDexSource;
1156 let dex = MockDexSource::new();
1157 let data = dex.get_token_data("ethereum", "0xtest").await.unwrap();
1158 assert_eq!(data.symbol, "MOCK");
1159 assert_eq!(data.price_usd, 1.0);
1160 }
1161
1162 #[tokio::test]
1163 async fn test_mock_dex_source_search() {
1164 use super::mocks::MockDexSource;
1165 let dex = MockDexSource::new();
1166 let results = dex.search_tokens("test", None).await.unwrap();
1167 assert!(results.is_empty());
1168 }
1169
1170 #[tokio::test]
1171 async fn test_mock_dex_source_native_price() {
1172 use super::mocks::MockDexSource;
1173 let dex = MockDexSource::new();
1174 let price = dex.get_native_token_price("ethereum").await;
1175 assert_eq!(price, Some(2500.0));
1176 }
1177
1178 struct MinimalChainClient;
1184
1185 #[async_trait::async_trait]
1186 impl ChainClient for MinimalChainClient {
1187 fn chain_name(&self) -> &str {
1188 "test"
1189 }
1190
1191 fn native_token_symbol(&self) -> &str {
1192 "TEST"
1193 }
1194
1195 async fn get_balance(&self, _address: &str) -> Result<Balance> {
1196 Ok(Balance {
1197 raw: "0".to_string(),
1198 formatted: "0".to_string(),
1199 decimals: 18,
1200 symbol: "TEST".to_string(),
1201 usd_value: None,
1202 })
1203 }
1204
1205 async fn get_transaction(&self, _hash: &str) -> Result<Transaction> {
1206 unimplemented!()
1207 }
1208
1209 async fn get_transactions(&self, _address: &str, _limit: u32) -> Result<Vec<Transaction>> {
1210 Ok(Vec::new())
1211 }
1212
1213 async fn get_block_number(&self) -> Result<u64> {
1214 Ok(0)
1215 }
1216
1217 async fn get_token_balances(&self, _address: &str) -> Result<Vec<TokenBalance>> {
1218 Ok(Vec::new())
1219 }
1220
1221 async fn enrich_balance_usd(&self, _balance: &mut Balance) {}
1222 }
1223
1224 #[tokio::test]
1225 async fn test_default_get_token_info() {
1226 let client = MinimalChainClient;
1227 let result = client.get_token_info("0xtest").await;
1228 assert!(result.is_err());
1229 assert!(result.unwrap_err().to_string().contains("not supported"));
1230 }
1231
1232 #[tokio::test]
1233 async fn test_default_get_token_holders() {
1234 let client = MinimalChainClient;
1235 let holders = client.get_token_holders("0xtest", 10).await.unwrap();
1236 assert!(holders.is_empty());
1237 }
1238
1239 #[tokio::test]
1240 async fn test_default_get_token_holder_count() {
1241 let client = MinimalChainClient;
1242 let count = client.get_token_holder_count("0xtest").await.unwrap();
1243 assert_eq!(count, 0);
1244 }
1245}
1246
1247#[cfg(test)]
1256pub mod mocks {
1257 use super::*;
1258 use crate::chains::dex::{DexDataSource, DexTokenData, TokenSearchResult};
1259 use async_trait::async_trait;
1260
1261 #[derive(Debug, Clone)]
1263 pub struct MockChainClient {
1264 pub chain: String,
1265 pub symbol: String,
1266 pub balance: Balance,
1267 pub transaction: Transaction,
1268 pub transactions: Vec<Transaction>,
1269 pub token_balances: Vec<TokenBalance>,
1270 pub block_number: u64,
1271 pub token_info: Option<Token>,
1272 pub token_holders: Vec<TokenHolder>,
1273 pub token_holder_count: u64,
1274 }
1275
1276 impl MockChainClient {
1277 pub fn new(chain: &str, symbol: &str) -> Self {
1279 Self {
1280 chain: chain.to_string(),
1281 symbol: symbol.to_string(),
1282 balance: Balance {
1283 raw: "1000000000000000000".to_string(),
1284 formatted: "1.0".to_string(),
1285 decimals: 18,
1286 symbol: symbol.to_string(),
1287 usd_value: Some(2500.0),
1288 },
1289 transaction: Transaction {
1290 hash: "0xmocktx".to_string(),
1291 block_number: Some(12345678),
1292 timestamp: Some(1700000000),
1293 from: "0xfrom".to_string(),
1294 to: Some("0xto".to_string()),
1295 value: "1.0".to_string(),
1296 gas_limit: 21000,
1297 gas_used: Some(21000),
1298 gas_price: "20000000000".to_string(),
1299 nonce: 42,
1300 input: "0x".to_string(),
1301 status: Some(true),
1302 },
1303 transactions: vec![],
1304 token_balances: vec![],
1305 block_number: 12345678,
1306 token_info: None,
1307 token_holders: vec![],
1308 token_holder_count: 0,
1309 }
1310 }
1311 }
1312
1313 #[async_trait]
1314 impl ChainClient for MockChainClient {
1315 fn chain_name(&self) -> &str {
1316 &self.chain
1317 }
1318
1319 fn native_token_symbol(&self) -> &str {
1320 &self.symbol
1321 }
1322
1323 async fn get_balance(&self, _address: &str) -> Result<Balance> {
1324 Ok(self.balance.clone())
1325 }
1326
1327 async fn enrich_balance_usd(&self, _balance: &mut Balance) {
1328 }
1330
1331 async fn get_transaction(&self, _hash: &str) -> Result<Transaction> {
1332 Ok(self.transaction.clone())
1333 }
1334
1335 async fn get_transactions(&self, _address: &str, _limit: u32) -> Result<Vec<Transaction>> {
1336 Ok(self.transactions.clone())
1337 }
1338
1339 async fn get_block_number(&self) -> Result<u64> {
1340 Ok(self.block_number)
1341 }
1342
1343 async fn get_token_balances(&self, _address: &str) -> Result<Vec<TokenBalance>> {
1344 Ok(self.token_balances.clone())
1345 }
1346
1347 async fn get_token_info(&self, _address: &str) -> Result<Token> {
1348 match &self.token_info {
1349 Some(t) => Ok(t.clone()),
1350 None => Err(crate::error::ScopeError::Chain(
1351 "Token info not available".to_string(),
1352 )),
1353 }
1354 }
1355
1356 async fn get_token_holders(&self, _address: &str, _limit: u32) -> Result<Vec<TokenHolder>> {
1357 Ok(self.token_holders.clone())
1358 }
1359
1360 async fn get_token_holder_count(&self, _address: &str) -> Result<u64> {
1361 Ok(self.token_holder_count)
1362 }
1363 }
1364
1365 #[derive(Debug, Clone)]
1367 pub struct MockDexSource {
1368 pub token_price: Option<f64>,
1369 pub native_price: Option<f64>,
1370 pub token_data: Option<DexTokenData>,
1371 pub search_results: Vec<TokenSearchResult>,
1372 }
1373
1374 impl Default for MockDexSource {
1375 fn default() -> Self {
1376 Self::new()
1377 }
1378 }
1379
1380 impl MockDexSource {
1381 pub fn new() -> Self {
1383 Self {
1384 token_price: Some(1.0),
1385 native_price: Some(2500.0),
1386 token_data: Some(DexTokenData {
1387 address: "0xmocktoken".to_string(),
1388 symbol: "MOCK".to_string(),
1389 name: "Mock Token".to_string(),
1390 price_usd: 1.0,
1391 price_change_24h: 5.0,
1392 price_change_6h: 2.0,
1393 price_change_1h: 0.5,
1394 price_change_5m: 0.1,
1395 volume_24h: 1_000_000.0,
1396 volume_6h: 250_000.0,
1397 volume_1h: 50_000.0,
1398 liquidity_usd: 5_000_000.0,
1399 market_cap: Some(100_000_000.0),
1400 fdv: Some(200_000_000.0),
1401 pairs: vec![],
1402 price_history: vec![],
1403 volume_history: vec![],
1404 total_buys_24h: 500,
1405 total_sells_24h: 450,
1406 total_buys_6h: 120,
1407 total_sells_6h: 110,
1408 total_buys_1h: 20,
1409 total_sells_1h: 18,
1410 earliest_pair_created_at: Some(1690000000),
1411 image_url: None,
1412 websites: vec![],
1413 socials: vec![],
1414 dexscreener_url: None,
1415 }),
1416 search_results: vec![],
1417 }
1418 }
1419 }
1420
1421 #[async_trait]
1422 impl DexDataSource for MockDexSource {
1423 async fn get_token_price(&self, _chain: &str, _address: &str) -> Option<f64> {
1424 self.token_price
1425 }
1426
1427 async fn get_native_token_price(&self, _chain: &str) -> Option<f64> {
1428 self.native_price
1429 }
1430
1431 async fn get_token_data(&self, _chain: &str, _address: &str) -> Result<DexTokenData> {
1432 match &self.token_data {
1433 Some(data) => Ok(data.clone()),
1434 None => Err(crate::error::ScopeError::NotFound(
1435 "No DEX data found".to_string(),
1436 )),
1437 }
1438 }
1439
1440 async fn search_tokens(
1441 &self,
1442 _query: &str,
1443 _chain: Option<&str>,
1444 ) -> Result<Vec<TokenSearchResult>> {
1445 Ok(self.search_results.clone())
1446 }
1447 }
1448
1449 pub struct MockClientFactory {
1451 pub mock_client: MockChainClient,
1452 pub mock_dex: MockDexSource,
1453 }
1454
1455 impl Default for MockClientFactory {
1456 fn default() -> Self {
1457 Self::new()
1458 }
1459 }
1460
1461 impl MockClientFactory {
1462 pub fn new() -> Self {
1464 Self {
1465 mock_client: MockChainClient::new("ethereum", "ETH"),
1466 mock_dex: MockDexSource::new(),
1467 }
1468 }
1469 }
1470
1471 impl ChainClientFactory for MockClientFactory {
1472 fn create_chain_client(&self, _chain: &str) -> Result<Box<dyn ChainClient>> {
1473 Ok(Box::new(self.mock_client.clone()))
1474 }
1475
1476 fn create_dex_client(&self) -> Box<dyn DexDataSource> {
1477 Box::new(self.mock_dex.clone())
1478 }
1479 }
1480}