1use crate::{client::WebClient, error::WebToolError};
8use chrono::{DateTime, Utc};
9use riglr_core::provider::ApplicationContext;
10use riglr_macros::tool;
11use schemars::JsonSchema;
12
13const FASTER100X_API_KEY: &str = "FASTER100X_API_KEY";
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use tracing::{debug, info};
17
18mod api_types {
20 use serde::{Deserialize, Serialize};
21
22 #[derive(Debug, Deserialize, Serialize)]
23 pub struct ApiResponseRaw {
24 pub data: DataRaw,
25 pub success: Option<bool>,
26 pub error: Option<String>,
27 }
28
29 #[derive(Debug, Deserialize, Serialize)]
30 pub struct DataRaw {
31 pub symbol: Option<String>,
32 pub name: Option<String>,
33 pub total_holders: Option<u64>,
34 pub unique_holders: Option<u64>,
35 pub distribution: Option<DistributionRaw>,
36 pub top_holders: Option<Vec<HolderRaw>>,
37 pub concentration_risk: Option<ConcentrationRiskRaw>,
38 pub recent_activity: Option<ActivityRaw>,
39 pub transactions: Option<Vec<WhaleTransactionRaw>>,
40 pub total_buys_usd: Option<f64>,
41 pub total_sells_usd: Option<f64>,
42 pub active_whales: Option<u64>,
43 pub points: Option<Vec<TrendPointRaw>>,
44 pub trend_direction: Option<String>,
45 pub trend_strength: Option<u64>,
46 pub insights: Option<Vec<String>>,
47 }
48
49 #[derive(Debug, Deserialize, Serialize)]
50 pub struct DistributionRaw {
51 pub top_1_percent: Option<f64>,
52 pub top_5_percent: Option<f64>,
53 pub top_10_percent: Option<f64>,
54 pub whale_percentage: Option<f64>,
55 pub retail_percentage: Option<f64>,
56 pub gini_coefficient: Option<f64>,
57 }
58
59 #[derive(Debug, Deserialize, Serialize)]
60 pub struct HolderRaw {
61 pub address: Option<String>,
62 pub balance: Option<f64>,
63 pub percentage: Option<f64>,
64 pub usd_value: Option<f64>,
65 #[serde(rename = "type")]
66 pub wallet_type: Option<String>,
67 pub tx_count: Option<u64>,
68 pub first_acquired: Option<String>,
69 pub last_activity: Option<String>,
70 }
71
72 #[derive(Debug, Deserialize, Serialize)]
73 pub struct ConcentrationRiskRaw {
74 pub level: Option<String>,
75 pub score: Option<u64>,
76 pub wallets_50_percent: Option<u64>,
77 pub largest_holder: Option<f64>,
78 pub exchange_percentage: Option<f64>,
79 pub locked_percentage: Option<f64>,
80 pub risk_factors: Option<Vec<String>>,
81 }
82
83 #[derive(Debug, Deserialize, Serialize)]
84 pub struct ActivityRaw {
85 pub new_holders_24h: Option<u64>,
86 pub exited_holders_24h: Option<u64>,
87 pub net_change_24h: Option<i64>,
88 pub avg_buy_size_24h: Option<f64>,
89 pub avg_sell_size_24h: Option<f64>,
90 pub growth_rate_7d: Option<f64>,
91 }
92
93 #[derive(Debug, Deserialize, Serialize)]
94 pub struct WhaleTransactionRaw {
95 pub hash: Option<String>,
96 pub from: Option<String>,
97 #[serde(rename = "type")]
98 pub transaction_type: Option<String>,
99 pub token_amount: Option<f64>,
100 pub usd_value: Option<f64>,
101 pub timestamp: Option<String>,
102 pub price_impact: Option<f64>,
103 pub gas_fee: Option<f64>,
104 }
105
106 #[derive(Debug, Deserialize, Serialize)]
107 pub struct WhaleStatsRaw {
108 pub total_whale_count: Option<u64>,
109 pub whale_holding_percentage: Option<f64>,
110 pub whale_activity_score: Option<f64>,
111 pub whale_accumulation_trend: Option<String>,
112 pub recent_whale_trend: Option<String>,
113 pub average_whale_balance: Option<f64>,
114 pub whale_dominance: Option<f64>,
115 }
116
117 #[derive(Debug, Deserialize, Serialize)]
118 pub struct TrendPointRaw {
119 pub timestamp: Option<String>,
120 pub holders: Option<u64>,
121 pub change: Option<i64>,
122 pub price: Option<f64>,
123 pub whale_percentage: Option<f64>,
124 pub volume_24h: Option<f64>,
125 }
126
127 #[derive(Debug, Deserialize, Serialize)]
128 pub struct TrendSummaryRaw {
129 pub trend_direction: Option<String>,
130 pub growth_rate_7d: Option<f64>,
131 pub growth_rate_30d: Option<f64>,
132 pub volatility_score: Option<f64>,
133 pub health_score: Option<f64>,
134 pub recommendation: Option<String>,
135 pub risk_factors: Option<Vec<String>>,
136 pub positive_factors: Option<Vec<String>>,
137 }
138}
139
140#[derive(Debug, Clone)]
142pub struct Faster100xConfig {
143 pub api_key: String,
145 pub base_url: String,
147 pub rate_limit_per_minute: u32,
149}
150
151impl Default for Faster100xConfig {
152 fn default() -> Self {
153 Self {
154 api_key: std::env::var(FASTER100X_API_KEY).unwrap_or_default(),
155 base_url: "https://api.faster100x.com/v1".to_string(),
156 rate_limit_per_minute: 100,
157 }
158 }
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
163pub struct TokenHolderAnalysis {
164 pub token_address: String,
166 pub token_symbol: String,
168 pub token_name: String,
170 pub total_holders: u64,
172 pub unique_holders: u64,
174 pub distribution: HolderDistribution,
176 pub top_holders: Vec<WalletHolding>,
178 pub concentration_risk: ConcentrationRisk,
180 pub recent_activity: HolderActivity,
182 pub timestamp: DateTime<Utc>,
184 pub chain: String,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
190pub struct HolderDistribution {
191 pub top_1_percent: f64,
193 pub top_5_percent: f64,
195 pub top_10_percent: f64,
197 pub whale_percentage: f64,
199 pub retail_percentage: f64,
201 pub gini_coefficient: f64,
203 pub holder_categories: HashMap<String, u64>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
209pub struct WalletHolding {
210 pub wallet_address: String,
212 pub token_amount: f64,
214 pub percentage_of_supply: f64,
216 pub usd_value: f64,
218 pub first_acquired: DateTime<Utc>,
220 pub last_activity: DateTime<Utc>,
222 pub wallet_type: String, pub transaction_count: u64,
226 pub avg_holding_time_days: f64,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
232pub struct ConcentrationRisk {
233 pub risk_level: String,
235 pub risk_score: u8,
237 pub wallets_controlling_50_percent: u64,
239 pub largest_holder_percentage: f64,
241 pub exchange_holdings_percentage: f64,
243 pub locked_holdings_percentage: f64,
245 pub risk_factors: Vec<String>,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
251pub struct HolderActivity {
252 pub new_holders_24h: u64,
254 pub exited_holders_24h: u64,
256 pub net_holder_change_24h: i64,
258 pub avg_buy_size_24h: f64,
260 pub avg_sell_size_24h: f64,
262 pub holder_growth_rate_7d: f64,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
268pub struct WhaleActivity {
269 pub token_address: String,
271 pub whale_transactions: Vec<WhaleTransaction>,
273 pub total_whale_buys: f64,
275 pub total_whale_sells: f64,
277 pub net_whale_flow: f64,
279 pub active_whales: u64,
281 pub timeframe: String,
283 pub timestamp: DateTime<Utc>,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
289pub struct WhaleTransaction {
290 pub tx_hash: String,
292 pub wallet_address: String,
294 pub transaction_type: String,
296 pub token_amount: f64,
298 pub usd_value: f64,
300 pub timestamp: DateTime<Utc>,
302 pub gas_fee: f64,
304 pub price_impact: f64,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
310pub struct HolderTrends {
311 pub token_address: String,
313 pub data_points: Vec<HolderTrendPoint>,
315 pub trend_direction: String, pub trend_strength: u8,
319 pub analysis_period: String,
321 pub insights: Vec<String>,
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
327pub struct HolderTrendPoint {
328 pub timestamp: DateTime<Utc>,
330 pub total_holders: u64,
332 pub holder_change: i64,
334 pub token_price: f64,
336 pub whale_percentage: f64,
338 pub volume_24h: f64,
340}
341
342fn convert_raw_holder(raw: &api_types::HolderRaw) -> WalletHolding {
346 let first_acquired = raw
347 .first_acquired
348 .as_ref()
349 .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
350 .map_or_else(Utc::now, |dt| dt.with_timezone(&Utc));
351
352 let last_activity = raw
353 .last_activity
354 .as_ref()
355 .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
356 .map_or_else(Utc::now, |dt| dt.with_timezone(&Utc));
357
358 WalletHolding {
359 wallet_address: raw.address.clone().unwrap_or_else(|| "unknown".to_string()),
360 token_amount: raw.balance.unwrap_or(0.0),
361 percentage_of_supply: raw.percentage.unwrap_or(0.0),
362 usd_value: raw.usd_value.unwrap_or(0.0),
363 first_acquired,
364 last_activity,
365 wallet_type: raw
366 .wallet_type
367 .clone()
368 .unwrap_or_else(|| "unknown".to_string()),
369 transaction_count: raw.tx_count.unwrap_or(0),
370 avg_holding_time_days: 0.0, }
372}
373
374fn convert_raw_distribution(raw: &api_types::DistributionRaw) -> HolderDistribution {
376 HolderDistribution {
377 top_1_percent: raw.top_1_percent.unwrap_or(0.0),
378 top_5_percent: raw.top_5_percent.unwrap_or(0.0),
379 top_10_percent: raw.top_10_percent.unwrap_or(0.0),
380 whale_percentage: raw.whale_percentage.unwrap_or(0.0),
381 retail_percentage: raw.retail_percentage.unwrap_or(0.0),
382 gini_coefficient: raw.gini_coefficient.unwrap_or(0.0),
383 holder_categories: HashMap::new(),
384 }
385}
386
387fn convert_raw_concentration_risk(raw: &api_types::ConcentrationRiskRaw) -> ConcentrationRisk {
389 ConcentrationRisk {
390 risk_level: raw.level.clone().unwrap_or_else(|| "Medium".to_string()),
391 risk_score: raw.score.unwrap_or(50) as u8,
392 wallets_controlling_50_percent: raw.wallets_50_percent.unwrap_or(0),
393 largest_holder_percentage: raw.largest_holder.unwrap_or(0.0),
394 exchange_holdings_percentage: raw.exchange_percentage.unwrap_or(0.0),
395 locked_holdings_percentage: raw.locked_percentage.unwrap_or(0.0),
396 risk_factors: raw.risk_factors.clone().unwrap_or_default(),
397 }
398}
399
400fn convert_raw_activity(raw: &api_types::ActivityRaw) -> HolderActivity {
402 HolderActivity {
403 new_holders_24h: raw.new_holders_24h.unwrap_or(0),
404 exited_holders_24h: raw.exited_holders_24h.unwrap_or(0),
405 net_holder_change_24h: raw.net_change_24h.unwrap_or(0),
406 avg_buy_size_24h: raw.avg_buy_size_24h.unwrap_or(0.0),
407 avg_sell_size_24h: raw.avg_sell_size_24h.unwrap_or(0.0),
408 holder_growth_rate_7d: raw.growth_rate_7d.unwrap_or(0.0),
409 }
410}
411
412fn convert_raw_whale_transaction(raw: &api_types::WhaleTransactionRaw) -> WhaleTransaction {
414 let timestamp = raw
415 .timestamp
416 .as_ref()
417 .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
418 .map_or_else(Utc::now, |dt| dt.with_timezone(&Utc));
419
420 WhaleTransaction {
421 tx_hash: raw.hash.clone().unwrap_or_default(),
422 wallet_address: raw.from.clone().unwrap_or_default(),
423 transaction_type: raw
424 .transaction_type
425 .clone()
426 .unwrap_or_else(|| "unknown".to_string()),
427 token_amount: raw.token_amount.unwrap_or(0.0),
428 usd_value: raw.usd_value.unwrap_or(0.0),
429 timestamp,
430 price_impact: raw.price_impact.unwrap_or(0.0),
431 gas_fee: raw.gas_fee.unwrap_or(0.0),
432 }
433}
434
435fn convert_raw_trend_point(raw: &api_types::TrendPointRaw) -> HolderTrendPoint {
437 let timestamp = raw
438 .timestamp
439 .as_ref()
440 .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
441 .map_or_else(Utc::now, |dt| dt.with_timezone(&Utc));
442
443 HolderTrendPoint {
444 timestamp,
445 total_holders: raw.holders.unwrap_or(0),
446 holder_change: raw.change.unwrap_or(0),
447 token_price: raw.price.unwrap_or(0.0),
448 whale_percentage: raw.whale_percentage.unwrap_or(0.0),
449 volume_24h: raw.volume_24h.unwrap_or(0.0),
450 }
451}
452
453fn get_api_key_from_context(context: &ApplicationContext) -> Result<String, WebToolError> {
463 context
464 .config
465 .providers
466 .faster100x_api_key
467 .clone()
468 .ok_or_else(|| {
469 WebToolError::Config(
470 "Faster100x API key not configured. Set FASTER100X_API_KEY in your environment."
471 .to_string(),
472 )
473 })
474}
475
476async fn create_faster100x_client_with_context(
479 context: &ApplicationContext,
480) -> Result<WebClient, WebToolError> {
481 let api_key = get_api_key_from_context(context)?;
482 let client = WebClient::default().with_api_key("faster100x", api_key);
483 Ok(client)
484}
485
486#[allow(dead_code)]
497async fn create_faster100x_client() -> Result<WebClient, WebToolError> {
498 let config = Faster100xConfig::default();
499
500 if config.api_key.is_empty() {
501 return Err(WebToolError::Config(
502 "FASTER100X_API_KEY environment variable not set".to_string(),
503 ));
504 }
505
506 let client = WebClient::default().with_api_key("faster100x", config.api_key);
507
508 Ok(client)
509}
510
511fn normalize_token_address(address: &str) -> Result<String, WebToolError> {
525 let address = address.trim();
526
527 if address.len() != 42 || !address.starts_with("0x") {
529 return Err(WebToolError::Config(
530 "Invalid token address format. Expected 42-character hex string starting with 0x"
531 .to_string(),
532 ));
533 }
534
535 Ok(address.to_lowercase())
537}
538
539#[tool]
540pub async fn analyze_token_holders(
552 context: &ApplicationContext,
553 token_address: String,
554 chain: Option<String>,
555) -> Result<TokenHolderAnalysis, WebToolError> {
556 let chain = chain.unwrap_or_else(|| "eth".to_string());
557 let normalized_address = normalize_token_address(&token_address)?;
558
559 info!(
560 "Analyzing token holders for {} on {}",
561 normalized_address, chain
562 );
563
564 if !["eth", "bsc", "polygon", "arbitrum", "base", "avalanche"].contains(&chain.as_str()) {
566 return Err(WebToolError::Config(
567 "Invalid chain. Must be one of: eth, bsc, polygon, arbitrum, base, avalanche"
568 .to_string(),
569 ));
570 }
571
572 let client = create_faster100x_client_with_context(context).await?;
573
574 let mut params = HashMap::new();
575 params.insert("chain".to_string(), chain.clone());
576 params.insert("include_distribution".to_string(), "true".to_string());
577 params.insert("include_top_holders".to_string(), "true".to_string());
578
579 let config = Faster100xConfig::default();
580 let url = format!("{}/tokens/{}/holders", config.base_url, normalized_address);
581
582 let response_text = client.get_with_params(&url, ¶ms).await?;
583
584 let response: api_types::ApiResponseRaw = serde_json::from_str(&response_text)
586 .map_err(|e| WebToolError::Parsing(format!("Invalid JSON response: {}", e)))?;
587
588 let data = response.data;
589
590 let token_symbol = data.symbol.unwrap_or_else(|| "UNKNOWN".to_string());
592 let token_name = data.name.unwrap_or_else(|| "Unknown Token".to_string());
593 let total_holders = data.total_holders.unwrap_or(0);
594 let unique_holders = data.unique_holders.unwrap_or(total_holders);
595
596 let distribution = data.distribution.as_ref().map_or_else(
598 || HolderDistribution {
599 top_1_percent: 0.0,
600 top_5_percent: 0.0,
601 top_10_percent: 0.0,
602 whale_percentage: 0.0,
603 retail_percentage: 0.0,
604 gini_coefficient: 0.0,
605 holder_categories: HashMap::new(),
606 },
607 convert_raw_distribution,
608 );
609
610 let top_holders = data
612 .top_holders
613 .as_ref()
614 .map(|holders| holders.iter().take(10).map(convert_raw_holder).collect())
615 .unwrap_or_default();
616
617 let concentration_risk = data.concentration_risk.as_ref().map_or_else(
619 || ConcentrationRisk {
620 risk_level: "Medium".to_string(),
621 risk_score: 50,
622 wallets_controlling_50_percent: 0,
623 largest_holder_percentage: 0.0,
624 exchange_holdings_percentage: 0.0,
625 locked_holdings_percentage: 0.0,
626 risk_factors: vec![],
627 },
628 convert_raw_concentration_risk,
629 );
630
631 let recent_activity = data.recent_activity.as_ref().map_or_else(
633 || HolderActivity {
634 new_holders_24h: 0,
635 exited_holders_24h: 0,
636 net_holder_change_24h: 0,
637 avg_buy_size_24h: 0.0,
638 avg_sell_size_24h: 0.0,
639 holder_growth_rate_7d: 0.0,
640 },
641 convert_raw_activity,
642 );
643
644 debug!(
645 "Successfully analyzed {} holders for token {} with {}% whale concentration",
646 total_holders, token_symbol, distribution.whale_percentage
647 );
648
649 Ok(TokenHolderAnalysis {
650 token_address: normalized_address,
651 token_symbol,
652 token_name,
653 total_holders,
654 unique_holders,
655 distribution,
656 top_holders,
657 concentration_risk,
658 recent_activity,
659 timestamp: Utc::now(),
660 chain,
661 })
662}
663
664#[tool]
665pub async fn get_whale_activity(
678 context: &ApplicationContext,
679 token_address: String,
680 timeframe: Option<String>,
681 min_usd_value: Option<f64>,
682) -> Result<WhaleActivity, WebToolError> {
683 let timeframe = timeframe.unwrap_or_else(|| "24h".to_string());
684 let min_usd_value = min_usd_value.unwrap_or(10000.0);
685 let normalized_address = normalize_token_address(&token_address)?;
686
687 info!(
688 "Analyzing whale activity for {} (timeframe: {}, min value: ${})",
689 normalized_address, timeframe, min_usd_value
690 );
691
692 if !["1h", "4h", "24h", "7d"].contains(&timeframe.as_str()) {
694 return Err(WebToolError::Config(
695 "Invalid timeframe. Must be one of: 1h, 4h, 24h, 7d".to_string(),
696 ));
697 }
698
699 let client = create_faster100x_client_with_context(context).await?;
700
701 let mut params = HashMap::new();
702 params.insert("timeframe".to_string(), timeframe.clone());
703 params.insert("min_usd".to_string(), min_usd_value.to_string());
704 params.insert("include_details".to_string(), "true".to_string());
705
706 let config = Faster100xConfig::default();
707 let url = format!(
708 "{}/tokens/{}/whale-activity",
709 config.base_url, normalized_address
710 );
711
712 let response_text = client.get_with_params(&url, ¶ms).await?;
713
714 let response: api_types::ApiResponseRaw = serde_json::from_str(&response_text)
716 .map_err(|e| WebToolError::Parsing(format!("Invalid JSON response: {}", e)))?;
717
718 let data = response.data;
719
720 let whale_transactions: Vec<WhaleTransaction> = data
722 .transactions
723 .as_ref()
724 .map(|txs| txs.iter().map(convert_raw_whale_transaction).collect())
725 .unwrap_or_default();
726
727 let total_whale_buys = data.total_buys_usd.unwrap_or(0.0);
729 let total_whale_sells = data.total_sells_usd.unwrap_or(0.0);
730 let net_whale_flow = total_whale_buys - total_whale_sells;
731 let active_whales = data.active_whales.unwrap_or(0);
732
733 debug!(
734 "Found {} whale transactions with net flow of ${:.2} for token {}",
735 whale_transactions.len(),
736 net_whale_flow,
737 normalized_address
738 );
739
740 Ok(WhaleActivity {
741 token_address: normalized_address,
742 whale_transactions,
743 total_whale_buys,
744 total_whale_sells,
745 net_whale_flow,
746 active_whales,
747 timeframe,
748 timestamp: Utc::now(),
749 })
750}
751
752#[tool]
753pub async fn get_holder_trends(
766 context: &ApplicationContext,
767 token_address: String,
768 period: Option<String>,
769 data_points: Option<u32>,
770) -> Result<HolderTrends, WebToolError> {
771 let period = period.unwrap_or_else(|| "30d".to_string());
772 let data_points = data_points.unwrap_or(30).clamp(10, 100);
773 let normalized_address = normalize_token_address(&token_address)?;
774
775 info!(
776 "Analyzing holder trends for {} (period: {}, data points: {})",
777 normalized_address, period, data_points
778 );
779
780 if !["7d", "30d", "90d"].contains(&period.as_str()) {
782 return Err(WebToolError::Config(
783 "Invalid period. Must be one of: 7d, 30d, 90d".to_string(),
784 ));
785 }
786
787 let client = create_faster100x_client_with_context(context).await?;
788
789 let mut params = HashMap::new();
790 params.insert("period".to_string(), period.clone());
791 params.insert("points".to_string(), data_points.to_string());
792 params.insert("include_price".to_string(), "true".to_string());
793
794 let config = Faster100xConfig::default();
795 let url = format!(
796 "{}/tokens/{}/holder-trends",
797 config.base_url, normalized_address
798 );
799
800 let response_text = client.get_with_params(&url, ¶ms).await?;
801
802 let response: api_types::ApiResponseRaw = serde_json::from_str(&response_text)
804 .map_err(|e| WebToolError::Parsing(format!("Invalid JSON response: {}", e)))?;
805
806 let data = response.data;
807
808 let trend_data_points: Vec<HolderTrendPoint> = data
810 .points
811 .as_ref()
812 .map(|points| points.iter().map(convert_raw_trend_point).collect())
813 .unwrap_or_default();
814
815 let trend_direction = data.trend_direction.unwrap_or_else(|| "stable".to_string());
817 let trend_strength = data.trend_strength.unwrap_or(50) as u8;
818 let insights = data.insights.unwrap_or_default();
819
820 debug!(
821 "Analyzed holder trends with {} data points, trend: {} (strength: {})",
822 trend_data_points.len(),
823 trend_direction,
824 trend_strength
825 );
826
827 Ok(HolderTrends {
828 token_address: normalized_address,
829 data_points: trend_data_points,
830 trend_direction,
831 trend_strength,
832 analysis_period: period,
833 insights,
834 })
835}
836
837#[cfg(test)]
838mod tests {
839 use super::*;
840
841 #[test]
842 fn test_faster100x_config_default() {
843 let config = Faster100xConfig::default();
844 assert_eq!(config.base_url, "https://api.faster100x.com/v1");
845 assert_eq!(config.rate_limit_per_minute, 100);
846 }
847
848 #[test]
849 fn test_normalize_token_address() {
850 let result = normalize_token_address("0x1234567890123456789012345678901234567890");
852 assert!(result.is_ok());
853 assert_eq!(
854 result.unwrap(),
855 "0x1234567890123456789012345678901234567890"
856 );
857
858 let result = normalize_token_address("0x123");
860 assert!(result.is_err());
861
862 let result = normalize_token_address("1234567890123456789012345678901234567890");
864 assert!(result.is_err());
865 }
866
867 #[test]
868 fn test_token_holder_analysis_serialization() {
869 let analysis = TokenHolderAnalysis {
870 token_address: "0x1234567890123456789012345678901234567890".to_string(),
871 token_symbol: "TEST".to_string(),
872 token_name: "Test Token".to_string(),
873 total_holders: 1000,
874 unique_holders: 950,
875 distribution: HolderDistribution {
876 top_1_percent: 50.0,
877 top_5_percent: 75.0,
878 top_10_percent: 85.0,
879 whale_percentage: 25.0,
880 retail_percentage: 40.0,
881 gini_coefficient: 0.65,
882 holder_categories: HashMap::new(),
883 },
884 top_holders: Vec::new(),
885 concentration_risk: ConcentrationRisk {
886 risk_level: "Medium".to_string(),
887 risk_score: 60,
888 wallets_controlling_50_percent: 5,
889 largest_holder_percentage: 15.0,
890 exchange_holdings_percentage: 20.0,
891 locked_holdings_percentage: 10.0,
892 risk_factors: Vec::new(),
893 },
894 recent_activity: HolderActivity {
895 new_holders_24h: 50,
896 exited_holders_24h: 30,
897 net_holder_change_24h: 20,
898 avg_buy_size_24h: 500.0,
899 avg_sell_size_24h: 300.0,
900 holder_growth_rate_7d: 5.2,
901 },
902 timestamp: Utc::now(),
903 chain: "eth".to_string(),
904 };
905
906 let serialized = serde_json::to_string(&analysis).unwrap();
907 assert!(serialized.contains("TEST"));
908 assert!(serialized.contains("eth"));
909 assert!(serialized.contains("60"));
910 }
911
912 #[test]
913 fn test_faster100x_config_with_env_var() {
914 std::env::set_var(FASTER100X_API_KEY, "test-api-key");
916 let config = Faster100xConfig::default();
917 assert_eq!(config.api_key, "test-api-key");
918 std::env::remove_var(FASTER100X_API_KEY);
919 }
920
921 #[test]
922 fn test_faster100x_config_without_env_var() {
923 std::env::remove_var(FASTER100X_API_KEY);
925 let config = Faster100xConfig::default();
926 assert_eq!(config.api_key, "");
927 }
928
929 #[test]
930 fn test_normalize_token_address_with_whitespace() {
931 let result = normalize_token_address(" 0x1234567890123456789012345678901234567890 ");
933 assert!(result.is_ok());
934 assert_eq!(
935 result.unwrap(),
936 "0x1234567890123456789012345678901234567890"
937 );
938 }
939
940 #[test]
941 fn test_normalize_token_address_uppercase_to_lowercase() {
942 let result = normalize_token_address("0X1234567890ABCDEF1234567890ABCDEF12345678");
944 assert!(result.is_ok());
945 assert_eq!(
946 result.unwrap(),
947 "0x1234567890abcdef1234567890abcdef12345678"
948 );
949 }
950
951 #[test]
952 fn test_normalize_token_address_invalid_length_too_long() {
953 let result = normalize_token_address("0x12345678901234567890123456789012345678901");
955 assert!(result.is_err());
956 assert!(result
957 .unwrap_err()
958 .to_string()
959 .contains("Invalid token address format"));
960 }
961
962 #[test]
963 fn test_normalize_token_address_invalid_length_too_short() {
964 let result = normalize_token_address("0x123456789012345678901234567890123456789");
966 assert!(result.is_err());
967 assert!(result
968 .unwrap_err()
969 .to_string()
970 .contains("Invalid token address format"));
971 }
972
973 #[test]
974 fn test_normalize_token_address_missing_0x_prefix() {
975 let result = normalize_token_address("1234567890123456789012345678901234567890");
977 assert!(result.is_err());
978 assert!(result
979 .unwrap_err()
980 .to_string()
981 .contains("Invalid token address format"));
982 }
983
984 #[test]
985 fn test_normalize_token_address_wrong_prefix() {
986 let result = normalize_token_address("0y1234567890123456789012345678901234567890");
988 assert!(result.is_err());
989 assert!(result
990 .unwrap_err()
991 .to_string()
992 .contains("Invalid token address format"));
993 }
994
995 #[test]
996 fn test_normalize_token_address_empty_string() {
997 let result = normalize_token_address("");
999 assert!(result.is_err());
1000 assert!(result
1001 .unwrap_err()
1002 .to_string()
1003 .contains("Invalid token address format"));
1004 }
1005
1006 #[test]
1007 fn test_normalize_token_address_exact_42_chars() {
1008 let result = normalize_token_address("0x1234567890123456789012345678901234567890");
1010 assert!(result.is_ok());
1011 assert_eq!(result.unwrap().len(), 42);
1012 }
1013
1014 #[test]
1015 fn test_whale_activity_serialization() {
1016 let whale_activity = WhaleActivity {
1017 token_address: "0x1234567890123456789012345678901234567890".to_string(),
1018 whale_transactions: vec![WhaleTransaction {
1019 tx_hash: "0xabc123".to_string(),
1020 wallet_address: "0x9876543210987654321098765432109876543210".to_string(),
1021 transaction_type: "buy".to_string(),
1022 token_amount: 1000.0,
1023 usd_value: 50000.0,
1024 timestamp: Utc::now(),
1025 gas_fee: 0.01,
1026 price_impact: 0.5,
1027 }],
1028 total_whale_buys: 100000.0,
1029 total_whale_sells: 50000.0,
1030 net_whale_flow: 50000.0,
1031 active_whales: 5,
1032 timeframe: "24h".to_string(),
1033 timestamp: Utc::now(),
1034 };
1035
1036 let serialized = serde_json::to_string(&whale_activity).unwrap();
1037 assert!(serialized.contains("buy"));
1038 assert!(serialized.contains("24h"));
1039 assert!(serialized.contains("50000"));
1040 }
1041
1042 #[test]
1043 fn test_holder_trends_serialization() {
1044 let holder_trends = HolderTrends {
1045 token_address: "0x1234567890123456789012345678901234567890".to_string(),
1046 data_points: vec![HolderTrendPoint {
1047 timestamp: Utc::now(),
1048 total_holders: 1000,
1049 holder_change: 50,
1050 token_price: 1.5,
1051 whale_percentage: 25.0,
1052 volume_24h: 100000.0,
1053 }],
1054 trend_direction: "increasing".to_string(),
1055 trend_strength: 80,
1056 analysis_period: "30d".to_string(),
1057 insights: vec!["Steady growth in holder count".to_string()],
1058 };
1059
1060 let serialized = serde_json::to_string(&holder_trends).unwrap();
1061 assert!(serialized.contains("increasing"));
1062 assert!(serialized.contains("30d"));
1063 assert!(serialized.contains("Steady growth"));
1064 }
1065
1066 #[test]
1067 fn test_holder_distribution_default_values() {
1068 let distribution = HolderDistribution {
1069 top_1_percent: 0.0,
1070 top_5_percent: 0.0,
1071 top_10_percent: 0.0,
1072 whale_percentage: 0.0,
1073 retail_percentage: 0.0,
1074 gini_coefficient: 0.0,
1075 holder_categories: HashMap::new(),
1076 };
1077
1078 assert_eq!(distribution.top_1_percent, 0.0);
1079 assert_eq!(distribution.gini_coefficient, 0.0);
1080 assert!(distribution.holder_categories.is_empty());
1081 }
1082
1083 #[test]
1084 fn test_wallet_holding_creation() {
1085 let now = Utc::now();
1086 let wallet = WalletHolding {
1087 wallet_address: "0x1234567890123456789012345678901234567890".to_string(),
1088 token_amount: 1000.0,
1089 percentage_of_supply: 5.0,
1090 usd_value: 50000.0,
1091 first_acquired: now,
1092 last_activity: now,
1093 wallet_type: "whale".to_string(),
1094 transaction_count: 25,
1095 avg_holding_time_days: 30.5,
1096 };
1097
1098 assert_eq!(wallet.token_amount, 1000.0);
1099 assert_eq!(wallet.wallet_type, "whale");
1100 assert_eq!(wallet.transaction_count, 25);
1101 }
1102
1103 #[test]
1104 fn test_concentration_risk_values() {
1105 let risk = ConcentrationRisk {
1106 risk_level: "High".to_string(),
1107 risk_score: 85,
1108 wallets_controlling_50_percent: 3,
1109 largest_holder_percentage: 30.0,
1110 exchange_holdings_percentage: 15.0,
1111 locked_holdings_percentage: 5.0,
1112 risk_factors: vec!["Large single holder".to_string()],
1113 };
1114
1115 assert_eq!(risk.risk_score, 85);
1116 assert_eq!(risk.wallets_controlling_50_percent, 3);
1117 assert!(!risk.risk_factors.is_empty());
1118 }
1119
1120 #[test]
1121 fn test_holder_activity_negative_change() {
1122 let activity = HolderActivity {
1123 new_holders_24h: 20,
1124 exited_holders_24h: 30,
1125 net_holder_change_24h: -10,
1126 avg_buy_size_24h: 250.0,
1127 avg_sell_size_24h: 500.0,
1128 holder_growth_rate_7d: -2.5,
1129 };
1130
1131 assert_eq!(activity.net_holder_change_24h, -10);
1132 assert_eq!(activity.holder_growth_rate_7d, -2.5);
1133 }
1134
1135 #[test]
1136 fn test_whale_transaction_values() {
1137 let now = Utc::now();
1138 let tx = WhaleTransaction {
1139 tx_hash: "0xabcdef123456".to_string(),
1140 wallet_address: "0x1234567890123456789012345678901234567890".to_string(),
1141 transaction_type: "sell".to_string(),
1142 token_amount: 5000.0,
1143 usd_value: 100000.0,
1144 timestamp: now,
1145 gas_fee: 0.05,
1146 price_impact: 2.5,
1147 };
1148
1149 assert_eq!(tx.transaction_type, "sell");
1150 assert_eq!(tx.usd_value, 100000.0);
1151 assert_eq!(tx.price_impact, 2.5);
1152 }
1153
1154 #[test]
1155 fn test_holder_trend_point_creation() {
1156 let now = Utc::now();
1157 let point = HolderTrendPoint {
1158 timestamp: now,
1159 total_holders: 1500,
1160 holder_change: 100,
1161 token_price: 2.5,
1162 whale_percentage: 20.0,
1163 volume_24h: 500000.0,
1164 };
1165
1166 assert_eq!(point.total_holders, 1500);
1167 assert_eq!(point.holder_change, 100);
1168 assert_eq!(point.token_price, 2.5);
1169 }
1170
1171 #[test]
1172 fn test_faster100x_config_clone() {
1173 let config = Faster100xConfig {
1174 api_key: "test-key".to_string(),
1175 base_url: "https://test.api.com".to_string(),
1176 rate_limit_per_minute: 50,
1177 };
1178
1179 let cloned = config.clone();
1180 assert_eq!(config.api_key, cloned.api_key);
1181 assert_eq!(config.base_url, cloned.base_url);
1182 assert_eq!(config.rate_limit_per_minute, cloned.rate_limit_per_minute);
1183 }
1184
1185 #[test]
1186 fn test_token_holder_analysis_clone() {
1187 let analysis = TokenHolderAnalysis {
1188 token_address: "0x1234567890123456789012345678901234567890".to_string(),
1189 token_symbol: "TEST".to_string(),
1190 token_name: "Test Token".to_string(),
1191 total_holders: 1000,
1192 unique_holders: 950,
1193 distribution: HolderDistribution {
1194 top_1_percent: 50.0,
1195 top_5_percent: 75.0,
1196 top_10_percent: 85.0,
1197 whale_percentage: 25.0,
1198 retail_percentage: 40.0,
1199 gini_coefficient: 0.65,
1200 holder_categories: HashMap::new(),
1201 },
1202 top_holders: Vec::new(),
1203 concentration_risk: ConcentrationRisk {
1204 risk_level: "Medium".to_string(),
1205 risk_score: 60,
1206 wallets_controlling_50_percent: 5,
1207 largest_holder_percentage: 15.0,
1208 exchange_holdings_percentage: 20.0,
1209 locked_holdings_percentage: 10.0,
1210 risk_factors: Vec::new(),
1211 },
1212 recent_activity: HolderActivity {
1213 new_holders_24h: 50,
1214 exited_holders_24h: 30,
1215 net_holder_change_24h: 20,
1216 avg_buy_size_24h: 500.0,
1217 avg_sell_size_24h: 300.0,
1218 holder_growth_rate_7d: 5.2,
1219 },
1220 timestamp: Utc::now(),
1221 chain: "eth".to_string(),
1222 };
1223
1224 let cloned = analysis.clone();
1225 assert_eq!(analysis.token_symbol, cloned.token_symbol);
1226 assert_eq!(analysis.total_holders, cloned.total_holders);
1227 }
1228
1229 #[test]
1230 fn test_debug_implementations() {
1231 let config = Faster100xConfig::default();
1232 let debug_str = format!("{:?}", config);
1233 assert!(debug_str.contains("Faster100xConfig"));
1234
1235 let distribution = HolderDistribution {
1236 top_1_percent: 50.0,
1237 top_5_percent: 75.0,
1238 top_10_percent: 85.0,
1239 whale_percentage: 25.0,
1240 retail_percentage: 40.0,
1241 gini_coefficient: 0.65,
1242 holder_categories: HashMap::new(),
1243 };
1244 let debug_str = format!("{:?}", distribution);
1245 assert!(debug_str.contains("HolderDistribution"));
1246 }
1247
1248 #[tokio::test]
1249 async fn test_create_faster100x_client_missing_api_key() {
1250 std::env::remove_var(FASTER100X_API_KEY);
1252
1253 let result = create_faster100x_client().await;
1254 assert!(result.is_err());
1255 assert!(result
1256 .unwrap_err()
1257 .to_string()
1258 .contains("FASTER100X_API_KEY environment variable not set"));
1259 }
1260
1261 #[tokio::test]
1262 async fn test_create_faster100x_client_with_api_key() {
1263 std::env::set_var(FASTER100X_API_KEY, "test-api-key");
1265
1266 let result = create_faster100x_client().await;
1267 if result.is_err() {
1270 assert!(!result
1272 .unwrap_err()
1273 .to_string()
1274 .contains("FASTER100X_API_KEY environment variable not set"));
1275 }
1276
1277 std::env::remove_var(FASTER100X_API_KEY);
1278 }
1279}