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