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