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("\n---\n\n");
1060 section.push_str("*This report was generated automatically. Always verify data from primary sources before making decisions.*\n");
1061
1062 section
1063}
1064
1065pub fn save_report(report: &str, path: impl AsRef<Path>) -> Result<()> {
1076 std::fs::write(path.as_ref(), report).map_err(|e| {
1077 crate::error::ScopeError::Io(format!(
1078 "Failed to write report to {}: {}",
1079 path.as_ref().display(),
1080 e
1081 ))
1082 })
1083}
1084
1085fn format_usd(value: f64) -> String {
1087 if value >= 1_000_000_000.0 {
1088 format!("${:.2}B", value / 1_000_000_000.0)
1089 } else if value >= 1_000_000.0 {
1090 format!("${:.2}M", value / 1_000_000.0)
1091 } else if value >= 1_000.0 {
1092 format!("${:.0}K", value / 1_000.0)
1093 } else {
1094 format!("${:.2}", value)
1095 }
1096}
1097
1098fn format_number(value: f64) -> String {
1100 if value >= 1_000_000.0 {
1101 format!("{:.2}M", value / 1_000_000.0)
1102 } else if value >= 1_000.0 {
1103 format!("{:.0}K", value / 1_000.0)
1104 } else {
1105 format!("{:.0}", value)
1106 }
1107}
1108
1109fn capitalize(s: &str) -> String {
1111 let mut chars = s.chars();
1112 match chars.next() {
1113 None => String::new(),
1114 Some(first) => first.to_uppercase().chain(chars).collect(),
1115 }
1116}
1117
1118fn generate_price_chart(analytics: &TokenAnalytics) -> String {
1124 let mut chart = String::new();
1126
1127 if analytics.price_change_1h == 0.0
1129 && analytics.price_change_6h == 0.0
1130 && analytics.price_change_24h == 0.0
1131 && analytics.price_change_7d == 0.0
1132 {
1133 if analytics.price_history.len() >= 2 {
1135 return generate_price_history_chart(analytics);
1136 }
1137 return String::new();
1138 }
1139
1140 chart.push_str("\n### Price Changes by Period\n\n");
1141 chart.push_str("```mermaid\n");
1142 chart.push_str("%%{init: {'theme': 'base'}}%%\n");
1143 chart.push_str("xychart-beta\n");
1144 chart.push_str(" title \"Price Change Comparison (%)\"\n");
1145 chart.push_str(" x-axis [\"1h\", \"6h\", \"24h\", \"7d\"]\n");
1146 chart.push_str(" y-axis \"Change %\"\n");
1147 chart.push_str(&format!(
1148 " bar [{:.2}, {:.2}, {:.2}, {:.2}]\n",
1149 analytics.price_change_1h,
1150 analytics.price_change_6h,
1151 analytics.price_change_24h,
1152 analytics.price_change_7d
1153 ));
1154 chart.push_str("```\n");
1155
1156 if analytics.price_history.len() >= 2 {
1158 chart.push_str(&generate_price_history_chart(analytics));
1159 }
1160
1161 chart
1162}
1163
1164fn generate_price_history_chart(analytics: &TokenAnalytics) -> String {
1166 if analytics.price_history.len() < 2 {
1167 return String::new();
1168 }
1169
1170 let mut chart = String::new();
1171 chart.push_str("\n### Price History\n\n");
1172 chart.push_str("```mermaid\n");
1173 chart.push_str("xychart-beta\n");
1174 chart.push_str(" title \"Price Over Time\"\n");
1175 chart.push_str(" x-axis [");
1176
1177 let step = (analytics.price_history.len() / 12).max(1);
1179 let sampled: Vec<_> = analytics
1180 .price_history
1181 .iter()
1182 .step_by(step)
1183 .take(12)
1184 .collect();
1185
1186 let labels: Vec<String> = sampled
1188 .iter()
1189 .enumerate()
1190 .map(|(i, _)| format!("\"{}\"", i + 1))
1191 .collect();
1192 chart.push_str(&labels.join(", "));
1193 chart.push_str("]\n");
1194
1195 let prices: Vec<String> = sampled.iter().map(|p| format!("{:.6}", p.price)).collect();
1197 chart.push_str(" y-axis \"Price (USD)\"\n");
1198 chart.push_str(" line [");
1199 chart.push_str(&prices.join(", "));
1200 chart.push_str("]\n");
1201 chart.push_str("```\n");
1202
1203 chart
1204}
1205
1206fn generate_volume_chart(analytics: &TokenAnalytics) -> String {
1208 if analytics.volume_history.len() < 2 {
1209 return String::new();
1210 }
1211
1212 let mut chart = String::new();
1213 chart.push_str("\n### Volume Chart\n\n");
1214 chart.push_str("```mermaid\n");
1215 chart.push_str("xychart-beta\n");
1216 chart.push_str(" title \"Trading Volume Over Time\"\n");
1217 chart.push_str(" x-axis [");
1218
1219 let step = (analytics.volume_history.len() / 12).max(1);
1221 let sampled: Vec<_> = analytics
1222 .volume_history
1223 .iter()
1224 .step_by(step)
1225 .take(12)
1226 .collect();
1227
1228 let labels: Vec<String> = sampled
1230 .iter()
1231 .enumerate()
1232 .map(|(i, _)| format!("\"{}\"", i + 1))
1233 .collect();
1234 chart.push_str(&labels.join(", "));
1235 chart.push_str("]\n");
1236
1237 let volumes: Vec<String> = sampled.iter().map(|v| format!("{:.0}", v.volume)).collect();
1239 chart.push_str(" y-axis \"Volume (USD)\"\n");
1240 chart.push_str(" bar [");
1241 chart.push_str(&volumes.join(", "));
1242 chart.push_str("]\n");
1243 chart.push_str("```\n");
1244
1245 chart
1246}
1247
1248fn generate_liquidity_chart(analytics: &TokenAnalytics) -> String {
1250 if analytics.dex_pairs.is_empty() {
1251 return String::new();
1252 }
1253
1254 if analytics.dex_pairs.len() < 2 {
1256 return String::new();
1257 }
1258
1259 let mut chart = String::new();
1260 chart.push_str("\n### Liquidity Distribution by DEX\n\n");
1261 chart.push_str("```mermaid\n");
1262 chart.push_str("pie showData\n");
1263 chart.push_str(" title Liquidity by DEX\n");
1264
1265 let mut dex_liquidity: std::collections::HashMap<String, f64> =
1267 std::collections::HashMap::new();
1268 for pair in &analytics.dex_pairs {
1269 *dex_liquidity.entry(pair.dex_name.clone()).or_insert(0.0) += pair.liquidity_usd;
1270 }
1271
1272 let mut sorted: Vec<_> = dex_liquidity.into_iter().collect();
1274 sorted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1275
1276 for (dex, liquidity) in sorted.iter().take(6) {
1277 let value = (liquidity / 1_000_000.0).max(0.01); chart.push_str(&format!(" \"{}\" : {:.2}\n", dex, value));
1280 }
1281
1282 chart.push_str("```\n");
1283
1284 chart
1285}
1286
1287fn generate_concentration_chart(analytics: &TokenAnalytics) -> String {
1289 let top_10_pct: f64 = analytics.top_10_concentration.unwrap_or_else(|| {
1291 analytics
1292 .holders
1293 .iter()
1294 .take(10)
1295 .map(|h| h.percentage)
1296 .sum()
1297 });
1298
1299 if top_10_pct <= 0.0 || analytics.holders.is_empty() {
1301 return String::new();
1302 }
1303
1304 let remaining = (100.0 - top_10_pct).max(0.0);
1305
1306 let mut chart = String::new();
1307 chart.push_str("\n### Holder Concentration Chart\n\n");
1308 chart.push_str("```mermaid\n");
1309 chart.push_str("pie showData\n");
1310 chart.push_str(" title Token Holder Distribution\n");
1311 chart.push_str(&format!(" \"Top 10 Holders\" : {:.1}\n", top_10_pct));
1312 chart.push_str(&format!(" \"Other Holders\" : {:.1}\n", remaining));
1313
1314 let top_50_pct = analytics.top_50_concentration.unwrap_or_else(|| {
1316 analytics
1317 .holders
1318 .iter()
1319 .take(50)
1320 .map(|h| h.percentage)
1321 .sum()
1322 });
1323
1324 if top_50_pct > top_10_pct + 5.0 {
1325 let between_10_50 = top_50_pct - top_10_pct;
1327 let rest = (100.0 - top_50_pct).max(0.0);
1328
1329 chart.clear();
1331 chart.push_str("\n### Holder Concentration Chart\n\n");
1332 chart.push_str("```mermaid\n");
1333 chart.push_str("pie showData\n");
1334 chart.push_str(" title Token Holder Distribution\n");
1335 chart.push_str(&format!(" \"Top 10\" : {:.1}\n", top_10_pct));
1336 chart.push_str(&format!(" \"Rank 11-50\" : {:.1}\n", between_10_50));
1337 chart.push_str(&format!(" \"Others\" : {:.1}\n", rest));
1338 }
1339
1340 chart.push_str("```\n");
1341
1342 chart
1343}
1344
1345#[cfg(test)]
1350mod tests {
1351 use super::*;
1352 use crate::chains::{DexPair, Token, TokenHolder, TokenSocial};
1353
1354 fn create_test_analytics() -> TokenAnalytics {
1355 TokenAnalytics {
1356 token: Token {
1357 contract_address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string(),
1358 symbol: "USDC".to_string(),
1359 name: "USD Coin".to_string(),
1360 decimals: 6,
1361 },
1362 chain: "ethereum".to_string(),
1363 holders: vec![
1364 TokenHolder {
1365 address: "0x55FE002e15bA7591a5E5Ce68a6D3c6E1593d3d8c".to_string(),
1366 balance: "1250000000000000".to_string(),
1367 formatted_balance: "1.25B".to_string(),
1368 percentage: 12.5,
1369 rank: 1,
1370 },
1371 TokenHolder {
1372 address: "0x8894E0a0c962CB723c1976a4421c95949bE2a912".to_string(),
1373 balance: "820000000000000".to_string(),
1374 formatted_balance: "820M".to_string(),
1375 percentage: 8.2,
1376 rank: 2,
1377 },
1378 ],
1379 total_holders: 1234567,
1380 volume_24h: 1234567890.0,
1381 volume_7d: 8641975230.0,
1382 price_usd: 1.0002,
1383 price_change_24h: 0.01,
1384 price_change_7d: -0.05,
1385 liquidity_usd: 500000000.0,
1386 market_cap: Some(32500000000.0),
1387 fdv: Some(40000000000.0),
1388 total_supply: Some("40,000,000,000".to_string()),
1389 circulating_supply: Some("32,500,000,000".to_string()),
1390 price_history: vec![],
1391 volume_history: vec![],
1392 holder_history: vec![],
1393 dex_pairs: vec![DexPair {
1394 dex_name: "Uniswap V3".to_string(),
1395 pair_address: "0x1234".to_string(),
1396 base_token: "USDC".to_string(),
1397 quote_token: "ETH".to_string(),
1398 price_usd: 1.0002,
1399 volume_24h: 500000000.0,
1400 liquidity_usd: 250000000.0,
1401 price_change_24h: 0.01,
1402 buys_24h: 1234,
1403 sells_24h: 1189,
1404 buys_6h: 234,
1405 sells_6h: 220,
1406 buys_1h: 45,
1407 sells_1h: 42,
1408 pair_created_at: Some(1700000000 - 86400 * 30), url: Some("https://dexscreener.com/ethereum/0x1234".to_string()),
1410 }],
1411 fetched_at: 1700000000,
1412 top_10_concentration: Some(45.2),
1413 top_50_concentration: Some(67.8),
1414 top_100_concentration: Some(78.5),
1415 price_change_6h: 0.5,
1416 price_change_1h: -0.1,
1417 total_buys_24h: 1234,
1418 total_sells_24h: 1189,
1419 total_buys_6h: 234,
1420 total_sells_6h: 220,
1421 total_buys_1h: 45,
1422 total_sells_1h: 42,
1423 token_age_hours: Some(720.0), image_url: Some("https://example.com/usdc.png".to_string()),
1425 websites: vec!["https://www.circle.com/usdc".to_string()],
1426 socials: vec![TokenSocial {
1427 platform: "twitter".to_string(),
1428 url: "https://twitter.com/USDC".to_string(),
1429 }],
1430 dexscreener_url: Some("https://dexscreener.com/ethereum/0x1234".to_string()),
1431 }
1432 }
1433
1434 #[test]
1435 fn test_generate_report() {
1436 let analytics = create_test_analytics();
1437 let report = generate_report(&analytics);
1438
1439 assert!(report.contains("# Token Analysis Report: USDC"));
1441 assert!(report.contains("**Chain:** Ethereum"));
1442 assert!(report.contains("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"));
1443
1444 assert!(report.contains("0x55FE002e15bA7591a5E5Ce68a6D3c6E1593d3d8c"));
1446 assert!(report.contains("0x8894E0a0c962CB723c1976a4421c95949bE2a912"));
1447
1448 assert!(report.contains("## Executive Summary"));
1450 assert!(report.contains("## Top Holders"));
1451 assert!(report.contains("## Concentration Analysis"));
1452 assert!(report.contains("## Data Sources"));
1453 }
1454
1455 #[test]
1456 fn test_format_usd() {
1457 assert_eq!(format_usd(1500000000.0), "$1.50B");
1458 assert_eq!(format_usd(1500000.0), "$1.50M");
1459 assert_eq!(format_usd(1500.0), "$2K"); assert_eq!(format_usd(15.5), "$15.50");
1461 }
1462
1463 #[test]
1464 fn test_capitalize() {
1465 assert_eq!(capitalize("ethereum"), "Ethereum");
1466 assert_eq!(capitalize("bsc"), "Bsc");
1467 assert_eq!(capitalize(""), "");
1468 }
1469
1470 #[test]
1471 fn test_full_addresses_not_truncated() {
1472 let analytics = create_test_analytics();
1473 let section = generate_holder_section(&analytics);
1474
1475 assert!(section.contains("0x55FE002e15bA7591a5E5Ce68a6D3c6E1593d3d8c"));
1477 assert!(section.contains("0x8894E0a0c962CB723c1976a4421c95949bE2a912"));
1478
1479 assert!(!section.contains("..."));
1481 }
1482
1483 #[test]
1484 fn test_concentration_analysis() {
1485 let analytics = create_test_analytics();
1486 let section = generate_concentration_analysis(&analytics);
1487
1488 assert!(section.contains("45.1%") || section.contains("45.2%"));
1489 assert!(section.contains("Top 10 holders"));
1490 }
1491
1492 #[test]
1493 fn test_security_analysis_section() {
1494 let analytics = create_test_analytics();
1495 let section = generate_security_analysis(&analytics);
1496
1497 assert!(section.contains("## Security Analysis"));
1499
1500 assert!(section.contains("Honeypot Risk"));
1502 assert!(section.contains("Token Age"));
1503 assert!(section.contains("Whale Risk"));
1504 assert!(section.contains("Social Presence"));
1505
1506 assert!(section.contains("1234"));
1508 assert!(section.contains("1189"));
1509 }
1510
1511 #[test]
1512 fn test_security_analysis_honeypot_detection() {
1513 let mut analytics = create_test_analytics();
1514
1515 analytics.total_buys_24h = 1000;
1517 analytics.total_sells_24h = 10;
1518 let section = generate_security_analysis(&analytics);
1519 assert!(section.contains("HIGH") || section.contains("Suspicious"));
1520
1521 analytics.total_buys_24h = 100;
1523 analytics.total_sells_24h = 95;
1524 let section = generate_security_analysis(&analytics);
1525 assert!(section.contains("LOW") || section.contains("Normal"));
1526 }
1527
1528 #[test]
1529 fn test_token_info_section() {
1530 let analytics = create_test_analytics();
1531 let section = generate_token_info_section(&analytics);
1532
1533 assert!(section.contains("## Token Information"));
1535
1536 assert!(section.contains("Twitter") || section.contains("twitter"));
1538 assert!(section.contains("https://twitter.com/USDC"));
1539
1540 assert!(section.contains("circle.com"));
1542
1543 assert!(section.contains("DexScreener"));
1545 }
1546
1547 #[test]
1548 fn test_risk_score_calculation() {
1549 let analytics = create_test_analytics();
1550 let factors = RiskFactors::from_analytics(&analytics);
1551
1552 assert!(factors.honeypot <= 10);
1554 assert!(factors.age <= 10);
1555 assert!(factors.liquidity <= 10);
1556 assert!(factors.concentration <= 10);
1557 assert!(factors.social <= 10);
1558
1559 let overall = factors.overall_score();
1561 assert!((1..=10).contains(&overall));
1562 }
1563
1564 #[test]
1565 fn test_risk_score_section() {
1566 let analytics = create_test_analytics();
1567 let section = generate_risk_score_section(&analytics);
1568
1569 assert!(section.contains("## Risk Score"));
1571
1572 assert!(section.contains("Overall Risk:"));
1574 assert!(section.contains("/10"));
1575
1576 assert!(section.contains("Honeypot Risk"));
1578 assert!(section.contains("Token Age"));
1579 assert!(section.contains("Liquidity"));
1580 assert!(section.contains("Concentration"));
1581 assert!(section.contains("Social Presence"));
1582
1583 assert!(section.contains("```mermaid"));
1585 assert!(section.contains("pie showData"));
1586 }
1587
1588 #[test]
1589 fn test_buysell_chart() {
1590 let analytics = create_test_analytics();
1591 let chart = generate_buysell_chart(&analytics);
1592
1593 assert!(chart.contains("```mermaid"));
1595 assert!(chart.contains("pie showData"));
1596 assert!(chart.contains("Buys"));
1597 assert!(chart.contains("Sells"));
1598 }
1599
1600 #[test]
1601 fn test_txn_activity_chart() {
1602 let analytics = create_test_analytics();
1603 let chart = generate_txn_activity_chart(&analytics);
1604
1605 assert!(chart.contains("```mermaid"));
1607 assert!(chart.contains("xychart-beta"));
1608 assert!(chart.contains("1h") || chart.contains("6h") || chart.contains("24h"));
1609 }
1610
1611 #[test]
1612 fn test_price_change_chart() {
1613 let analytics = create_test_analytics();
1614 let chart = generate_price_chart(&analytics);
1615
1616 assert!(chart.contains("```mermaid"));
1618 assert!(chart.contains("Price Change"));
1619 }
1620
1621 #[test]
1622 fn test_new_report_sections_included() {
1623 let analytics = create_test_analytics();
1624 let report = generate_report(&analytics);
1625
1626 assert!(report.contains("## Token Information"));
1628 assert!(report.contains("## Security Analysis"));
1629 assert!(report.contains("## Risk Score"));
1630 }
1631
1632 #[test]
1637 fn test_generate_report_no_holders() {
1638 let mut analytics = create_test_analytics();
1639 analytics.holders = vec![];
1640 analytics.total_holders = 0;
1641 analytics.top_10_concentration = None;
1642 analytics.top_50_concentration = None;
1643 analytics.top_100_concentration = None;
1644 let report = generate_report(&analytics);
1645 assert!(report.contains("No holder data available"));
1646 }
1647
1648 #[test]
1649 fn test_generate_report_no_market_cap() {
1650 let mut analytics = create_test_analytics();
1651 analytics.market_cap = None;
1652 analytics.fdv = None;
1653 analytics.total_supply = None;
1654 analytics.circulating_supply = None;
1655 let report = generate_report(&analytics);
1656 assert!(!report.contains("Market Cap | $"));
1657 assert!(!report.contains("Fully Diluted Valuation | $"));
1658 }
1659
1660 #[test]
1661 fn test_generate_report_no_dex_pairs() {
1662 let mut analytics = create_test_analytics();
1663 analytics.dex_pairs = vec![];
1664 analytics.liquidity_usd = 0.0;
1665 let report = generate_report(&analytics);
1666 assert!(report.contains("## Liquidity Analysis"));
1668 }
1669
1670 #[test]
1671 fn test_generate_report_no_social_no_website() {
1672 let mut analytics = create_test_analytics();
1673 analytics.socials = vec![];
1674 analytics.websites = vec![];
1675 analytics.image_url = None;
1676 analytics.dexscreener_url = None;
1677 let section = generate_token_info_section(&analytics);
1678 assert!(section.contains("No additional token metadata available"));
1679 }
1680
1681 #[test]
1682 fn test_security_analysis_zero_transactions() {
1683 let mut analytics = create_test_analytics();
1684 analytics.total_buys_24h = 0;
1685 analytics.total_sells_24h = 0;
1686 analytics.total_buys_6h = 0;
1687 analytics.total_sells_6h = 0;
1688 analytics.total_buys_1h = 0;
1689 analytics.total_sells_1h = 0;
1690 let section = generate_security_analysis(&analytics);
1691 assert!(section.contains("UNKNOWN") || section.contains("No transaction data"));
1692 }
1693
1694 #[test]
1695 fn test_security_analysis_only_buys() {
1696 let mut analytics = create_test_analytics();
1697 analytics.total_buys_24h = 100;
1698 analytics.total_sells_24h = 0;
1699 let section = generate_security_analysis(&analytics);
1700 assert!(section.contains("Possible honeypot") || section.contains("HIGH"));
1701 }
1702
1703 #[test]
1704 fn test_security_analysis_token_age_very_new() {
1705 let mut analytics = create_test_analytics();
1706 analytics.token_age_hours = Some(6.0);
1707 let section = generate_security_analysis(&analytics);
1708 assert!(section.contains("Very new token") || section.contains("HIGH RISK"));
1709 }
1710
1711 #[test]
1712 fn test_security_analysis_token_age_unknown() {
1713 let mut analytics = create_test_analytics();
1714 analytics.token_age_hours = None;
1715 let section = generate_security_analysis(&analytics);
1716 assert!(section.contains("not available") || section.contains("UNKNOWN"));
1717 }
1718
1719 #[test]
1720 fn test_security_analysis_whale_risk_extreme() {
1721 let mut analytics = create_test_analytics();
1722 analytics.holders = vec![TokenHolder {
1723 address: "0xwhale".to_string(),
1724 balance: "9000000".to_string(),
1725 formatted_balance: "9M".to_string(),
1726 percentage: 60.0,
1727 rank: 1,
1728 }];
1729 let section = generate_security_analysis(&analytics);
1730 assert!(section.contains("HIGH") || section.contains("Extreme concentration"));
1731 }
1732
1733 #[test]
1734 fn test_security_analysis_no_holders() {
1735 let mut analytics = create_test_analytics();
1736 analytics.holders = vec![];
1737 let section = generate_security_analysis(&analytics);
1738 assert!(section.contains("Whale Risk"));
1739 assert!(section.contains("UNKNOWN") || section.contains("not available"));
1740 }
1741
1742 #[test]
1743 fn test_risk_factors_high_risk_token() {
1744 let mut analytics = create_test_analytics();
1745 analytics.total_buys_24h = 1000;
1746 analytics.total_sells_24h = 0; analytics.token_age_hours = Some(12.0); analytics.liquidity_usd = 5_000.0; analytics.holders = vec![TokenHolder {
1750 address: "0x1".to_string(),
1751 balance: "1000".to_string(),
1752 formatted_balance: "1K".to_string(),
1753 percentage: 80.0, rank: 1,
1755 }];
1756 analytics.socials = vec![]; analytics.websites = vec![];
1758
1759 let factors = RiskFactors::from_analytics(&analytics);
1760 assert_eq!(factors.honeypot, 10);
1761 assert_eq!(factors.age, 10);
1762 assert_eq!(factors.liquidity, 10);
1763 assert_eq!(factors.concentration, 10);
1764 assert_eq!(factors.social, 8);
1765 assert!(factors.overall_score() >= 8);
1766 assert!(factors.risk_level() == "HIGH" || factors.risk_level() == "CRITICAL");
1767 assert!(factors.risk_emoji() == "🟠" || factors.risk_emoji() == "🔴");
1768 }
1769
1770 #[test]
1771 fn test_risk_factors_low_risk_token() {
1772 let mut analytics = create_test_analytics();
1773 analytics.total_buys_24h = 100;
1774 analytics.total_sells_24h = 95; analytics.token_age_hours = Some(10_000.0); analytics.liquidity_usd = 50_000_000.0; analytics.holders = vec![TokenHolder {
1778 address: "0x1".to_string(),
1779 balance: "1000".to_string(),
1780 formatted_balance: "1K".to_string(),
1781 percentage: 3.0, rank: 1,
1783 }];
1784 analytics.socials = vec![
1785 TokenSocial {
1786 platform: "twitter".to_string(),
1787 url: "https://twitter.com/test".to_string(),
1788 },
1789 TokenSocial {
1790 platform: "telegram".to_string(),
1791 url: "https://t.me/test".to_string(),
1792 },
1793 ];
1794 analytics.websites = vec!["https://example.com".to_string()];
1795
1796 let factors = RiskFactors::from_analytics(&analytics);
1797 assert!(factors.overall_score() <= 3);
1798 assert_eq!(factors.risk_level(), "LOW");
1799 assert_eq!(factors.risk_emoji(), "🟢");
1800 }
1801
1802 #[test]
1803 fn test_risk_assessment_labels() {
1804 assert_eq!(risk_assessment(0), "Low Risk");
1805 assert_eq!(risk_assessment(1), "Low Risk");
1806 assert_eq!(risk_assessment(3), "Moderate");
1807 assert_eq!(risk_assessment(5), "Elevated");
1808 assert_eq!(risk_assessment(7), "High Risk");
1809 assert_eq!(risk_assessment(9), "Critical");
1810 assert_eq!(risk_assessment(10), "Critical");
1811 }
1812
1813 #[test]
1814 fn test_format_usd_edge_cases() {
1815 assert_eq!(format_usd(0.0), "$0.00");
1816 assert_eq!(format_usd(0.50), "$0.50");
1817 assert_eq!(format_usd(999.0), "$999.00");
1818 }
1819
1820 #[test]
1821 fn test_format_number_edge_cases() {
1822 assert_eq!(format_number(0.0), "0");
1823 assert_eq!(format_number(500.0), "500");
1824 assert_eq!(format_number(1500.0), "2K");
1825 assert_eq!(format_number(1_500_000.0), "1.50M");
1826 }
1827
1828 #[test]
1829 fn test_capitalize_edge_cases() {
1830 assert_eq!(capitalize("a"), "A");
1831 assert_eq!(capitalize("ABC"), "ABC");
1832 }
1833
1834 #[test]
1835 fn test_data_sources_different_chains() {
1836 let chains = vec![
1837 ("ethereum", "etherscan.io"),
1838 ("polygon", "polygonscan.com"),
1839 ("arbitrum", "arbiscan.io"),
1840 ("optimism", "optimistic.etherscan.io"),
1841 ("base", "basescan.org"),
1842 ("bsc", "bscscan.com"),
1843 ("solana", "solscan.io"),
1844 ];
1845
1846 for (chain, expected_domain) in chains {
1847 let mut analytics = create_test_analytics();
1848 analytics.chain = chain.to_string();
1849 let section = generate_data_sources(&analytics);
1850 assert!(
1851 section.contains(expected_domain),
1852 "Chain {} should link to {}",
1853 chain,
1854 expected_domain
1855 );
1856 }
1857 }
1858
1859 #[test]
1860 fn test_buysell_chart_empty() {
1861 let mut analytics = create_test_analytics();
1862 analytics.total_buys_24h = 0;
1863 analytics.total_sells_24h = 0;
1864 let chart = generate_buysell_chart(&analytics);
1865 assert!(chart.is_empty());
1866 }
1867
1868 #[test]
1869 fn test_txn_activity_chart_empty() {
1870 let mut analytics = create_test_analytics();
1871 analytics.total_buys_24h = 0;
1872 analytics.total_sells_24h = 0;
1873 analytics.total_buys_6h = 0;
1874 analytics.total_sells_6h = 0;
1875 analytics.total_buys_1h = 0;
1876 analytics.total_sells_1h = 0;
1877 let chart = generate_txn_activity_chart(&analytics);
1878 assert!(chart.is_empty());
1879 }
1880
1881 #[test]
1882 fn test_volume_chart_empty() {
1883 let analytics = create_test_analytics();
1884 let chart = generate_volume_chart(&analytics);
1886 assert!(chart.is_empty());
1887 }
1888
1889 #[test]
1890 fn test_liquidity_chart_single_pair() {
1891 let analytics = create_test_analytics();
1892 assert_eq!(analytics.dex_pairs.len(), 1);
1894 let chart = generate_liquidity_chart(&analytics);
1895 assert!(chart.is_empty());
1896 }
1897
1898 #[test]
1899 fn test_liquidity_chart_multiple_pairs() {
1900 let mut analytics = create_test_analytics();
1901 analytics.dex_pairs.push(DexPair {
1902 dex_name: "SushiSwap".to_string(),
1903 pair_address: "0x5678".to_string(),
1904 base_token: "USDC".to_string(),
1905 quote_token: "DAI".to_string(),
1906 price_usd: 1.0,
1907 volume_24h: 100_000.0,
1908 liquidity_usd: 5_000_000.0,
1909 price_change_24h: 0.0,
1910 buys_24h: 50,
1911 sells_24h: 50,
1912 buys_6h: 10,
1913 sells_6h: 10,
1914 buys_1h: 2,
1915 sells_1h: 2,
1916 pair_created_at: None,
1917 url: None,
1918 });
1919 let chart = generate_liquidity_chart(&analytics);
1920 assert!(chart.contains("mermaid"));
1921 assert!(chart.contains("Uniswap V3"));
1922 assert!(chart.contains("SushiSwap"));
1923 }
1924
1925 #[test]
1926 fn test_concentration_chart_no_holders() {
1927 let mut analytics = create_test_analytics();
1928 analytics.holders = vec![];
1929 analytics.top_10_concentration = Some(0.0);
1930 let chart = generate_concentration_chart(&analytics);
1931 assert!(chart.is_empty());
1932 }
1933
1934 #[test]
1935 fn test_concentration_analysis_ranges() {
1936 let mut analytics = create_test_analytics();
1938 analytics.top_10_concentration = Some(85.0);
1939 let section = generate_concentration_analysis(&analytics);
1940 assert!(section.contains("Very High Concentration"));
1941
1942 analytics.top_10_concentration = Some(55.0);
1944 let section = generate_concentration_analysis(&analytics);
1945 assert!(section.contains("High Concentration"));
1946
1947 analytics.top_10_concentration = Some(15.0);
1949 let section = generate_concentration_analysis(&analytics);
1950 assert!(section.contains("Low Concentration"));
1951 }
1952
1953 #[test]
1954 fn test_risk_indicators_low_liquidity() {
1955 let mut analytics = create_test_analytics();
1956 analytics.liquidity_usd = 5_000.0;
1957 let section = generate_risk_indicators(&analytics);
1958 assert!(section.contains("Very low liquidity"));
1959 }
1960
1961 #[test]
1962 fn test_risk_indicators_high_volatility() {
1963 let mut analytics = create_test_analytics();
1964 analytics.price_change_24h = 25.0;
1965 let section = generate_risk_indicators(&analytics);
1966 assert!(section.contains("High price volatility"));
1967 }
1968
1969 #[test]
1970 fn test_risk_indicators_healthy_token() {
1971 let mut analytics = create_test_analytics();
1972 analytics.holders = vec![TokenHolder {
1973 address: "0x1".to_string(),
1974 balance: "100".to_string(),
1975 formatted_balance: "100".to_string(),
1976 percentage: 5.0,
1977 rank: 1,
1978 }];
1979 analytics.liquidity_usd = 10_000_000.0;
1980 analytics.volume_24h = 500_000.0;
1981 analytics.price_change_24h = 2.0;
1982 let section = generate_risk_indicators(&analytics);
1983 assert!(section.contains("Reasonable distribution"));
1984 assert!(section.contains("Good liquidity"));
1985 assert!(section.contains("Active trading"));
1986 }
1987
1988 #[test]
1989 fn test_risk_indicators_empty() {
1990 let mut analytics = create_test_analytics();
1991 analytics.holders = vec![];
1992 analytics.liquidity_usd = 500_000.0;
1993 analytics.volume_24h = 50_000.0;
1994 analytics.price_change_24h = 5.0;
1995 let section = generate_risk_indicators(&analytics);
1996 assert!(section.contains("Reasonable distribution"));
1998 }
1999
2000 #[test]
2001 fn test_save_report() {
2002 let tmp = std::env::temp_dir().join("bcc_test_report.md");
2003 let result = save_report("# Test Report\n\nContent here.", &tmp);
2004 assert!(result.is_ok());
2005 let content = std::fs::read_to_string(&tmp).unwrap();
2006 assert!(content.contains("# Test Report"));
2007 let _ = std::fs::remove_file(&tmp);
2008 }
2009
2010 #[test]
2011 fn test_save_report_invalid_path() {
2012 let result = save_report("content", "/nonexistent/directory/report.md");
2013 assert!(result.is_err());
2014 }
2015
2016 #[test]
2017 fn test_volume_analysis_high_vol_to_liq() {
2018 let mut analytics = create_test_analytics();
2019 analytics.volume_24h = 100_000_000.0;
2020 analytics.liquidity_usd = 10_000_000.0; let section = generate_volume_analysis(&analytics);
2022 assert!(section.contains("unusual trading activity"));
2023 }
2024
2025 #[test]
2026 fn test_price_analysis_with_history() {
2027 use crate::chains::PricePoint;
2028 let mut analytics = create_test_analytics();
2029 analytics.price_history = vec![
2030 PricePoint {
2031 timestamp: 1700000000,
2032 price: 1.0,
2033 },
2034 PricePoint {
2035 timestamp: 1700003600,
2036 price: 1.5,
2037 },
2038 PricePoint {
2039 timestamp: 1700007200,
2040 price: 0.8,
2041 },
2042 ];
2043 let section = generate_price_analysis(&analytics);
2044 assert!(section.contains("Price Range"));
2045 assert!(section.contains("High"));
2046 assert!(section.contains("Low"));
2047 assert!(section.contains("Average"));
2048 }
2049
2050 #[test]
2051 fn test_social_platform_icons() {
2052 let mut analytics = create_test_analytics();
2053 analytics.socials = vec![
2054 TokenSocial {
2055 platform: "twitter".to_string(),
2056 url: "https://twitter.com/test".to_string(),
2057 },
2058 TokenSocial {
2059 platform: "telegram".to_string(),
2060 url: "https://t.me/test".to_string(),
2061 },
2062 TokenSocial {
2063 platform: "discord".to_string(),
2064 url: "https://discord.gg/test".to_string(),
2065 },
2066 TokenSocial {
2067 platform: "github".to_string(),
2068 url: "https://github.com/test".to_string(),
2069 },
2070 TokenSocial {
2071 platform: "unknown".to_string(),
2072 url: "https://example.com".to_string(),
2073 },
2074 ];
2075 let section = generate_token_info_section(&analytics);
2076 assert!(section.contains("🐦")); assert!(section.contains("📱")); assert!(section.contains("💬")); assert!(section.contains("💻")); assert!(section.contains("🔗")); }
2082
2083 #[test]
2084 fn test_security_analysis_token_age_ranges() {
2085 let mut analytics = create_test_analytics();
2086
2087 analytics.token_age_hours = Some(6.0);
2089 let section = generate_security_analysis(&analytics);
2090 assert!(section.contains("HIGH RISK"));
2091
2092 analytics.token_age_hours = Some(36.0);
2094 let section = generate_security_analysis(&analytics);
2095 assert!(section.contains("MEDIUM"));
2096
2097 analytics.token_age_hours = Some(120.0);
2099 let section = generate_security_analysis(&analytics);
2100 assert!(section.contains("CAUTION"));
2101
2102 analytics.token_age_hours = Some(10_000.0);
2104 let section = generate_security_analysis(&analytics);
2105 assert!(section.contains("ESTABLISHED"));
2106 }
2107
2108 #[test]
2109 fn test_price_history_chart_with_data() {
2110 use crate::chains::PricePoint;
2111 let mut analytics = create_test_analytics();
2112 analytics.price_history = (0..20)
2113 .map(|i| PricePoint {
2114 timestamp: 1700000000 + i * 3600,
2115 price: 1.0 + (i as f64) * 0.01,
2116 })
2117 .collect();
2118 let chart = generate_price_history_chart(&analytics);
2119 assert!(chart.contains("Price History"));
2120 assert!(chart.contains("mermaid"));
2121 assert!(chart.contains("xychart-beta"));
2122 assert!(chart.contains("line ["));
2123 }
2124
2125 #[test]
2126 fn test_price_chart_with_changes_and_history() {
2127 use crate::chains::PricePoint;
2128 let mut analytics = create_test_analytics();
2129 analytics.price_change_1h = 1.5;
2130 analytics.price_change_6h = -2.3;
2131 analytics.price_change_24h = 5.0;
2132 analytics.price_change_7d = -10.0;
2133 analytics.price_history = (0..5)
2134 .map(|i| PricePoint {
2135 timestamp: 1700000000 + i * 3600,
2136 price: 1.0 + (i as f64) * 0.1,
2137 })
2138 .collect();
2139 let chart = generate_price_chart(&analytics);
2140 assert!(chart.contains("Price Changes by Period"));
2141 assert!(chart.contains("Price History")); }
2143
2144 #[test]
2145 fn test_price_chart_zero_changes_with_history() {
2146 use crate::chains::PricePoint;
2147 let mut analytics = create_test_analytics();
2148 analytics.price_change_1h = 0.0;
2149 analytics.price_change_6h = 0.0;
2150 analytics.price_change_24h = 0.0;
2151 analytics.price_change_7d = 0.0;
2152 analytics.price_history = vec![
2153 PricePoint {
2154 timestamp: 1700000000,
2155 price: 1.0,
2156 },
2157 PricePoint {
2158 timestamp: 1700003600,
2159 price: 1.5,
2160 },
2161 ];
2162 let chart = generate_price_chart(&analytics);
2163 assert!(chart.contains("Price History")); }
2165
2166 #[test]
2167 fn test_volume_chart_with_data() {
2168 use crate::chains::VolumePoint;
2169 let mut analytics = create_test_analytics();
2170 analytics.volume_history = (0..10)
2171 .map(|i| VolumePoint {
2172 timestamp: 1700000000 + i * 3600,
2173 volume: 100_000.0 + (i as f64) * 50_000.0,
2174 })
2175 .collect();
2176 let chart = generate_volume_chart(&analytics);
2177 assert!(chart.contains("Volume Chart"));
2178 assert!(chart.contains("mermaid"));
2179 assert!(chart.contains("bar ["));
2180 }
2181
2182 #[test]
2183 fn test_concentration_chart_three_segments() {
2184 let mut analytics = create_test_analytics();
2185 analytics.top_10_concentration = Some(30.0);
2187 analytics.top_50_concentration = Some(60.0);
2188 let chart = generate_concentration_chart(&analytics);
2189 assert!(chart.contains("Top 10"));
2190 assert!(chart.contains("Rank 11-50"));
2191 assert!(chart.contains("Others"));
2192 }
2193
2194 #[test]
2195 fn test_risk_indicators_very_low_liquidity() {
2196 let mut analytics = create_test_analytics();
2197 analytics.liquidity_usd = 5_000.0;
2198 analytics.volume_24h = 500.0;
2199 let section = generate_risk_indicators(&analytics);
2200 assert!(section.contains("Very low liquidity"));
2201 assert!(section.contains("Very low trading volume"));
2202 }
2203
2204 #[test]
2205 fn test_risk_indicators_moderate_liquidity() {
2206 let mut analytics = create_test_analytics();
2207 analytics.liquidity_usd = 50_000.0;
2208 let section = generate_risk_indicators(&analytics);
2209 assert!(section.contains("Low liquidity"));
2210 }
2211
2212 #[test]
2213 fn test_risk_indicators_extreme_concentration() {
2214 let mut analytics = create_test_analytics();
2215 analytics.holders = vec![TokenHolder {
2216 address: "0xwhale".to_string(),
2217 balance: "900000000".to_string(),
2218 formatted_balance: "900M".to_string(),
2219 percentage: 90.0,
2220 rank: 1,
2221 }];
2222 let section = generate_risk_indicators(&analytics);
2223 assert!(section.contains("Extreme whale concentration"));
2224 }
2225
2226 #[test]
2227 fn test_risk_indicators_no_data() {
2228 let mut analytics = create_test_analytics();
2229 analytics.holders = vec![];
2230 analytics.liquidity_usd = 500_000.0; analytics.volume_24h = 50_000.0; analytics.price_change_24h = 5.0; let section = generate_risk_indicators(&analytics);
2234 assert!(section.contains("Risk Indicators"));
2236 }
2237
2238 #[test]
2239 fn test_holder_section_with_data() {
2240 let analytics = create_test_analytics();
2241 let section = generate_holder_section(&analytics);
2242 assert!(section.contains("Top Holders"));
2243 assert!(section.contains("0x55FE002e15bA7591a5E5Ce68a6D3c6E1593d3d8c")); assert!(section.contains("12.50%"));
2245 }
2246
2247 #[test]
2248 fn test_risk_breakdown_chart() {
2249 let analytics = create_test_analytics();
2250 let section = generate_risk_score_section(&analytics);
2251 assert!(section.contains("Risk Score"));
2252 assert!(section.contains("Risk Factor Breakdown"));
2253 assert!(section.contains("Honeypot"));
2254 assert!(section.contains("Token Age"));
2255 }
2256
2257 #[test]
2258 fn test_data_sources_section() {
2259 let analytics = create_test_analytics();
2260 let section = generate_data_sources(&analytics);
2261 assert!(section.contains("Data Sources"));
2262 assert!(section.contains("ethereum"));
2263 }
2264
2265 #[test]
2266 fn test_volume_analysis_section() {
2267 let analytics = create_test_analytics();
2268 let section = generate_volume_analysis(&analytics);
2269 assert!(section.contains("Volume Analysis"));
2270 }
2271
2272 #[test]
2273 fn test_liquidity_analysis_section() {
2274 let analytics = create_test_analytics();
2275 let section = generate_liquidity_analysis(&analytics);
2276 assert!(section.contains("Liquidity Analysis"));
2277 }
2278
2279 #[test]
2280 fn test_security_analysis_medium_buy_sell_ratio() {
2281 let mut analytics = create_test_analytics();
2282 analytics.total_buys_24h = 100;
2284 analytics.total_sells_24h = 20;
2285 let section = generate_security_analysis(&analytics);
2286 assert!(section.contains("MEDIUM") || section.contains("Elevated"));
2287 }
2288
2289 #[test]
2290 fn test_security_analysis_token_age_months() {
2291 let mut analytics = create_test_analytics();
2292 analytics.token_age_hours = Some(2000.0);
2294 let section = generate_security_analysis(&analytics);
2295 assert!(section.contains("ESTABLISHED") || section.contains("months"));
2296 }
2297
2298 #[test]
2299 fn test_security_analysis_whale_risk_medium() {
2300 let mut analytics = create_test_analytics();
2301 analytics.holders = vec![TokenHolder {
2302 address: "0xwhale".to_string(),
2303 balance: "3000000".to_string(),
2304 formatted_balance: "3M".to_string(),
2305 percentage: 30.0, rank: 1,
2307 }];
2308 let section = generate_security_analysis(&analytics);
2309 assert!(section.contains("MEDIUM") || section.contains("High concentration"));
2310 }
2311
2312 #[test]
2313 fn test_security_analysis_whale_risk_low() {
2314 let mut analytics = create_test_analytics();
2315 analytics.holders = vec![TokenHolder {
2316 address: "0xholder".to_string(),
2317 balance: "500000".to_string(),
2318 formatted_balance: "500K".to_string(),
2319 percentage: 5.0, rank: 1,
2321 }];
2322 let section = generate_security_analysis(&analytics);
2323 assert!(section.contains("LOW") || section.contains("Well distributed"));
2324 }
2325
2326 #[test]
2327 fn test_security_analysis_token_age_days_format() {
2328 let mut analytics = create_test_analytics();
2329 analytics.token_age_hours = Some(480.0);
2331 let section = generate_security_analysis(&analytics);
2332 assert!(section.contains("days ago") || section.contains("MODERATE"));
2333 }
2334
2335 #[test]
2336 fn test_security_buysell_zero_buys_zero_sells_in_period() {
2337 let mut analytics = create_test_analytics();
2338 analytics.total_buys_1h = 0;
2340 analytics.total_sells_1h = 0;
2341 analytics.total_buys_6h = 0;
2342 analytics.total_sells_6h = 0;
2343 analytics.total_buys_24h = 100;
2344 analytics.total_sells_24h = 80;
2345 let section = generate_security_analysis(&analytics);
2346 assert!(section.contains("-") || section.contains("100")); }
2348
2349 #[test]
2350 fn test_risk_factors_various_honeypot_ratios() {
2351 let mut analytics = create_test_analytics();
2352
2353 analytics.total_buys_24h = 110;
2355 analytics.total_sells_24h = 10;
2356 let factors = RiskFactors::from_analytics(&analytics);
2357 assert_eq!(factors.honeypot, 9);
2358
2359 analytics.total_buys_24h = 60;
2361 analytics.total_sells_24h = 10;
2362 let factors = RiskFactors::from_analytics(&analytics);
2363 assert_eq!(factors.honeypot, 7);
2364
2365 analytics.total_buys_24h = 40;
2367 analytics.total_sells_24h = 10;
2368 let factors = RiskFactors::from_analytics(&analytics);
2369 assert_eq!(factors.honeypot, 5);
2370
2371 analytics.total_buys_24h = 25;
2373 analytics.total_sells_24h = 10;
2374 let factors = RiskFactors::from_analytics(&analytics);
2375 assert_eq!(factors.honeypot, 3);
2376
2377 analytics.total_buys_24h = 15;
2379 analytics.total_sells_24h = 10;
2380 let factors = RiskFactors::from_analytics(&analytics);
2381 assert_eq!(factors.honeypot, 1);
2382 }
2383
2384 #[test]
2385 fn test_risk_factors_various_age_thresholds() {
2386 let mut analytics = create_test_analytics();
2387
2388 analytics.token_age_hours = Some(36.0);
2390 let factors = RiskFactors::from_analytics(&analytics);
2391 assert_eq!(factors.age, 8);
2392
2393 analytics.token_age_hours = Some(120.0);
2395 let factors = RiskFactors::from_analytics(&analytics);
2396 assert_eq!(factors.age, 6);
2397
2398 analytics.token_age_hours = Some(500.0);
2400 let factors = RiskFactors::from_analytics(&analytics);
2401 assert_eq!(factors.age, 4);
2402
2403 analytics.token_age_hours = Some(1500.0);
2405 let factors = RiskFactors::from_analytics(&analytics);
2406 assert_eq!(factors.age, 2);
2407 }
2408
2409 #[test]
2410 fn test_risk_factors_various_liquidity_thresholds() {
2411 let mut analytics = create_test_analytics();
2412
2413 analytics.liquidity_usd = 75_000.0;
2415 let factors = RiskFactors::from_analytics(&analytics);
2416 assert_eq!(factors.liquidity, 6);
2417
2418 analytics.liquidity_usd = 200_000.0;
2420 let factors = RiskFactors::from_analytics(&analytics);
2421 assert_eq!(factors.liquidity, 4);
2422
2423 analytics.liquidity_usd = 750_000.0;
2425 let factors = RiskFactors::from_analytics(&analytics);
2426 assert_eq!(factors.liquidity, 2);
2427
2428 analytics.liquidity_usd = 2_000_000.0;
2430 let factors = RiskFactors::from_analytics(&analytics);
2431 assert_eq!(factors.liquidity, 1);
2432 }
2433
2434 #[test]
2435 fn test_risk_factors_various_concentration_thresholds() {
2436 let mut analytics = create_test_analytics();
2437
2438 analytics.holders = vec![TokenHolder {
2440 address: "0x1".to_string(),
2441 balance: "1".to_string(),
2442 formatted_balance: "1".to_string(),
2443 percentage: 35.0,
2444 rank: 1,
2445 }];
2446 let factors = RiskFactors::from_analytics(&analytics);
2447 assert_eq!(factors.concentration, 8);
2448
2449 analytics.holders[0].percentage = 25.0;
2451 let factors = RiskFactors::from_analytics(&analytics);
2452 assert_eq!(factors.concentration, 6);
2453
2454 analytics.holders[0].percentage = 15.0;
2456 let factors = RiskFactors::from_analytics(&analytics);
2457 assert_eq!(factors.concentration, 4);
2458
2459 analytics.holders[0].percentage = 7.0;
2461 let factors = RiskFactors::from_analytics(&analytics);
2462 assert_eq!(factors.concentration, 2);
2463
2464 analytics.holders[0].percentage = 3.0;
2466 let factors = RiskFactors::from_analytics(&analytics);
2467 assert_eq!(factors.concentration, 1);
2468 }
2469
2470 #[test]
2471 fn test_risk_factors_social_one_social() {
2472 let mut analytics = create_test_analytics();
2473 analytics.socials = vec![TokenSocial {
2474 platform: "twitter".to_string(),
2475 url: "https://twitter.com/test".to_string(),
2476 }];
2477 analytics.websites = vec![];
2478 let factors = RiskFactors::from_analytics(&analytics);
2479 assert!(factors.social <= 5);
2481 }
2482
2483 #[test]
2484 fn test_format_number_large_values() {
2485 assert_eq!(format_number(1_500_000_000.0), "1500.00M");
2486 assert_eq!(format_number(500_000.0), "500K");
2487 assert_eq!(format_number(42.0), "42");
2488 }
2489}