1use crate::chains::TokenAnalytics;
27use crate::error::Result;
28use chrono::{DateTime, Utc};
29use std::path::Path;
30
31const FALLBACK_EXPLORER_TOKEN_BASE: &str = "https://etherscan.io/token";
37
38const DEXSCREENER_BASE: &str = "https://dexscreener.com";
40const GECKOTERMINAL_BASE: &str = "https://www.geckoterminal.com";
42
43pub fn generate_report(analytics: &TokenAnalytics) -> String {
57 let mut report = String::new();
58
59 report.push_str(&generate_header(analytics));
61 report.push_str("\n---\n\n");
62
63 report.push_str(&generate_executive_summary(analytics));
65 report.push_str("\n---\n\n");
66
67 report.push_str(&generate_price_analysis(analytics));
69 report.push_str(&generate_price_chart(analytics));
70 report.push_str("\n---\n\n");
71
72 report.push_str(&generate_volume_analysis(analytics));
74 report.push_str(&generate_volume_chart(analytics));
75 report.push_str("\n---\n\n");
76
77 report.push_str(&generate_liquidity_analysis(analytics));
79 report.push_str(&generate_liquidity_chart(analytics));
80 report.push_str("\n---\n\n");
81
82 report.push_str(&generate_holder_section(analytics));
84 report.push_str("\n---\n\n");
85
86 report.push_str(&generate_concentration_analysis(analytics));
88 report.push_str(&generate_concentration_chart(analytics));
89 report.push_str("\n---\n\n");
90
91 report.push_str(&generate_token_info_section(analytics));
93 report.push_str("\n---\n\n");
94
95 report.push_str(&generate_security_analysis(analytics));
97 report.push_str("\n---\n\n");
98
99 report.push_str(&generate_risk_score_section(analytics));
101 report.push_str("\n---\n\n");
102
103 report.push_str(&generate_risk_indicators(analytics));
105 report.push_str("\n---\n\n");
106
107 report.push_str(&generate_data_sources(analytics));
109
110 report
111}
112
113pub fn token_risk_summary(analytics: &TokenAnalytics) -> TokenRiskSummary {
116 let factors = RiskFactors::from_analytics(analytics);
117 let score = factors.overall_score();
118 let level = factors.risk_level();
119 let emoji = factors.risk_emoji();
120
121 let mut concerns = Vec::new();
122 let mut positives = Vec::new();
123
124 if factors.honeypot >= 7 {
125 concerns.push("Possible honeypot (buys >> sells)".to_string());
126 } else if factors.honeypot <= 3 {
127 positives.push("Normal buy/sell activity".to_string());
128 }
129
130 if factors.concentration >= 7 {
131 concerns.push(format!(
132 "High holder concentration (top holder {:.0}%+)",
133 analytics
134 .holders
135 .first()
136 .map(|h| h.percentage)
137 .unwrap_or(0.0)
138 ));
139 } else if factors.concentration <= 3 {
140 positives.push("Reasonable holder distribution".to_string());
141 }
142
143 if factors.liquidity >= 7 {
144 concerns.push("Very low liquidity".to_string());
145 } else if factors.liquidity <= 3 {
146 positives.push("Good liquidity".to_string());
147 }
148
149 if factors.age >= 7 {
150 concerns.push("Very new token (elevated risk)".to_string());
151 }
152
153 TokenRiskSummary {
154 score,
155 level,
156 emoji,
157 concerns,
158 positives,
159 }
160}
161
162pub struct TokenRiskSummary {
164 pub score: u8,
165 pub level: &'static str,
166 pub emoji: &'static str,
167 pub concerns: Vec<String>,
168 pub positives: Vec<String>,
169}
170
171fn generate_header(analytics: &TokenAnalytics) -> String {
173 let timestamp = DateTime::<Utc>::from_timestamp(analytics.fetched_at, 0)
174 .map(|dt| dt.format("%Y-%m-%d %H:%M:%S UTC").to_string())
175 .unwrap_or_else(|| "Unknown".to_string());
176
177 let mut header = String::new();
178 header.push_str(&format!(
179 "# Token Analysis Report: {}\n\n",
180 analytics.token.symbol
181 ));
182 header.push_str(&format!("**Token Name:** {} \n", analytics.token.name));
183 header.push_str(&format!("**Chain:** {} \n", capitalize(&analytics.chain)));
184 header.push_str(&format!("**Generated:** {} \n", timestamp));
185 header.push_str(&format!(
186 "**Contract:** `{}`\n",
187 analytics.token.contract_address
188 ));
189
190 header
191}
192
193fn generate_executive_summary(analytics: &TokenAnalytics) -> String {
195 let mut summary = String::new();
196 summary.push_str("## Executive Summary\n\n");
197 summary.push_str("| Metric | Value |\n");
198 summary.push_str("|--------|-------|\n");
199 summary.push_str(&format!("| Price | ${:.6} |\n", analytics.price_usd));
200 summary.push_str(&format!(
201 "| 24h Change | {:+.2}% |\n",
202 analytics.price_change_24h
203 ));
204 summary.push_str(&format!(
205 "| 7d Change | {:+.2}% |\n",
206 analytics.price_change_7d
207 ));
208 summary.push_str(&format!(
209 "| 24h Volume | {} |\n",
210 crate::display::format_usd(analytics.volume_24h)
211 ));
212 summary.push_str(&format!(
213 "| 7d Volume | {} |\n",
214 crate::display::format_usd(analytics.volume_7d)
215 ));
216 summary.push_str(&format!(
217 "| Liquidity | {} |\n",
218 crate::display::format_usd(analytics.liquidity_usd)
219 ));
220
221 if let Some(mc) = analytics.market_cap {
222 summary.push_str(&format!(
223 "| Market Cap | {} |\n",
224 crate::display::format_usd(mc)
225 ));
226 }
227
228 if let Some(fdv) = analytics.fdv {
229 summary.push_str(&format!(
230 "| Fully Diluted Valuation | {} |\n",
231 crate::display::format_usd(fdv)
232 ));
233 }
234
235 summary.push_str(&format!(
236 "| Total Holders | {} |\n",
237 format_number(analytics.total_holders as f64)
238 ));
239
240 if let Some(ref supply) = analytics.total_supply {
241 summary.push_str(&format!("| Total Supply | {} |\n", supply));
242 }
243
244 if let Some(ref circ) = analytics.circulating_supply {
245 summary.push_str(&format!("| Circulating Supply | {} |\n", circ));
246 }
247
248 summary
249}
250
251fn generate_price_analysis(analytics: &TokenAnalytics) -> String {
253 let mut section = String::new();
254 section.push_str("## Price Analysis\n\n");
255
256 section.push_str(&format!(
257 "**Current Price:** ${:.6}\n\n",
258 analytics.price_usd
259 ));
260
261 section.push_str("### Price Changes\n\n");
263 section.push_str("| Period | Change |\n");
264 section.push_str("|--------|--------|\n");
265 section.push_str(&format!(
266 "| 24 Hours | {:+.2}% |\n",
267 analytics.price_change_24h
268 ));
269 section.push_str(&format!(
270 "| 7 Days | {:+.2}% |\n",
271 analytics.price_change_7d
272 ));
273
274 if !analytics.price_history.is_empty() {
276 let prices: Vec<f64> = analytics.price_history.iter().map(|p| p.price).collect();
277 let min_price = prices.iter().cloned().fold(f64::INFINITY, f64::min);
278 let max_price = prices.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
279 let avg_price: f64 = prices.iter().sum::<f64>() / prices.len() as f64;
280
281 section.push_str("\n### Price Range (Period)\n\n");
282 section.push_str("| Stat | Value |\n");
283 section.push_str("|------|-------|\n");
284 section.push_str(&format!("| High | ${:.6} |\n", max_price));
285 section.push_str(&format!("| Low | ${:.6} |\n", min_price));
286 section.push_str(&format!("| Average | ${:.6} |\n", avg_price));
287 }
288
289 section
290}
291
292fn generate_volume_analysis(analytics: &TokenAnalytics) -> String {
294 let mut section = String::new();
295 section.push_str("## Volume Analysis\n\n");
296
297 section.push_str("| Period | Volume |\n");
298 section.push_str("|--------|--------|\n");
299 section.push_str(&format!(
300 "| 24 Hours | {} |\n",
301 crate::display::format_usd(analytics.volume_24h)
302 ));
303 section.push_str(&format!(
304 "| 7 Days | {} |\n",
305 crate::display::format_usd(analytics.volume_7d)
306 ));
307
308 if analytics.liquidity_usd > 0.0 {
310 let vol_to_liq = analytics.volume_24h / analytics.liquidity_usd;
311 section.push_str(&format!(
312 "\n**Volume/Liquidity Ratio (24h):** {:.2}x\n",
313 vol_to_liq
314 ));
315
316 if vol_to_liq > 5.0 {
317 section.push_str(
318 "\n> ⚠️ High volume relative to liquidity may indicate unusual trading activity.\n",
319 );
320 }
321 }
322
323 section
324}
325
326fn generate_liquidity_analysis(analytics: &TokenAnalytics) -> String {
328 let mut section = String::new();
329 section.push_str("## Liquidity Analysis\n\n");
330
331 section.push_str(&format!(
332 "**Total Liquidity:** {}\n\n",
333 crate::display::format_usd(analytics.liquidity_usd)
334 ));
335
336 if !analytics.dex_pairs.is_empty() {
337 section.push_str("### Trading Pairs\n\n");
338 section.push_str("| DEX | Pair | Liquidity | 24h Volume | Price |\n");
339 section.push_str("|-----|------|-----------|------------|-------|\n");
340
341 for pair in analytics.dex_pairs.iter().take(10) {
342 section.push_str(&format!(
343 "| {} | {}/{} | {} | {} | ${:.6} |\n",
344 pair.dex_name,
345 pair.base_token,
346 pair.quote_token,
347 crate::display::format_usd(pair.liquidity_usd),
348 crate::display::format_usd(pair.volume_24h),
349 pair.price_usd
350 ));
351 }
352 }
353
354 section
355}
356
357fn generate_holder_section(analytics: &TokenAnalytics) -> String {
359 let mut section = String::new();
360 section.push_str("## Top Holders\n\n");
361
362 if analytics.holders.is_empty() {
363 section.push_str("*No holder data available*\n");
364 return section;
365 }
366
367 section.push_str("| Rank | Address | Balance | % of Supply |\n");
368 section.push_str("|------|---------|---------|-------------|\n");
369
370 for holder in &analytics.holders {
371 section.push_str(&format!(
373 "| {} | `{}` | {} | {:.2}% |\n",
374 holder.rank,
375 holder.address, holder.formatted_balance,
377 holder.percentage
378 ));
379 }
380
381 section
382}
383
384fn generate_concentration_analysis(analytics: &TokenAnalytics) -> String {
386 let mut section = String::new();
387 section.push_str("## Concentration Analysis\n\n");
388
389 let top_10_pct: f64 = analytics
391 .holders
392 .iter()
393 .take(10)
394 .map(|h| h.percentage)
395 .sum();
396
397 let top_50_pct: f64 = analytics
398 .holders
399 .iter()
400 .take(50)
401 .map(|h| h.percentage)
402 .sum();
403
404 let top_100_pct: f64 = analytics
405 .holders
406 .iter()
407 .take(100)
408 .map(|h| h.percentage)
409 .sum();
410
411 let top_10 = analytics.top_10_concentration.unwrap_or(top_10_pct);
413 let top_50 = analytics.top_50_concentration.unwrap_or(top_50_pct);
414 let top_100 = analytics.top_100_concentration.unwrap_or(top_100_pct);
415
416 section.push_str(&format!(
417 "- **Top 10 holders control:** {:.1}% of supply\n",
418 top_10
419 ));
420 section.push_str(&format!(
421 "- **Top 50 holders control:** {:.1}% of supply\n",
422 top_50
423 ));
424 section.push_str(&format!(
425 "- **Top 100 holders control:** {:.1}% of supply\n",
426 top_100
427 ));
428
429 section.push_str("\n### Interpretation\n\n");
431
432 if top_10 > 80.0 {
433 section.push_str("- 🔴 **Very High Concentration:** Top 10 holders control over 80% of supply. This indicates significant centralization risk.\n");
434 } else if top_10 > 50.0 {
435 section.push_str("- 🟠 **High Concentration:** Top 10 holders control over 50% of supply. Moderate centralization risk.\n");
436 } else if top_10 > 25.0 {
437 section.push_str("- 🟡 **Moderate Concentration:** Top 10 holders control 25-50% of supply. Typical for many tokens.\n");
438 } else {
439 section.push_str("- 🟢 **Low Concentration:** Top 10 holders control less than 25% of supply. Well-distributed ownership.\n");
440 }
441
442 section
443}
444
445fn generate_token_info_section(analytics: &TokenAnalytics) -> String {
447 let mut section = String::new();
448 section.push_str("## Token Information\n\n");
449
450 if let Some(ref image_url) = analytics.image_url {
452 section.push_str(&format!("**Token Logo:** [View Image]({})\n\n", image_url));
453 }
454
455 if !analytics.websites.is_empty() {
457 section.push_str("### Websites\n\n");
458 for website in &analytics.websites {
459 section.push_str(&format!("- [{}]({})\n", website, website));
460 }
461 section.push('\n');
462 }
463
464 if !analytics.socials.is_empty() {
466 section.push_str("### Social Media\n\n");
467 for social in &analytics.socials {
468 let icon = match social.platform.to_lowercase().as_str() {
469 "twitter" | "x" => "🐦",
470 "telegram" => "📱",
471 "discord" => "💬",
472 "medium" => "📝",
473 "github" => "💻",
474 "reddit" => "🔴",
475 "youtube" => "📺",
476 "facebook" => "📘",
477 "instagram" => "📷",
478 _ => "🔗",
479 };
480 section.push_str(&format!(
481 "- {} **{}**: [{}]({})\n",
482 icon,
483 capitalize(&social.platform),
484 social.url,
485 social.url
486 ));
487 }
488 section.push('\n');
489 }
490
491 if let Some(ref dexscreener_url) = analytics.dexscreener_url {
493 section.push_str("### Trading Links\n\n");
494 section.push_str(&format!(
495 "- 📊 **DexScreener:** [View on DexScreener]({})\n",
496 dexscreener_url
497 ));
498 section.push('\n');
499 }
500
501 if analytics.image_url.is_none()
503 && analytics.websites.is_empty()
504 && analytics.socials.is_empty()
505 && analytics.dexscreener_url.is_none()
506 {
507 section.push_str("*No additional token metadata available*\n");
508 }
509
510 section
511}
512
513fn generate_security_analysis(analytics: &TokenAnalytics) -> String {
515 let mut section = String::new();
516 section.push_str("## Security Analysis\n\n");
517
518 section.push_str("| Check | Status | Details |\n");
520 section.push_str("|-------|--------|--------|\n");
521
522 let buys = analytics.total_buys_24h;
524 let sells = analytics.total_sells_24h;
525 let (honeypot_status, honeypot_details) = if buys == 0 && sells == 0 {
526 ("⚪ UNKNOWN", "No transaction data available".to_string())
527 } else if sells == 0 && buys > 0 {
528 (
529 "🔴 HIGH",
530 format!("{} buys / 0 sells - Possible honeypot!", buys),
531 )
532 } else {
533 let ratio = if sells > 0 {
534 buys as f64 / sells as f64
535 } else {
536 f64::INFINITY
537 };
538 if ratio > 10.0 {
539 (
540 "🔴 HIGH",
541 format!(
542 "{} buys / {} sells (ratio: {:.2}) - Suspicious activity!",
543 buys, sells, ratio
544 ),
545 )
546 } else if ratio > 3.0 {
547 (
548 "🟠 MEDIUM",
549 format!(
550 "{} buys / {} sells (ratio: {:.2}) - Elevated risk",
551 buys, sells, ratio
552 ),
553 )
554 } else {
555 (
556 "🟢 LOW",
557 format!(
558 "{} buys / {} sells (ratio: {:.2}) - Normal activity",
559 buys, sells, ratio
560 ),
561 )
562 }
563 };
564 section.push_str(&format!(
565 "| Honeypot Risk | {} | {} |\n",
566 honeypot_status, honeypot_details
567 ));
568
569 let (age_status, age_details) = match analytics.token_age_hours {
571 Some(hours) if hours < 24.0 => (
572 "🔴 HIGH RISK",
573 format!("Created {:.1} hours ago - Very new token!", hours),
574 ),
575 Some(hours) if hours < 48.0 => (
576 "🟠 MEDIUM",
577 format!("Created {:.1} hours ago - New token", hours),
578 ),
579 Some(hours) if hours < 168.0 => {
580 let days = hours / 24.0;
582 (
583 "🟡 CAUTION",
584 format!("Created {:.1} days ago - Relatively new", days),
585 )
586 }
587 Some(hours) => {
588 let days = hours / 24.0;
589 if days > 365.0 {
590 let years = days / 365.0;
591 ("🟢 ESTABLISHED", format!("Created {:.1} years ago", years))
592 } else if days > 30.0 {
593 let months = days / 30.0;
594 (
595 "🟢 ESTABLISHED",
596 format!("Created {:.1} months ago", months),
597 )
598 } else {
599 ("🟢 MODERATE", format!("Created {:.1} days ago", days))
600 }
601 }
602 None => ("⚪ UNKNOWN", "Token age data not available".to_string()),
603 };
604 section.push_str(&format!(
605 "| Token Age | {} | {} |\n",
606 age_status, age_details
607 ));
608
609 let top_holder_pct = analytics
611 .holders
612 .first()
613 .map(|h| h.percentage)
614 .unwrap_or(0.0);
615 let (whale_status, whale_details) = if top_holder_pct > 50.0 {
616 (
617 "🔴 HIGH",
618 format!(
619 "Largest holder owns {:.1}% - Extreme concentration!",
620 top_holder_pct
621 ),
622 )
623 } else if top_holder_pct > 25.0 {
624 (
625 "🟠 MEDIUM",
626 format!(
627 "Largest holder owns {:.1}% - High concentration",
628 top_holder_pct
629 ),
630 )
631 } else if top_holder_pct > 10.0 {
632 (
633 "🟡 MODERATE",
634 format!("Largest holder owns {:.1}%", top_holder_pct),
635 )
636 } else if top_holder_pct > 0.0 {
637 (
638 "🟢 LOW",
639 format!(
640 "Largest holder owns {:.1}% - Well distributed",
641 top_holder_pct
642 ),
643 )
644 } else {
645 ("⚪ UNKNOWN", "Holder data not available".to_string())
646 };
647 section.push_str(&format!(
648 "| Whale Risk | {} | {} |\n",
649 whale_status, whale_details
650 ));
651
652 let (social_status, social_details) =
654 if analytics.socials.is_empty() && analytics.websites.is_empty() {
655 (
656 "🟠 NONE",
657 "No verified social links or websites".to_string(),
658 )
659 } else {
660 let social_count = analytics.socials.len();
661 let website_count = analytics.websites.len();
662 (
663 "🟢 PRESENT",
664 format!("{} social links, {} websites", social_count, website_count),
665 )
666 };
667 section.push_str(&format!(
668 "| Social Presence | {} | {} |\n",
669 social_status, social_details
670 ));
671
672 section.push('\n');
673
674 if analytics.total_buys_24h > 0 || analytics.total_sells_24h > 0 {
676 section.push_str(&generate_buysell_chart(analytics));
678 section.push('\n');
679
680 section.push_str(&generate_txn_activity_chart(analytics));
682 section.push('\n');
683 }
684
685 if analytics.total_buys_1h > 0 || analytics.total_sells_1h > 0 {
687 section.push_str("### Recent Activity\n\n");
688 section.push_str(&format!(
689 "- **1h:** {} buys, {} sells\n",
690 analytics.total_buys_1h, analytics.total_sells_1h
691 ));
692 section.push_str(&format!(
693 "- **6h:** {} buys, {} sells\n",
694 analytics.total_buys_6h, analytics.total_sells_6h
695 ));
696 section.push_str(&format!(
697 "- **24h:** {} buys, {} sells\n",
698 analytics.total_buys_24h, analytics.total_sells_24h
699 ));
700 section.push('\n');
701 }
702
703 section
704}
705
706fn generate_buysell_chart(analytics: &TokenAnalytics) -> String {
708 let buys = analytics.total_buys_24h;
709 let sells = analytics.total_sells_24h;
710
711 if buys == 0 && sells == 0 {
712 return String::new();
713 }
714
715 let mut chart = String::new();
716 chart.push_str("### 24h Transaction Distribution\n\n");
717 chart.push_str("```mermaid\n");
718 chart.push_str("pie showData\n");
719 chart.push_str(" title \"24h Buy vs Sell Transactions\"\n");
720 chart.push_str(&format!(" \"Buys\" : {}\n", buys));
721 chart.push_str(&format!(" \"Sells\" : {}\n", sells));
722 chart.push_str("```\n");
723
724 chart
725}
726
727fn generate_txn_activity_chart(analytics: &TokenAnalytics) -> String {
729 if analytics.total_buys_24h == 0
731 && analytics.total_sells_24h == 0
732 && analytics.total_buys_6h == 0
733 && analytics.total_sells_6h == 0
734 && analytics.total_buys_1h == 0
735 && analytics.total_sells_1h == 0
736 {
737 return String::new();
738 }
739
740 let mut chart = String::new();
741 chart.push_str("### Transaction Activity by Period\n\n");
742
743 chart.push_str("| Period | Buys | Sells | Ratio |\n");
746 chart.push_str("|--------|------|-------|-------|\n");
747
748 let periods = [
749 ("1h", analytics.total_buys_1h, analytics.total_sells_1h),
750 ("6h", analytics.total_buys_6h, analytics.total_sells_6h),
751 ("24h", analytics.total_buys_24h, analytics.total_sells_24h),
752 ];
753
754 for (period, buys, sells) in periods {
755 let ratio = if sells > 0 {
756 format!("{:.2}", buys as f64 / sells as f64)
757 } else if buys > 0 {
758 "∞".to_string()
759 } else {
760 "-".to_string()
761 };
762 chart.push_str(&format!(
763 "| {} | {} | {} | {} |\n",
764 period, buys, sells, ratio
765 ));
766 }
767
768 chart.push('\n');
769
770 chart.push_str("```mermaid\n");
772 chart.push_str("xychart-beta\n");
773 chart.push_str(" title \"24h Transaction Volume\"\n");
774 chart.push_str(" x-axis [\"Buys\", \"Sells\"]\n");
775 chart.push_str(" y-axis \"Count\"\n");
776 chart.push_str(&format!(
777 " bar [{}, {}]\n",
778 analytics.total_buys_24h, analytics.total_sells_24h
779 ));
780 chart.push_str("```\n");
781
782 chart
783}
784
785struct RiskFactors {
787 honeypot: u8,
789 age: u8,
791 liquidity: u8,
793 concentration: u8,
795 social: u8,
797}
798
799impl RiskFactors {
800 fn from_analytics(analytics: &TokenAnalytics) -> Self {
802 let honeypot = if analytics.total_buys_24h == 0 && analytics.total_sells_24h == 0 {
804 5 } else if analytics.total_sells_24h == 0 && analytics.total_buys_24h > 0 {
806 10 } else {
808 let ratio = analytics.total_buys_24h as f64 / analytics.total_sells_24h.max(1) as f64;
809 if ratio > 10.0 {
810 9
811 } else if ratio > 5.0 {
812 7
813 } else if ratio > 3.0 {
814 5
815 } else if ratio > 2.0 {
816 3
817 } else {
818 1
819 }
820 };
821
822 let age = match analytics.token_age_hours {
824 Some(hours) if hours < 24.0 => 10,
825 Some(hours) if hours < 48.0 => 8,
826 Some(hours) if hours < 168.0 => 6, Some(hours) if hours < 720.0 => 4, Some(hours) if hours < 2160.0 => 2, Some(_) => 1,
830 None => 5, };
832
833 let liquidity = if analytics.liquidity_usd < 10_000.0 {
835 10
836 } else if analytics.liquidity_usd < 50_000.0 {
837 8
838 } else if analytics.liquidity_usd < 100_000.0 {
839 6
840 } else if analytics.liquidity_usd < 500_000.0 {
841 4
842 } else if analytics.liquidity_usd < 1_000_000.0 {
843 2
844 } else {
845 1
846 };
847
848 let top_holder_pct = analytics
850 .holders
851 .first()
852 .map(|h| h.percentage)
853 .unwrap_or(0.0);
854 let concentration = if top_holder_pct > 50.0 {
855 10
856 } else if top_holder_pct > 30.0 {
857 8
858 } else if top_holder_pct > 20.0 {
859 6
860 } else if top_holder_pct > 10.0 {
861 4
862 } else if top_holder_pct > 5.0 {
863 2
864 } else {
865 1
866 };
867
868 let social = if analytics.socials.is_empty() && analytics.websites.is_empty() {
870 8
871 } else if analytics.socials.is_empty() || analytics.websites.is_empty() {
872 4
873 } else if analytics.socials.len() >= 2 && !analytics.websites.is_empty() {
874 1
875 } else {
876 2
877 };
878
879 RiskFactors {
880 honeypot,
881 age,
882 liquidity,
883 concentration,
884 social,
885 }
886 }
887
888 fn overall_score(&self) -> u8 {
890 let weighted = (self.honeypot as u16 * 3
892 + self.age as u16 * 2
893 + self.liquidity as u16 * 2
894 + self.concentration as u16 * 3
895 + self.social as u16)
896 / 11;
897 weighted.clamp(1, 10) as u8
898 }
899
900 fn risk_level(&self) -> &'static str {
902 match self.overall_score() {
903 1..=3 => "LOW",
904 4..=6 => "MEDIUM",
905 7..=8 => "HIGH",
906 _ => "CRITICAL",
907 }
908 }
909
910 fn risk_emoji(&self) -> &'static str {
912 match self.overall_score() {
913 1..=3 => "🟢",
914 4..=6 => "🟡",
915 7..=8 => "🟠",
916 _ => "🔴",
917 }
918 }
919}
920
921fn generate_risk_score_section(analytics: &TokenAnalytics) -> String {
923 let mut section = String::new();
924 section.push_str("## Risk Score\n\n");
925
926 let factors = RiskFactors::from_analytics(analytics);
927 let overall = factors.overall_score();
928 let level = factors.risk_level();
929 let emoji = factors.risk_emoji();
930
931 section.push_str(&format!(
933 "### Overall Risk: {} {}/10 ({})\n\n",
934 emoji, overall, level
935 ));
936
937 section.push_str("| Factor | Score | Assessment |\n");
939 section.push_str("|--------|-------|------------|\n");
940 section.push_str(&format!(
941 "| Honeypot Risk | {}/10 | {} |\n",
942 factors.honeypot,
943 risk_assessment(factors.honeypot)
944 ));
945 section.push_str(&format!(
946 "| Token Age | {}/10 | {} |\n",
947 factors.age,
948 risk_assessment(factors.age)
949 ));
950 section.push_str(&format!(
951 "| Liquidity | {}/10 | {} |\n",
952 factors.liquidity,
953 risk_assessment(factors.liquidity)
954 ));
955 section.push_str(&format!(
956 "| Concentration | {}/10 | {} |\n",
957 factors.concentration,
958 risk_assessment(factors.concentration)
959 ));
960 section.push_str(&format!(
961 "| Social Presence | {}/10 | {} |\n",
962 factors.social,
963 risk_assessment(factors.social)
964 ));
965 section.push('\n');
966
967 section.push_str(&generate_risk_breakdown_chart(&factors));
969
970 section
971}
972
973fn risk_assessment(score: u8) -> &'static str {
975 match score {
976 0..=2 => "Low Risk",
977 3..=4 => "Moderate",
978 5..=6 => "Elevated",
979 7..=8 => "High Risk",
980 _ => "Critical",
981 }
982}
983
984fn generate_risk_breakdown_chart(factors: &RiskFactors) -> String {
986 let mut chart = String::new();
987 chart.push_str("### Risk Factor Breakdown\n\n");
988 chart.push_str("```mermaid\n");
989 chart.push_str("pie showData\n");
990 chart.push_str(" title \"Risk Factor Contribution\"\n");
991 chart.push_str(&format!(" \"Honeypot\" : {}\n", factors.honeypot));
992 chart.push_str(&format!(" \"Token Age\" : {}\n", factors.age));
993 chart.push_str(&format!(" \"Liquidity\" : {}\n", factors.liquidity));
994 chart.push_str(&format!(
995 " \"Concentration\" : {}\n",
996 factors.concentration
997 ));
998 chart.push_str(&format!(" \"Social\" : {}\n", factors.social));
999 chart.push_str("```\n");
1000
1001 chart
1002}
1003
1004fn generate_risk_indicators(analytics: &TokenAnalytics) -> String {
1005 let mut section = String::new();
1006 section.push_str("## Risk Indicators\n\n");
1007
1008 let mut risks = Vec::new();
1009 let mut positives = Vec::new();
1010
1011 let top_10_pct: f64 = analytics
1013 .holders
1014 .iter()
1015 .take(10)
1016 .map(|h| h.percentage)
1017 .sum();
1018
1019 if top_10_pct > 80.0 {
1020 risks
1021 .push("🔴 **Extreme whale concentration** - Top 10 holders control over 80% of supply");
1022 } else if top_10_pct > 50.0 {
1023 risks.push("🟠 **High whale concentration** - Top 10 holders control over 50% of supply");
1024 } else {
1025 positives.push("🟢 **Reasonable distribution** - No extreme concentration in top holders");
1026 }
1027
1028 if analytics.liquidity_usd < 10_000.0 {
1030 risks.push("🔴 **Very low liquidity** - High slippage risk for trades");
1031 } else if analytics.liquidity_usd < 100_000.0 {
1032 risks.push("🟠 **Low liquidity** - Moderate slippage risk for larger trades");
1033 } else if analytics.liquidity_usd > 1_000_000.0 {
1034 positives.push("🟢 **Good liquidity** - Sufficient depth for most trades");
1035 }
1036
1037 if analytics.volume_24h < 1_000.0 {
1039 risks
1040 .push("🟠 **Very low trading volume** - May indicate low interest or liquidity issues");
1041 } else if analytics.volume_24h > 100_000.0 {
1042 positives.push("🟢 **Active trading** - Healthy trading volume");
1043 }
1044
1045 if analytics.price_change_24h.abs() > 20.0 {
1047 risks.push("🟠 **High price volatility** - Price moved over 20% in 24 hours");
1048 }
1049
1050 if !risks.is_empty() {
1051 section.push_str("### Risk Factors\n\n");
1052 for risk in &risks {
1053 section.push_str(&format!("- {}\n", risk));
1054 }
1055 section.push('\n');
1056 }
1057
1058 if !positives.is_empty() {
1059 section.push_str("### Positive Indicators\n\n");
1060 for positive in &positives {
1061 section.push_str(&format!("- {}\n", positive));
1062 }
1063 }
1064
1065 if risks.is_empty() && positives.is_empty() {
1066 section.push_str("*Insufficient data for risk assessment*\n");
1067 }
1068
1069 section
1070}
1071
1072fn generate_data_sources(analytics: &TokenAnalytics) -> String {
1074 let mut section = String::new();
1075 section.push_str("## Data Sources\n\n");
1076
1077 let chain = &analytics.chain.to_lowercase();
1078 let address = &analytics.token.contract_address;
1079
1080 let explorer_url = crate::chains::chain_metadata(chain)
1082 .map(|m| format!("{}/{}", m.explorer_token_base, address))
1083 .unwrap_or_else(|| format!("{}/{}", FALLBACK_EXPLORER_TOKEN_BASE, address));
1084
1085 section.push_str(&format!(
1086 "- [Block Explorer ({})]({})\n",
1087 capitalize(chain),
1088 explorer_url
1089 ));
1090
1091 section.push_str(&format!(
1092 "- [DexScreener]({}/{}/{})\n",
1093 DEXSCREENER_BASE, chain, address
1094 ));
1095
1096 section.push_str(&format!(
1097 "- [GeckoTerminal]({}/{}/pools/{})\n",
1098 GECKOTERMINAL_BASE, chain, address
1099 ));
1100
1101 section.push_str(&report_footer());
1102
1103 section
1104}
1105
1106pub fn report_footer() -> String {
1109 format!(
1110 "\n---\n\n*Report generated by Scope v{} at {} UTC. Always verify data from primary sources.*",
1111 crate::VERSION,
1112 Utc::now().format("%Y-%m-%d %H:%M:%S")
1113 )
1114}
1115
1116pub fn save_report(report: &str, path: impl AsRef<Path>) -> Result<()> {
1127 std::fs::write(path.as_ref(), report).map_err(|e| {
1128 crate::error::ScopeError::Io(format!(
1129 "Failed to write report to {}: {}",
1130 path.as_ref().display(),
1131 e
1132 ))
1133 })
1134}
1135
1136fn format_number(value: f64) -> String {
1138 if value >= 1_000_000.0 {
1139 format!("{:.2}M", value / 1_000_000.0)
1140 } else if value >= 1_000.0 {
1141 format!("{:.0}K", value / 1_000.0)
1142 } else {
1143 format!("{:.0}", value)
1144 }
1145}
1146
1147fn capitalize(s: &str) -> String {
1149 let mut chars = s.chars();
1150 match chars.next() {
1151 None => String::new(),
1152 Some(first) => first.to_uppercase().chain(chars).collect(),
1153 }
1154}
1155
1156fn generate_price_chart(analytics: &TokenAnalytics) -> String {
1162 let mut chart = String::new();
1164
1165 if analytics.price_change_1h == 0.0
1167 && analytics.price_change_6h == 0.0
1168 && analytics.price_change_24h == 0.0
1169 && analytics.price_change_7d == 0.0
1170 {
1171 if analytics.price_history.len() >= 2 {
1173 return generate_price_history_chart(analytics);
1174 }
1175 return String::new();
1176 }
1177
1178 chart.push_str("\n### Price Changes by Period\n\n");
1179 chart.push_str("```mermaid\n");
1180 chart.push_str("%%{init: {'theme': 'base'}}%%\n");
1181 chart.push_str("xychart-beta\n");
1182 chart.push_str(" title \"Price Change Comparison (%)\"\n");
1183 chart.push_str(" x-axis [\"1h\", \"6h\", \"24h\", \"7d\"]\n");
1184 chart.push_str(" y-axis \"Change %\"\n");
1185 chart.push_str(&format!(
1186 " bar [{:.2}, {:.2}, {:.2}, {:.2}]\n",
1187 analytics.price_change_1h,
1188 analytics.price_change_6h,
1189 analytics.price_change_24h,
1190 analytics.price_change_7d
1191 ));
1192 chart.push_str("```\n");
1193
1194 if analytics.price_history.len() >= 2 {
1196 chart.push_str(&generate_price_history_chart(analytics));
1197 }
1198
1199 chart
1200}
1201
1202fn generate_price_history_chart(analytics: &TokenAnalytics) -> String {
1204 if analytics.price_history.len() < 2 {
1205 return String::new();
1206 }
1207
1208 let mut chart = String::new();
1209 chart.push_str("\n### Price History\n\n");
1210 chart.push_str("```mermaid\n");
1211 chart.push_str("xychart-beta\n");
1212 chart.push_str(" title \"Price Over Time\"\n");
1213 chart.push_str(" x-axis [");
1214
1215 let step = (analytics.price_history.len() / 12).max(1);
1217 let sampled: Vec<_> = analytics
1218 .price_history
1219 .iter()
1220 .step_by(step)
1221 .take(12)
1222 .collect();
1223
1224 let labels: Vec<String> = sampled
1226 .iter()
1227 .enumerate()
1228 .map(|(i, _)| format!("\"{}\"", i + 1))
1229 .collect();
1230 chart.push_str(&labels.join(", "));
1231 chart.push_str("]\n");
1232
1233 let prices: Vec<String> = sampled.iter().map(|p| format!("{:.6}", p.price)).collect();
1235 chart.push_str(" y-axis \"Price (USD)\"\n");
1236 chart.push_str(" line [");
1237 chart.push_str(&prices.join(", "));
1238 chart.push_str("]\n");
1239 chart.push_str("```\n");
1240
1241 chart
1242}
1243
1244fn generate_volume_chart(analytics: &TokenAnalytics) -> String {
1246 if analytics.volume_history.len() < 2 {
1247 return String::new();
1248 }
1249
1250 let mut chart = String::new();
1251 chart.push_str("\n### Volume Chart\n\n");
1252 chart.push_str("```mermaid\n");
1253 chart.push_str("xychart-beta\n");
1254 chart.push_str(" title \"Trading Volume Over Time\"\n");
1255 chart.push_str(" x-axis [");
1256
1257 let step = (analytics.volume_history.len() / 12).max(1);
1259 let sampled: Vec<_> = analytics
1260 .volume_history
1261 .iter()
1262 .step_by(step)
1263 .take(12)
1264 .collect();
1265
1266 let labels: Vec<String> = sampled
1268 .iter()
1269 .enumerate()
1270 .map(|(i, _)| format!("\"{}\"", i + 1))
1271 .collect();
1272 chart.push_str(&labels.join(", "));
1273 chart.push_str("]\n");
1274
1275 let volumes: Vec<String> = sampled.iter().map(|v| format!("{:.0}", v.volume)).collect();
1277 chart.push_str(" y-axis \"Volume (USD)\"\n");
1278 chart.push_str(" bar [");
1279 chart.push_str(&volumes.join(", "));
1280 chart.push_str("]\n");
1281 chart.push_str("```\n");
1282
1283 chart
1284}
1285
1286fn generate_liquidity_chart(analytics: &TokenAnalytics) -> String {
1288 if analytics.dex_pairs.is_empty() {
1289 return String::new();
1290 }
1291
1292 if analytics.dex_pairs.len() < 2 {
1294 return String::new();
1295 }
1296
1297 let mut chart = String::new();
1298 chart.push_str("\n### Liquidity Distribution by DEX\n\n");
1299 chart.push_str("```mermaid\n");
1300 chart.push_str("pie showData\n");
1301 chart.push_str(" title Liquidity by DEX\n");
1302
1303 let mut dex_liquidity: std::collections::HashMap<String, f64> =
1305 std::collections::HashMap::new();
1306 for pair in &analytics.dex_pairs {
1307 *dex_liquidity.entry(pair.dex_name.clone()).or_insert(0.0) += pair.liquidity_usd;
1308 }
1309
1310 let mut sorted: Vec<_> = dex_liquidity.into_iter().collect();
1312 sorted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1313
1314 for (dex, liquidity) in sorted.iter().take(6) {
1315 let value = (liquidity / 1_000_000.0).max(0.01); chart.push_str(&format!(" \"{}\" : {:.2}\n", dex, value));
1318 }
1319
1320 chart.push_str("```\n");
1321
1322 chart
1323}
1324
1325fn generate_concentration_chart(analytics: &TokenAnalytics) -> String {
1327 let top_10_pct: f64 = analytics.top_10_concentration.unwrap_or_else(|| {
1329 analytics
1330 .holders
1331 .iter()
1332 .take(10)
1333 .map(|h| h.percentage)
1334 .sum()
1335 });
1336
1337 if top_10_pct <= 0.0 || analytics.holders.is_empty() {
1339 return String::new();
1340 }
1341
1342 let remaining = (100.0 - top_10_pct).max(0.0);
1343
1344 let mut chart = String::new();
1345 chart.push_str("\n### Holder Concentration Chart\n\n");
1346 chart.push_str("```mermaid\n");
1347 chart.push_str("pie showData\n");
1348 chart.push_str(" title Token Holder Distribution\n");
1349 chart.push_str(&format!(" \"Top 10 Holders\" : {:.1}\n", top_10_pct));
1350 chart.push_str(&format!(" \"Other Holders\" : {:.1}\n", remaining));
1351
1352 let top_50_pct = analytics.top_50_concentration.unwrap_or_else(|| {
1354 analytics
1355 .holders
1356 .iter()
1357 .take(50)
1358 .map(|h| h.percentage)
1359 .sum()
1360 });
1361
1362 if top_50_pct > top_10_pct + 5.0 {
1363 let between_10_50 = top_50_pct - top_10_pct;
1365 let rest = (100.0 - top_50_pct).max(0.0);
1366
1367 chart.clear();
1369 chart.push_str("\n### Holder Concentration Chart\n\n");
1370 chart.push_str("```mermaid\n");
1371 chart.push_str("pie showData\n");
1372 chart.push_str(" title Token Holder Distribution\n");
1373 chart.push_str(&format!(" \"Top 10\" : {:.1}\n", top_10_pct));
1374 chart.push_str(&format!(" \"Rank 11-50\" : {:.1}\n", between_10_50));
1375 chart.push_str(&format!(" \"Others\" : {:.1}\n", rest));
1376 }
1377
1378 chart.push_str("```\n");
1379
1380 chart
1381}
1382
1383#[cfg(test)]
1388mod tests {
1389 use super::*;
1390 use crate::chains::{DexPair, Token, TokenHolder, TokenSocial};
1391
1392 fn create_test_analytics() -> TokenAnalytics {
1393 TokenAnalytics {
1394 token: Token {
1395 contract_address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string(),
1396 symbol: "USDC".to_string(),
1397 name: "USD Coin".to_string(),
1398 decimals: 6,
1399 },
1400 chain: "ethereum".to_string(),
1401 holders: vec![
1402 TokenHolder {
1403 address: "0x55FE002e15bA7591a5E5Ce68a6D3c6E1593d3d8c".to_string(),
1404 balance: "1250000000000000".to_string(),
1405 formatted_balance: "1.25B".to_string(),
1406 percentage: 12.5,
1407 rank: 1,
1408 },
1409 TokenHolder {
1410 address: "0x8894E0a0c962CB723c1976a4421c95949bE2a912".to_string(),
1411 balance: "820000000000000".to_string(),
1412 formatted_balance: "820M".to_string(),
1413 percentage: 8.2,
1414 rank: 2,
1415 },
1416 ],
1417 total_holders: 1234567,
1418 volume_24h: 1234567890.0,
1419 volume_7d: 8641975230.0,
1420 price_usd: 1.0002,
1421 price_change_24h: 0.01,
1422 price_change_7d: -0.05,
1423 liquidity_usd: 500000000.0,
1424 market_cap: Some(32500000000.0),
1425 fdv: Some(40000000000.0),
1426 total_supply: Some("40,000,000,000".to_string()),
1427 circulating_supply: Some("32,500,000,000".to_string()),
1428 price_history: vec![],
1429 volume_history: vec![],
1430 holder_history: vec![],
1431 dex_pairs: vec![DexPair {
1432 dex_name: "Uniswap V3".to_string(),
1433 pair_address: "0x1234".to_string(),
1434 base_token: "USDC".to_string(),
1435 quote_token: "ETH".to_string(),
1436 price_usd: 1.0002,
1437 volume_24h: 500000000.0,
1438 liquidity_usd: 250000000.0,
1439 price_change_24h: 0.01,
1440 buys_24h: 1234,
1441 sells_24h: 1189,
1442 buys_6h: 234,
1443 sells_6h: 220,
1444 buys_1h: 45,
1445 sells_1h: 42,
1446 pair_created_at: Some(1700000000 - 86400 * 30), url: Some("https://dexscreener.com/ethereum/0x1234".to_string()),
1448 }],
1449 fetched_at: 1700000000,
1450 top_10_concentration: Some(45.2),
1451 top_50_concentration: Some(67.8),
1452 top_100_concentration: Some(78.5),
1453 price_change_6h: 0.5,
1454 price_change_1h: -0.1,
1455 total_buys_24h: 1234,
1456 total_sells_24h: 1189,
1457 total_buys_6h: 234,
1458 total_sells_6h: 220,
1459 total_buys_1h: 45,
1460 total_sells_1h: 42,
1461 token_age_hours: Some(720.0), image_url: Some("https://example.com/usdc.png".to_string()),
1463 websites: vec!["https://www.circle.com/usdc".to_string()],
1464 socials: vec![TokenSocial {
1465 platform: "twitter".to_string(),
1466 url: "https://twitter.com/USDC".to_string(),
1467 }],
1468 dexscreener_url: Some("https://dexscreener.com/ethereum/0x1234".to_string()),
1469 }
1470 }
1471
1472 #[test]
1473 fn test_generate_report() {
1474 let analytics = create_test_analytics();
1475 let report = generate_report(&analytics);
1476
1477 assert!(report.contains("# Token Analysis Report: USDC"));
1479 assert!(report.contains("**Chain:** Ethereum"));
1480 assert!(report.contains("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"));
1481
1482 assert!(report.contains("0x55FE002e15bA7591a5E5Ce68a6D3c6E1593d3d8c"));
1484 assert!(report.contains("0x8894E0a0c962CB723c1976a4421c95949bE2a912"));
1485
1486 assert!(report.contains("## Executive Summary"));
1488 assert!(report.contains("## Top Holders"));
1489 assert!(report.contains("## Concentration Analysis"));
1490 assert!(report.contains("## Data Sources"));
1491 }
1492
1493 #[test]
1494 fn test_token_risk_summary() {
1495 let analytics = create_test_analytics();
1496 let summary = token_risk_summary(&analytics);
1497 assert!(summary.score >= 1 && summary.score <= 10);
1498 assert!(!summary.level.is_empty());
1499 assert!(!summary.emoji.is_empty());
1500 }
1501
1502 #[test]
1503 fn test_format_usd() {
1504 assert_eq!(crate::display::format_usd(1500000000.0), "$1.50B");
1505 assert_eq!(crate::display::format_usd(1500000.0), "$1.50M");
1506 assert_eq!(crate::display::format_usd(1500.0), "$1.50K");
1507 assert_eq!(crate::display::format_usd(15.5), "$15.50");
1508 }
1509
1510 #[test]
1511 fn test_capitalize() {
1512 assert_eq!(capitalize("ethereum"), "Ethereum");
1513 assert_eq!(capitalize("bsc"), "Bsc");
1514 assert_eq!(capitalize(""), "");
1515 }
1516
1517 #[test]
1518 fn test_full_addresses_not_truncated() {
1519 let analytics = create_test_analytics();
1520 let section = generate_holder_section(&analytics);
1521
1522 assert!(section.contains("0x55FE002e15bA7591a5E5Ce68a6D3c6E1593d3d8c"));
1524 assert!(section.contains("0x8894E0a0c962CB723c1976a4421c95949bE2a912"));
1525
1526 assert!(!section.contains("..."));
1528 }
1529
1530 #[test]
1531 fn test_concentration_analysis() {
1532 let analytics = create_test_analytics();
1533 let section = generate_concentration_analysis(&analytics);
1534
1535 assert!(section.contains("45.1%") || section.contains("45.2%"));
1536 assert!(section.contains("Top 10 holders"));
1537 }
1538
1539 #[test]
1540 fn test_security_analysis_section() {
1541 let analytics = create_test_analytics();
1542 let section = generate_security_analysis(&analytics);
1543
1544 assert!(section.contains("## Security Analysis"));
1546
1547 assert!(section.contains("Honeypot Risk"));
1549 assert!(section.contains("Token Age"));
1550 assert!(section.contains("Whale Risk"));
1551 assert!(section.contains("Social Presence"));
1552
1553 assert!(section.contains("1234"));
1555 assert!(section.contains("1189"));
1556 }
1557
1558 #[test]
1559 fn test_security_analysis_honeypot_detection() {
1560 let mut analytics = create_test_analytics();
1561
1562 analytics.total_buys_24h = 1000;
1564 analytics.total_sells_24h = 10;
1565 let section = generate_security_analysis(&analytics);
1566 assert!(section.contains("HIGH") || section.contains("Suspicious"));
1567
1568 analytics.total_buys_24h = 100;
1570 analytics.total_sells_24h = 95;
1571 let section = generate_security_analysis(&analytics);
1572 assert!(section.contains("LOW") || section.contains("Normal"));
1573 }
1574
1575 #[test]
1576 fn test_token_info_section() {
1577 let analytics = create_test_analytics();
1578 let section = generate_token_info_section(&analytics);
1579
1580 assert!(section.contains("## Token Information"));
1582
1583 assert!(section.contains("Twitter") || section.contains("twitter"));
1585 assert!(section.contains("https://twitter.com/USDC"));
1586
1587 assert!(section.contains("circle.com"));
1589
1590 assert!(section.contains("DexScreener"));
1592 }
1593
1594 #[test]
1595 fn test_risk_score_calculation() {
1596 let analytics = create_test_analytics();
1597 let factors = RiskFactors::from_analytics(&analytics);
1598
1599 assert!(factors.honeypot <= 10);
1601 assert!(factors.age <= 10);
1602 assert!(factors.liquidity <= 10);
1603 assert!(factors.concentration <= 10);
1604 assert!(factors.social <= 10);
1605
1606 let overall = factors.overall_score();
1608 assert!((1..=10).contains(&overall));
1609 }
1610
1611 #[test]
1612 fn test_risk_score_section() {
1613 let analytics = create_test_analytics();
1614 let section = generate_risk_score_section(&analytics);
1615
1616 assert!(section.contains("## Risk Score"));
1618
1619 assert!(section.contains("Overall Risk:"));
1621 assert!(section.contains("/10"));
1622
1623 assert!(section.contains("Honeypot Risk"));
1625 assert!(section.contains("Token Age"));
1626 assert!(section.contains("Liquidity"));
1627 assert!(section.contains("Concentration"));
1628 assert!(section.contains("Social Presence"));
1629
1630 assert!(section.contains("```mermaid"));
1632 assert!(section.contains("pie showData"));
1633 }
1634
1635 #[test]
1636 fn test_buysell_chart() {
1637 let analytics = create_test_analytics();
1638 let chart = generate_buysell_chart(&analytics);
1639
1640 assert!(chart.contains("```mermaid"));
1642 assert!(chart.contains("pie showData"));
1643 assert!(chart.contains("Buys"));
1644 assert!(chart.contains("Sells"));
1645 }
1646
1647 #[test]
1648 fn test_txn_activity_chart() {
1649 let analytics = create_test_analytics();
1650 let chart = generate_txn_activity_chart(&analytics);
1651
1652 assert!(chart.contains("```mermaid"));
1654 assert!(chart.contains("xychart-beta"));
1655 assert!(chart.contains("1h") || chart.contains("6h") || chart.contains("24h"));
1656 }
1657
1658 #[test]
1659 fn test_price_change_chart() {
1660 let analytics = create_test_analytics();
1661 let chart = generate_price_chart(&analytics);
1662
1663 assert!(chart.contains("```mermaid"));
1665 assert!(chart.contains("Price Change"));
1666 }
1667
1668 #[test]
1669 fn test_new_report_sections_included() {
1670 let analytics = create_test_analytics();
1671 let report = generate_report(&analytics);
1672
1673 assert!(report.contains("## Token Information"));
1675 assert!(report.contains("## Security Analysis"));
1676 assert!(report.contains("## Risk Score"));
1677 }
1678
1679 #[test]
1684 fn test_generate_report_no_holders() {
1685 let mut analytics = create_test_analytics();
1686 analytics.holders = vec![];
1687 analytics.total_holders = 0;
1688 analytics.top_10_concentration = None;
1689 analytics.top_50_concentration = None;
1690 analytics.top_100_concentration = None;
1691 let report = generate_report(&analytics);
1692 assert!(report.contains("No holder data available"));
1693 }
1694
1695 #[test]
1696 fn test_generate_report_no_market_cap() {
1697 let mut analytics = create_test_analytics();
1698 analytics.market_cap = None;
1699 analytics.fdv = None;
1700 analytics.total_supply = None;
1701 analytics.circulating_supply = None;
1702 let report = generate_report(&analytics);
1703 assert!(!report.contains("Market Cap | $"));
1704 assert!(!report.contains("Fully Diluted Valuation | $"));
1705 }
1706
1707 #[test]
1708 fn test_generate_report_no_dex_pairs() {
1709 let mut analytics = create_test_analytics();
1710 analytics.dex_pairs = vec![];
1711 analytics.liquidity_usd = 0.0;
1712 let report = generate_report(&analytics);
1713 assert!(report.contains("## Liquidity Analysis"));
1715 }
1716
1717 #[test]
1718 fn test_generate_report_no_social_no_website() {
1719 let mut analytics = create_test_analytics();
1720 analytics.socials = vec![];
1721 analytics.websites = vec![];
1722 analytics.image_url = None;
1723 analytics.dexscreener_url = None;
1724 let section = generate_token_info_section(&analytics);
1725 assert!(section.contains("No additional token metadata available"));
1726 }
1727
1728 #[test]
1729 fn test_security_analysis_zero_transactions() {
1730 let mut analytics = create_test_analytics();
1731 analytics.total_buys_24h = 0;
1732 analytics.total_sells_24h = 0;
1733 analytics.total_buys_6h = 0;
1734 analytics.total_sells_6h = 0;
1735 analytics.total_buys_1h = 0;
1736 analytics.total_sells_1h = 0;
1737 let section = generate_security_analysis(&analytics);
1738 assert!(section.contains("UNKNOWN") || section.contains("No transaction data"));
1739 }
1740
1741 #[test]
1742 fn test_security_analysis_only_buys() {
1743 let mut analytics = create_test_analytics();
1744 analytics.total_buys_24h = 100;
1745 analytics.total_sells_24h = 0;
1746 let section = generate_security_analysis(&analytics);
1747 assert!(section.contains("Possible honeypot") || section.contains("HIGH"));
1748 }
1749
1750 #[test]
1751 fn test_security_analysis_token_age_very_new() {
1752 let mut analytics = create_test_analytics();
1753 analytics.token_age_hours = Some(6.0);
1754 let section = generate_security_analysis(&analytics);
1755 assert!(section.contains("Very new token") || section.contains("HIGH RISK"));
1756 }
1757
1758 #[test]
1759 fn test_security_analysis_token_age_unknown() {
1760 let mut analytics = create_test_analytics();
1761 analytics.token_age_hours = None;
1762 let section = generate_security_analysis(&analytics);
1763 assert!(section.contains("not available") || section.contains("UNKNOWN"));
1764 }
1765
1766 #[test]
1767 fn test_security_analysis_whale_risk_extreme() {
1768 let mut analytics = create_test_analytics();
1769 analytics.holders = vec![TokenHolder {
1770 address: "0xwhale".to_string(),
1771 balance: "9000000".to_string(),
1772 formatted_balance: "9M".to_string(),
1773 percentage: 60.0,
1774 rank: 1,
1775 }];
1776 let section = generate_security_analysis(&analytics);
1777 assert!(section.contains("HIGH") || section.contains("Extreme concentration"));
1778 }
1779
1780 #[test]
1781 fn test_security_analysis_no_holders() {
1782 let mut analytics = create_test_analytics();
1783 analytics.holders = vec![];
1784 let section = generate_security_analysis(&analytics);
1785 assert!(section.contains("Whale Risk"));
1786 assert!(section.contains("UNKNOWN") || section.contains("not available"));
1787 }
1788
1789 #[test]
1790 fn test_risk_factors_high_risk_token() {
1791 let mut analytics = create_test_analytics();
1792 analytics.total_buys_24h = 1000;
1793 analytics.total_sells_24h = 0; analytics.token_age_hours = Some(12.0); analytics.liquidity_usd = 5_000.0; analytics.holders = vec![TokenHolder {
1797 address: "0x1".to_string(),
1798 balance: "1000".to_string(),
1799 formatted_balance: "1K".to_string(),
1800 percentage: 80.0, rank: 1,
1802 }];
1803 analytics.socials = vec![]; analytics.websites = vec![];
1805
1806 let factors = RiskFactors::from_analytics(&analytics);
1807 assert_eq!(factors.honeypot, 10);
1808 assert_eq!(factors.age, 10);
1809 assert_eq!(factors.liquidity, 10);
1810 assert_eq!(factors.concentration, 10);
1811 assert_eq!(factors.social, 8);
1812 assert!(factors.overall_score() >= 8);
1813 assert!(factors.risk_level() == "HIGH" || factors.risk_level() == "CRITICAL");
1814 assert!(factors.risk_emoji() == "🟠" || factors.risk_emoji() == "🔴");
1815 }
1816
1817 #[test]
1818 fn test_risk_factors_low_risk_token() {
1819 let mut analytics = create_test_analytics();
1820 analytics.total_buys_24h = 100;
1821 analytics.total_sells_24h = 95; analytics.token_age_hours = Some(10_000.0); analytics.liquidity_usd = 50_000_000.0; analytics.holders = vec![TokenHolder {
1825 address: "0x1".to_string(),
1826 balance: "1000".to_string(),
1827 formatted_balance: "1K".to_string(),
1828 percentage: 3.0, rank: 1,
1830 }];
1831 analytics.socials = vec![
1832 TokenSocial {
1833 platform: "twitter".to_string(),
1834 url: "https://twitter.com/test".to_string(),
1835 },
1836 TokenSocial {
1837 platform: "telegram".to_string(),
1838 url: "https://t.me/test".to_string(),
1839 },
1840 ];
1841 analytics.websites = vec!["https://example.com".to_string()];
1842
1843 let factors = RiskFactors::from_analytics(&analytics);
1844 assert!(factors.overall_score() <= 3);
1845 assert_eq!(factors.risk_level(), "LOW");
1846 assert_eq!(factors.risk_emoji(), "🟢");
1847 }
1848
1849 #[test]
1850 fn test_risk_assessment_labels() {
1851 assert_eq!(risk_assessment(0), "Low Risk");
1852 assert_eq!(risk_assessment(1), "Low Risk");
1853 assert_eq!(risk_assessment(3), "Moderate");
1854 assert_eq!(risk_assessment(5), "Elevated");
1855 assert_eq!(risk_assessment(7), "High Risk");
1856 assert_eq!(risk_assessment(9), "Critical");
1857 assert_eq!(risk_assessment(10), "Critical");
1858 }
1859
1860 #[test]
1861 fn test_format_usd_edge_cases() {
1862 assert_eq!(crate::display::format_usd(0.0), "$0.00");
1863 assert_eq!(crate::display::format_usd(0.50), "$0.50");
1864 assert_eq!(crate::display::format_usd(999.0), "$999.00");
1865 }
1866
1867 #[test]
1868 fn test_format_number_edge_cases() {
1869 assert_eq!(format_number(0.0), "0");
1870 assert_eq!(format_number(500.0), "500");
1871 assert_eq!(format_number(1500.0), "2K");
1872 assert_eq!(format_number(1_500_000.0), "1.50M");
1873 }
1874
1875 #[test]
1876 fn test_capitalize_edge_cases() {
1877 assert_eq!(capitalize("a"), "A");
1878 assert_eq!(capitalize("ABC"), "ABC");
1879 }
1880
1881 #[test]
1882 fn test_data_sources_different_chains() {
1883 let chains = vec![
1884 ("ethereum", "etherscan.io"),
1885 ("polygon", "polygonscan.com"),
1886 ("arbitrum", "arbiscan.io"),
1887 ("optimism", "optimistic.etherscan.io"),
1888 ("base", "basescan.org"),
1889 ("bsc", "bscscan.com"),
1890 ("solana", "solscan.io"),
1891 ("tron", "tronscan.org"),
1892 ];
1893
1894 for (chain, expected_domain) in chains {
1895 let mut analytics = create_test_analytics();
1896 analytics.chain = chain.to_string();
1897 let section = generate_data_sources(&analytics);
1898 assert!(
1899 section.contains(expected_domain),
1900 "Chain {} should link to {}",
1901 chain,
1902 expected_domain
1903 );
1904 }
1905 }
1906
1907 #[test]
1908 fn test_buysell_chart_empty() {
1909 let mut analytics = create_test_analytics();
1910 analytics.total_buys_24h = 0;
1911 analytics.total_sells_24h = 0;
1912 let chart = generate_buysell_chart(&analytics);
1913 assert!(chart.is_empty());
1914 }
1915
1916 #[test]
1917 fn test_txn_activity_chart_empty() {
1918 let mut analytics = create_test_analytics();
1919 analytics.total_buys_24h = 0;
1920 analytics.total_sells_24h = 0;
1921 analytics.total_buys_6h = 0;
1922 analytics.total_sells_6h = 0;
1923 analytics.total_buys_1h = 0;
1924 analytics.total_sells_1h = 0;
1925 let chart = generate_txn_activity_chart(&analytics);
1926 assert!(chart.is_empty());
1927 }
1928
1929 #[test]
1930 fn test_volume_chart_empty() {
1931 let analytics = create_test_analytics();
1932 let chart = generate_volume_chart(&analytics);
1934 assert!(chart.is_empty());
1935 }
1936
1937 #[test]
1938 fn test_liquidity_chart_single_pair() {
1939 let analytics = create_test_analytics();
1940 assert_eq!(analytics.dex_pairs.len(), 1);
1942 let chart = generate_liquidity_chart(&analytics);
1943 assert!(chart.is_empty());
1944 }
1945
1946 #[test]
1947 fn test_liquidity_chart_multiple_pairs() {
1948 let mut analytics = create_test_analytics();
1949 analytics.dex_pairs.push(DexPair {
1950 dex_name: "SushiSwap".to_string(),
1951 pair_address: "0x5678".to_string(),
1952 base_token: "USDC".to_string(),
1953 quote_token: "DAI".to_string(),
1954 price_usd: 1.0,
1955 volume_24h: 100_000.0,
1956 liquidity_usd: 5_000_000.0,
1957 price_change_24h: 0.0,
1958 buys_24h: 50,
1959 sells_24h: 50,
1960 buys_6h: 10,
1961 sells_6h: 10,
1962 buys_1h: 2,
1963 sells_1h: 2,
1964 pair_created_at: None,
1965 url: None,
1966 });
1967 let chart = generate_liquidity_chart(&analytics);
1968 assert!(chart.contains("mermaid"));
1969 assert!(chart.contains("Uniswap V3"));
1970 assert!(chart.contains("SushiSwap"));
1971 }
1972
1973 #[test]
1974 fn test_concentration_chart_no_holders() {
1975 let mut analytics = create_test_analytics();
1976 analytics.holders = vec![];
1977 analytics.top_10_concentration = Some(0.0);
1978 let chart = generate_concentration_chart(&analytics);
1979 assert!(chart.is_empty());
1980 }
1981
1982 #[test]
1983 fn test_concentration_analysis_ranges() {
1984 let mut analytics = create_test_analytics();
1986 analytics.top_10_concentration = Some(85.0);
1987 let section = generate_concentration_analysis(&analytics);
1988 assert!(section.contains("Very High Concentration"));
1989
1990 analytics.top_10_concentration = Some(55.0);
1992 let section = generate_concentration_analysis(&analytics);
1993 assert!(section.contains("High Concentration"));
1994
1995 analytics.top_10_concentration = Some(15.0);
1997 let section = generate_concentration_analysis(&analytics);
1998 assert!(section.contains("Low Concentration"));
1999 }
2000
2001 #[test]
2002 fn test_risk_indicators_low_liquidity() {
2003 let mut analytics = create_test_analytics();
2004 analytics.liquidity_usd = 5_000.0;
2005 let section = generate_risk_indicators(&analytics);
2006 assert!(section.contains("Very low liquidity"));
2007 }
2008
2009 #[test]
2010 fn test_risk_indicators_high_volatility() {
2011 let mut analytics = create_test_analytics();
2012 analytics.price_change_24h = 25.0;
2013 let section = generate_risk_indicators(&analytics);
2014 assert!(section.contains("High price volatility"));
2015 }
2016
2017 #[test]
2018 fn test_risk_indicators_healthy_token() {
2019 let mut analytics = create_test_analytics();
2020 analytics.holders = vec![TokenHolder {
2021 address: "0x1".to_string(),
2022 balance: "100".to_string(),
2023 formatted_balance: "100".to_string(),
2024 percentage: 5.0,
2025 rank: 1,
2026 }];
2027 analytics.liquidity_usd = 10_000_000.0;
2028 analytics.volume_24h = 500_000.0;
2029 analytics.price_change_24h = 2.0;
2030 let section = generate_risk_indicators(&analytics);
2031 assert!(section.contains("Reasonable distribution"));
2032 assert!(section.contains("Good liquidity"));
2033 assert!(section.contains("Active trading"));
2034 }
2035
2036 #[test]
2037 fn test_risk_indicators_empty() {
2038 let mut analytics = create_test_analytics();
2039 analytics.holders = vec![];
2040 analytics.liquidity_usd = 500_000.0;
2041 analytics.volume_24h = 50_000.0;
2042 analytics.price_change_24h = 5.0;
2043 let section = generate_risk_indicators(&analytics);
2044 assert!(section.contains("Reasonable distribution"));
2046 }
2047
2048 #[test]
2049 fn test_report_footer() {
2050 let footer = report_footer();
2051 assert!(footer.contains("Scope"));
2052 assert!(footer.contains("UTC"));
2053 assert!(footer.contains("verify"));
2054 }
2055
2056 #[test]
2057 fn test_save_report() {
2058 let tmp = std::env::temp_dir().join("bcc_test_report.md");
2059 let result = save_report("# Test Report\n\nContent here.", &tmp);
2060 assert!(result.is_ok());
2061 let content = std::fs::read_to_string(&tmp).unwrap();
2062 assert!(content.contains("# Test Report"));
2063 let _ = std::fs::remove_file(&tmp);
2064 }
2065
2066 #[test]
2067 fn test_save_report_invalid_path() {
2068 let result = save_report("content", "/nonexistent/directory/report.md");
2069 assert!(result.is_err());
2070 }
2071
2072 #[test]
2073 fn test_volume_analysis_high_vol_to_liq() {
2074 let mut analytics = create_test_analytics();
2075 analytics.volume_24h = 100_000_000.0;
2076 analytics.liquidity_usd = 10_000_000.0; let section = generate_volume_analysis(&analytics);
2078 assert!(section.contains("unusual trading activity"));
2079 }
2080
2081 #[test]
2082 fn test_price_analysis_with_history() {
2083 use crate::chains::PricePoint;
2084 let mut analytics = create_test_analytics();
2085 analytics.price_history = vec![
2086 PricePoint {
2087 timestamp: 1700000000,
2088 price: 1.0,
2089 },
2090 PricePoint {
2091 timestamp: 1700003600,
2092 price: 1.5,
2093 },
2094 PricePoint {
2095 timestamp: 1700007200,
2096 price: 0.8,
2097 },
2098 ];
2099 let section = generate_price_analysis(&analytics);
2100 assert!(section.contains("Price Range"));
2101 assert!(section.contains("High"));
2102 assert!(section.contains("Low"));
2103 assert!(section.contains("Average"));
2104 }
2105
2106 #[test]
2107 fn test_social_platform_icons() {
2108 let mut analytics = create_test_analytics();
2109 analytics.socials = vec![
2110 TokenSocial {
2111 platform: "twitter".to_string(),
2112 url: "https://twitter.com/test".to_string(),
2113 },
2114 TokenSocial {
2115 platform: "telegram".to_string(),
2116 url: "https://t.me/test".to_string(),
2117 },
2118 TokenSocial {
2119 platform: "discord".to_string(),
2120 url: "https://discord.gg/test".to_string(),
2121 },
2122 TokenSocial {
2123 platform: "github".to_string(),
2124 url: "https://github.com/test".to_string(),
2125 },
2126 TokenSocial {
2127 platform: "unknown".to_string(),
2128 url: "https://example.com".to_string(),
2129 },
2130 ];
2131 let section = generate_token_info_section(&analytics);
2132 assert!(section.contains("🐦")); assert!(section.contains("📱")); assert!(section.contains("💬")); assert!(section.contains("💻")); assert!(section.contains("🔗")); }
2138
2139 #[test]
2140 fn test_security_analysis_token_age_ranges() {
2141 let mut analytics = create_test_analytics();
2142
2143 analytics.token_age_hours = Some(6.0);
2145 let section = generate_security_analysis(&analytics);
2146 assert!(section.contains("HIGH RISK"));
2147
2148 analytics.token_age_hours = Some(36.0);
2150 let section = generate_security_analysis(&analytics);
2151 assert!(section.contains("MEDIUM"));
2152
2153 analytics.token_age_hours = Some(120.0);
2155 let section = generate_security_analysis(&analytics);
2156 assert!(section.contains("CAUTION"));
2157
2158 analytics.token_age_hours = Some(10_000.0);
2160 let section = generate_security_analysis(&analytics);
2161 assert!(section.contains("ESTABLISHED"));
2162 }
2163
2164 #[test]
2165 fn test_price_history_chart_with_data() {
2166 use crate::chains::PricePoint;
2167 let mut analytics = create_test_analytics();
2168 analytics.price_history = (0..20)
2169 .map(|i| PricePoint {
2170 timestamp: 1700000000 + i * 3600,
2171 price: 1.0 + (i as f64) * 0.01,
2172 })
2173 .collect();
2174 let chart = generate_price_history_chart(&analytics);
2175 assert!(chart.contains("Price History"));
2176 assert!(chart.contains("mermaid"));
2177 assert!(chart.contains("xychart-beta"));
2178 assert!(chart.contains("line ["));
2179 }
2180
2181 #[test]
2182 fn test_price_chart_with_changes_and_history() {
2183 use crate::chains::PricePoint;
2184 let mut analytics = create_test_analytics();
2185 analytics.price_change_1h = 1.5;
2186 analytics.price_change_6h = -2.3;
2187 analytics.price_change_24h = 5.0;
2188 analytics.price_change_7d = -10.0;
2189 analytics.price_history = (0..5)
2190 .map(|i| PricePoint {
2191 timestamp: 1700000000 + i * 3600,
2192 price: 1.0 + (i as f64) * 0.1,
2193 })
2194 .collect();
2195 let chart = generate_price_chart(&analytics);
2196 assert!(chart.contains("Price Changes by Period"));
2197 assert!(chart.contains("Price History")); }
2199
2200 #[test]
2201 fn test_price_chart_zero_changes_with_history() {
2202 use crate::chains::PricePoint;
2203 let mut analytics = create_test_analytics();
2204 analytics.price_change_1h = 0.0;
2205 analytics.price_change_6h = 0.0;
2206 analytics.price_change_24h = 0.0;
2207 analytics.price_change_7d = 0.0;
2208 analytics.price_history = vec![
2209 PricePoint {
2210 timestamp: 1700000000,
2211 price: 1.0,
2212 },
2213 PricePoint {
2214 timestamp: 1700003600,
2215 price: 1.5,
2216 },
2217 ];
2218 let chart = generate_price_chart(&analytics);
2219 assert!(chart.contains("Price History")); }
2221
2222 #[test]
2223 fn test_volume_chart_with_data() {
2224 use crate::chains::VolumePoint;
2225 let mut analytics = create_test_analytics();
2226 analytics.volume_history = (0..10)
2227 .map(|i| VolumePoint {
2228 timestamp: 1700000000 + i * 3600,
2229 volume: 100_000.0 + (i as f64) * 50_000.0,
2230 })
2231 .collect();
2232 let chart = generate_volume_chart(&analytics);
2233 assert!(chart.contains("Volume Chart"));
2234 assert!(chart.contains("mermaid"));
2235 assert!(chart.contains("bar ["));
2236 }
2237
2238 #[test]
2239 fn test_concentration_chart_three_segments() {
2240 let mut analytics = create_test_analytics();
2241 analytics.top_10_concentration = Some(30.0);
2243 analytics.top_50_concentration = Some(60.0);
2244 let chart = generate_concentration_chart(&analytics);
2245 assert!(chart.contains("Top 10"));
2246 assert!(chart.contains("Rank 11-50"));
2247 assert!(chart.contains("Others"));
2248 }
2249
2250 #[test]
2251 fn test_risk_indicators_very_low_liquidity() {
2252 let mut analytics = create_test_analytics();
2253 analytics.liquidity_usd = 5_000.0;
2254 analytics.volume_24h = 500.0;
2255 let section = generate_risk_indicators(&analytics);
2256 assert!(section.contains("Very low liquidity"));
2257 assert!(section.contains("Very low trading volume"));
2258 }
2259
2260 #[test]
2261 fn test_risk_indicators_moderate_liquidity() {
2262 let mut analytics = create_test_analytics();
2263 analytics.liquidity_usd = 50_000.0;
2264 let section = generate_risk_indicators(&analytics);
2265 assert!(section.contains("Low liquidity"));
2266 }
2267
2268 #[test]
2269 fn test_risk_indicators_extreme_concentration() {
2270 let mut analytics = create_test_analytics();
2271 analytics.holders = vec![TokenHolder {
2272 address: "0xwhale".to_string(),
2273 balance: "900000000".to_string(),
2274 formatted_balance: "900M".to_string(),
2275 percentage: 90.0,
2276 rank: 1,
2277 }];
2278 let section = generate_risk_indicators(&analytics);
2279 assert!(section.contains("Extreme whale concentration"));
2280 }
2281
2282 #[test]
2283 fn test_risk_indicators_no_data() {
2284 let mut analytics = create_test_analytics();
2285 analytics.holders = vec![];
2286 analytics.liquidity_usd = 500_000.0; analytics.volume_24h = 50_000.0; analytics.price_change_24h = 5.0; let section = generate_risk_indicators(&analytics);
2290 assert!(section.contains("Risk Indicators"));
2292 }
2293
2294 #[test]
2295 fn test_holder_section_with_data() {
2296 let analytics = create_test_analytics();
2297 let section = generate_holder_section(&analytics);
2298 assert!(section.contains("Top Holders"));
2299 assert!(section.contains("0x55FE002e15bA7591a5E5Ce68a6D3c6E1593d3d8c")); assert!(section.contains("12.50%"));
2301 }
2302
2303 #[test]
2304 fn test_risk_breakdown_chart() {
2305 let analytics = create_test_analytics();
2306 let section = generate_risk_score_section(&analytics);
2307 assert!(section.contains("Risk Score"));
2308 assert!(section.contains("Risk Factor Breakdown"));
2309 assert!(section.contains("Honeypot"));
2310 assert!(section.contains("Token Age"));
2311 }
2312
2313 #[test]
2314 fn test_data_sources_section() {
2315 let analytics = create_test_analytics();
2316 let section = generate_data_sources(&analytics);
2317 assert!(section.contains("Data Sources"));
2318 assert!(section.contains("ethereum"));
2319 }
2320
2321 #[test]
2322 fn test_volume_analysis_section() {
2323 let analytics = create_test_analytics();
2324 let section = generate_volume_analysis(&analytics);
2325 assert!(section.contains("Volume Analysis"));
2326 }
2327
2328 #[test]
2329 fn test_liquidity_analysis_section() {
2330 let analytics = create_test_analytics();
2331 let section = generate_liquidity_analysis(&analytics);
2332 assert!(section.contains("Liquidity Analysis"));
2333 }
2334
2335 #[test]
2336 fn test_security_analysis_medium_buy_sell_ratio() {
2337 let mut analytics = create_test_analytics();
2338 analytics.total_buys_24h = 100;
2340 analytics.total_sells_24h = 20;
2341 let section = generate_security_analysis(&analytics);
2342 assert!(section.contains("MEDIUM") || section.contains("Elevated"));
2343 }
2344
2345 #[test]
2346 fn test_security_analysis_token_age_months() {
2347 let mut analytics = create_test_analytics();
2348 analytics.token_age_hours = Some(2000.0);
2350 let section = generate_security_analysis(&analytics);
2351 assert!(section.contains("ESTABLISHED") || section.contains("months"));
2352 }
2353
2354 #[test]
2355 fn test_security_analysis_whale_risk_medium() {
2356 let mut analytics = create_test_analytics();
2357 analytics.holders = vec![TokenHolder {
2358 address: "0xwhale".to_string(),
2359 balance: "3000000".to_string(),
2360 formatted_balance: "3M".to_string(),
2361 percentage: 30.0, rank: 1,
2363 }];
2364 let section = generate_security_analysis(&analytics);
2365 assert!(section.contains("MEDIUM") || section.contains("High concentration"));
2366 }
2367
2368 #[test]
2369 fn test_security_analysis_whale_risk_low() {
2370 let mut analytics = create_test_analytics();
2371 analytics.holders = vec![TokenHolder {
2372 address: "0xholder".to_string(),
2373 balance: "500000".to_string(),
2374 formatted_balance: "500K".to_string(),
2375 percentage: 5.0, rank: 1,
2377 }];
2378 let section = generate_security_analysis(&analytics);
2379 assert!(section.contains("LOW") || section.contains("Well distributed"));
2380 }
2381
2382 #[test]
2383 fn test_security_analysis_token_age_days_format() {
2384 let mut analytics = create_test_analytics();
2385 analytics.token_age_hours = Some(480.0);
2387 let section = generate_security_analysis(&analytics);
2388 assert!(section.contains("days ago") || section.contains("MODERATE"));
2389 }
2390
2391 #[test]
2392 fn test_security_buysell_zero_buys_zero_sells_in_period() {
2393 let mut analytics = create_test_analytics();
2394 analytics.total_buys_1h = 0;
2396 analytics.total_sells_1h = 0;
2397 analytics.total_buys_6h = 0;
2398 analytics.total_sells_6h = 0;
2399 analytics.total_buys_24h = 100;
2400 analytics.total_sells_24h = 80;
2401 let section = generate_security_analysis(&analytics);
2402 assert!(section.contains("-") || section.contains("100")); }
2404
2405 #[test]
2406 fn test_risk_factors_various_honeypot_ratios() {
2407 let mut analytics = create_test_analytics();
2408
2409 analytics.total_buys_24h = 110;
2411 analytics.total_sells_24h = 10;
2412 let factors = RiskFactors::from_analytics(&analytics);
2413 assert_eq!(factors.honeypot, 9);
2414
2415 analytics.total_buys_24h = 60;
2417 analytics.total_sells_24h = 10;
2418 let factors = RiskFactors::from_analytics(&analytics);
2419 assert_eq!(factors.honeypot, 7);
2420
2421 analytics.total_buys_24h = 40;
2423 analytics.total_sells_24h = 10;
2424 let factors = RiskFactors::from_analytics(&analytics);
2425 assert_eq!(factors.honeypot, 5);
2426
2427 analytics.total_buys_24h = 25;
2429 analytics.total_sells_24h = 10;
2430 let factors = RiskFactors::from_analytics(&analytics);
2431 assert_eq!(factors.honeypot, 3);
2432
2433 analytics.total_buys_24h = 15;
2435 analytics.total_sells_24h = 10;
2436 let factors = RiskFactors::from_analytics(&analytics);
2437 assert_eq!(factors.honeypot, 1);
2438 }
2439
2440 #[test]
2441 fn test_risk_factors_various_age_thresholds() {
2442 let mut analytics = create_test_analytics();
2443
2444 analytics.token_age_hours = Some(36.0);
2446 let factors = RiskFactors::from_analytics(&analytics);
2447 assert_eq!(factors.age, 8);
2448
2449 analytics.token_age_hours = Some(120.0);
2451 let factors = RiskFactors::from_analytics(&analytics);
2452 assert_eq!(factors.age, 6);
2453
2454 analytics.token_age_hours = Some(500.0);
2456 let factors = RiskFactors::from_analytics(&analytics);
2457 assert_eq!(factors.age, 4);
2458
2459 analytics.token_age_hours = Some(1500.0);
2461 let factors = RiskFactors::from_analytics(&analytics);
2462 assert_eq!(factors.age, 2);
2463 }
2464
2465 #[test]
2466 fn test_risk_factors_various_liquidity_thresholds() {
2467 let mut analytics = create_test_analytics();
2468
2469 analytics.liquidity_usd = 75_000.0;
2471 let factors = RiskFactors::from_analytics(&analytics);
2472 assert_eq!(factors.liquidity, 6);
2473
2474 analytics.liquidity_usd = 200_000.0;
2476 let factors = RiskFactors::from_analytics(&analytics);
2477 assert_eq!(factors.liquidity, 4);
2478
2479 analytics.liquidity_usd = 750_000.0;
2481 let factors = RiskFactors::from_analytics(&analytics);
2482 assert_eq!(factors.liquidity, 2);
2483
2484 analytics.liquidity_usd = 2_000_000.0;
2486 let factors = RiskFactors::from_analytics(&analytics);
2487 assert_eq!(factors.liquidity, 1);
2488 }
2489
2490 #[test]
2491 fn test_risk_factors_various_concentration_thresholds() {
2492 let mut analytics = create_test_analytics();
2493
2494 analytics.holders = vec![TokenHolder {
2496 address: "0x1".to_string(),
2497 balance: "1".to_string(),
2498 formatted_balance: "1".to_string(),
2499 percentage: 35.0,
2500 rank: 1,
2501 }];
2502 let factors = RiskFactors::from_analytics(&analytics);
2503 assert_eq!(factors.concentration, 8);
2504
2505 analytics.holders[0].percentage = 25.0;
2507 let factors = RiskFactors::from_analytics(&analytics);
2508 assert_eq!(factors.concentration, 6);
2509
2510 analytics.holders[0].percentage = 15.0;
2512 let factors = RiskFactors::from_analytics(&analytics);
2513 assert_eq!(factors.concentration, 4);
2514
2515 analytics.holders[0].percentage = 7.0;
2517 let factors = RiskFactors::from_analytics(&analytics);
2518 assert_eq!(factors.concentration, 2);
2519
2520 analytics.holders[0].percentage = 3.0;
2522 let factors = RiskFactors::from_analytics(&analytics);
2523 assert_eq!(factors.concentration, 1);
2524 }
2525
2526 #[test]
2527 fn test_risk_factors_social_one_social() {
2528 let mut analytics = create_test_analytics();
2529 analytics.socials = vec![TokenSocial {
2530 platform: "twitter".to_string(),
2531 url: "https://twitter.com/test".to_string(),
2532 }];
2533 analytics.websites = vec![];
2534 let factors = RiskFactors::from_analytics(&analytics);
2535 assert!(factors.social <= 5);
2537 }
2538
2539 #[test]
2540 fn test_format_number_large_values() {
2541 assert_eq!(format_number(1_500_000_000.0), "1500.00M");
2542 assert_eq!(format_number(500_000.0), "500K");
2543 assert_eq!(format_number(42.0), "42");
2544 }
2545
2546 #[test]
2547 fn test_token_risk_summary_honeypot_concern() {
2548 let mut analytics = create_test_analytics();
2549 analytics.total_buys_24h = 500;
2551 analytics.total_sells_24h = 5;
2552 analytics.total_buys_6h = 100;
2553 analytics.total_sells_6h = 1;
2554 analytics.total_buys_1h = 20;
2555 analytics.total_sells_1h = 0;
2556 if let Some(pair) = analytics.dex_pairs.first_mut() {
2558 pair.buys_24h = 500;
2559 pair.sells_24h = 5;
2560 }
2561 let summary = token_risk_summary(&analytics);
2562 assert!(summary.concerns.iter().any(|c| c.contains("honeypot")));
2563 }
2564
2565 #[test]
2566 fn test_token_risk_summary_low_liquidity_concern() {
2567 let mut analytics = create_test_analytics();
2568 analytics.liquidity_usd = 5_000.0;
2570 if let Some(pair) = analytics.dex_pairs.first_mut() {
2571 pair.liquidity_usd = 5_000.0;
2572 }
2573 let summary = token_risk_summary(&analytics);
2574 assert!(
2575 summary
2576 .concerns
2577 .iter()
2578 .any(|c| c.contains("low liquidity") || c.contains("Low liquidity"))
2579 );
2580 }
2581
2582 #[test]
2583 fn test_token_risk_summary_new_token_concern() {
2584 let mut analytics = create_test_analytics();
2585 analytics.token_age_hours = Some(12.0); let summary = token_risk_summary(&analytics);
2588 assert!(
2589 summary
2590 .concerns
2591 .iter()
2592 .any(|c| c.contains("new token") || c.contains("New token"))
2593 );
2594 }
2595
2596 #[test]
2597 fn test_token_risk_summary_reasonable_distribution() {
2598 let mut analytics = create_test_analytics();
2599 analytics.top_10_concentration = Some(15.0);
2601 analytics.top_50_concentration = Some(30.0);
2602 analytics.top_100_concentration = Some(40.0);
2603 analytics.holders = vec![TokenHolder {
2604 address: "0x1111".to_string(),
2605 balance: "1000".to_string(),
2606 formatted_balance: "1000".to_string(),
2607 percentage: 3.0,
2608 rank: 1,
2609 }];
2610 let summary = token_risk_summary(&analytics);
2611 assert!(
2612 summary
2613 .positives
2614 .iter()
2615 .any(|p| p.contains("holder distribution") || p.contains("distribution"))
2616 );
2617 }
2618
2619 #[test]
2620 fn test_risk_factors_no_buys_no_sells() {
2621 let mut analytics = create_test_analytics();
2622 analytics.total_buys_24h = 0;
2623 analytics.total_sells_24h = 0;
2624 let factors = RiskFactors::from_analytics(&analytics);
2625 assert_eq!(factors.honeypot, 5);
2627 }
2628
2629 #[test]
2630 fn test_risk_factors_zero_sells_positive_buys() {
2631 let mut analytics = create_test_analytics();
2632 analytics.total_buys_24h = 50;
2633 analytics.total_sells_24h = 0;
2634 let factors = RiskFactors::from_analytics(&analytics);
2635 assert_eq!(factors.honeypot, 10); }
2637
2638 #[test]
2639 fn test_risk_factors_unknown_age() {
2640 let mut analytics = create_test_analytics();
2641 analytics.token_age_hours = None;
2642 let factors = RiskFactors::from_analytics(&analytics);
2643 assert_eq!(factors.age, 5); }
2645
2646 #[test]
2647 fn test_risk_factors_very_low_liquidity() {
2648 let mut analytics = create_test_analytics();
2649 analytics.liquidity_usd = 8_000.0;
2650 let factors = RiskFactors::from_analytics(&analytics);
2651 assert_eq!(factors.liquidity, 10); }
2653
2654 #[test]
2655 fn test_risk_factors_moderate_liquidity() {
2656 let mut analytics = create_test_analytics();
2657 analytics.liquidity_usd = 75_000.0;
2658 let factors = RiskFactors::from_analytics(&analytics);
2659 assert_eq!(factors.liquidity, 6);
2660 }
2661
2662 #[test]
2663 fn test_security_analysis_zero_buys_sells() {
2664 let mut analytics = create_test_analytics();
2665 analytics.total_buys_24h = 0;
2666 analytics.total_sells_24h = 0;
2667 let section = generate_security_analysis(&analytics);
2668 assert!(section.contains("UNKNOWN") || section.contains("No transaction data"));
2669 }
2670
2671 #[test]
2672 fn test_security_analysis_sells_zero_buys_positive() {
2673 let mut analytics = create_test_analytics();
2674 analytics.total_buys_24h = 100;
2675 analytics.total_sells_24h = 0;
2676 let section = generate_security_analysis(&analytics);
2677 assert!(section.contains("HIGH") || section.contains("honeypot"));
2678 }
2679
2680 #[test]
2685 fn test_risk_factors_liquidity_10k_to_50k() {
2686 let mut analytics = create_test_analytics();
2688 analytics.liquidity_usd = 25_000.0;
2689 let factors = RiskFactors::from_analytics(&analytics);
2690 assert_eq!(factors.liquidity, 8);
2691 }
2692
2693 #[test]
2694 fn test_price_chart_all_zeros_no_history() {
2695 let mut analytics = create_test_analytics();
2697 analytics.price_change_1h = 0.0;
2698 analytics.price_change_6h = 0.0;
2699 analytics.price_change_24h = 0.0;
2700 analytics.price_change_7d = 0.0;
2701 analytics.price_history = vec![];
2702 let chart = generate_price_chart(&analytics);
2703 assert!(chart.is_empty());
2704 }
2705
2706 #[test]
2707 fn test_price_history_chart_too_few_points() {
2708 let mut analytics = create_test_analytics();
2710 analytics.price_history = vec![crate::chains::PricePoint {
2711 timestamp: 1700000000,
2712 price: 1.0,
2713 }];
2714 let chart = generate_price_history_chart(&analytics);
2715 assert!(chart.is_empty());
2716 }
2717
2718 #[test]
2719 fn test_concentration_chart_top50_fallback_calculation() {
2720 let mut analytics = create_test_analytics();
2723 analytics.top_10_concentration = Some(20.0);
2724 analytics.top_50_concentration = None; analytics.holders = (0..10)
2728 .map(|i| TokenHolder {
2729 address: format!("0xholder{:02}", i),
2730 balance: "100".to_string(),
2731 formatted_balance: "100".to_string(),
2732 percentage: 2.0,
2733 rank: i as u32 + 1,
2734 })
2735 .chain((10..15).map(|i| TokenHolder {
2736 address: format!("0xholder{:02}", i),
2737 balance: "200".to_string(),
2738 formatted_balance: "200".to_string(),
2739 percentage: 4.0,
2740 rank: i as u32 + 1,
2741 }))
2742 .collect();
2743 let chart = generate_concentration_chart(&analytics);
2744 assert!(chart.contains("Holder Concentration"));
2745 assert!(chart.contains("Rank 11-50")); assert!(chart.contains("Top 10"));
2747 assert!(chart.contains("Others"));
2748 }
2749
2750 #[test]
2751 fn test_price_chart_all_zeros_with_history() {
2752 let mut analytics = create_test_analytics();
2755 analytics.price_change_1h = 0.0;
2756 analytics.price_change_6h = 0.0;
2757 analytics.price_change_24h = 0.0;
2758 analytics.price_change_7d = 0.0;
2759 analytics.price_history = vec![
2760 crate::chains::PricePoint {
2761 timestamp: 1700000000,
2762 price: 1.0,
2763 },
2764 crate::chains::PricePoint {
2765 timestamp: 1700003600,
2766 price: 1.01,
2767 },
2768 ];
2769 let chart = generate_price_chart(&analytics);
2770 assert!(chart.contains("Price History"));
2771 }
2772}