1use crate::cli::address::{self, AddressArgs};
7use crate::cli::crawl::{Period, fetch_analytics_for_input};
8use crate::cli::output::Output;
9use crate::cli::tx::{fetch_transaction_report, format_tx_markdown};
10use crate::outln;
11use clap::Args;
12use scope::chains::{
13 ChainClientFactory, infer_chain_from_address, infer_chain_from_hash, native_symbol,
14};
15use scope::config::Config;
16
17use super::health_args::HealthArgs;
18use scope::display::report;
19use scope::error::Result;
20use scope::market::health::PegStatus;
21use scope::market::{MarketSummary, MarketVenue, VenueRegistry, fetch_token_summary};
22use scope::tokens::{TokenAliases, is_stablecoin_symbol};
23
24#[derive(Debug, Clone)]
26pub enum InferredTarget {
27 Address { chain: String },
29 Transaction { chain: String },
31 Token { chain: String },
33}
34
35#[derive(Debug, Args)]
37#[command(after_help = "\x1b[1mExamples:\x1b[0m
38 scope insights 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2
39 scope insights @main-wallet \x1b[2m# address book shortcut\x1b[0m
40 scope insights 0xabc123def456... --decode --trace
41 scope insights USDC
42 scope insights DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy --chain solana")]
43pub struct InsightsArgs {
44 pub target: String,
56
57 #[arg(long = "token", value_name = "CONTRACT")]
59 pub token_contract: Option<String>,
60
61 #[arg(short, long)]
63 pub chain: Option<String>,
64
65 #[arg(long)]
67 pub decode: bool,
68
69 #[arg(long)]
71 pub trace: bool,
72
73 #[command(flatten)]
75 pub health: HealthArgs,
76}
77
78pub fn infer_target(input: &str, chain_override: Option<&str>) -> InferredTarget {
80 let trimmed = input.trim();
81
82 if let Some(chain) = chain_override {
83 let chain = chain.to_lowercase();
84 if infer_chain_from_hash(trimmed).is_some() {
86 return InferredTarget::Transaction { chain };
87 }
88 if TokenAliases::is_address(trimmed) {
89 return InferredTarget::Address { chain };
90 }
91 return InferredTarget::Token { chain };
92 }
93
94 if let Some(chain) = infer_chain_from_hash(trimmed) {
96 return InferredTarget::Transaction {
97 chain: chain.to_string(),
98 };
99 }
100
101 if TokenAliases::is_address(trimmed) {
103 let chain = infer_chain_from_address(trimmed).unwrap_or(scope::chains::DEFAULT_CHAIN);
104 return InferredTarget::Address {
105 chain: chain.to_string(),
106 };
107 }
108
109 InferredTarget::Token {
111 chain: scope::chains::DEFAULT_CHAIN.to_string(),
112 }
113}
114
115pub async fn run(
117 mut args: InsightsArgs,
118 config: &Config,
119 clients: &dyn ChainClientFactory,
120 out: &Output,
121) -> Result<()> {
122 if let Some((address, chain)) =
124 crate::cli::address_book::resolve_address_book_input(&args.target, config)?
125 {
126 args.target = address;
127 if args.chain.is_none() {
128 args.chain = Some(chain);
129 }
130 }
131
132 let chain_override = args.chain.as_deref();
133 let target = infer_target(&args.target, chain_override);
134
135 let sp = crate::cli::progress::Spinner::new(
136 &format!(
137 "Analyzing {} on {}...",
138 target_type_label(&target),
139 chain_label(&target)
140 ),
141 out,
142 );
143
144 let mut output = String::new();
145 output.push_str("# Scope Insights\n\n");
146 output.push_str(&format!("**Target:** `{}`\n\n", args.target));
147 output.push_str(&format!(
148 "**Detected:** {} on {}\n\n",
149 target_type_label(&target),
150 chain_label(&target)
151 ));
152 output.push_str("---\n\n");
153
154 match &target {
155 InferredTarget::Address { chain } => {
156 output.push_str("## Observations\n\n");
157 let addr_args = AddressArgs {
158 address: args.target.clone(),
159 chain: chain.clone(),
160 format: Some(scope::config::OutputFormat::Markdown),
161 include_txs: false,
162 include_tokens: true,
163 limit: 10,
164 report: None,
165 dossier: false,
166 };
167 let client = clients.create_chain_client(chain)?;
168 let report = address::analyze_address(&addr_args, client.as_ref(), out).await?;
169
170 let code_result = client.get_code(&args.target).await;
172 let is_contract = code_result
173 .as_ref()
174 .is_ok_and(|c| !c.is_empty() && c != "0x");
175 if code_result.is_ok() {
176 output.push_str(&format!(
177 "- **Type:** {}\n",
178 if is_contract {
179 "Contract"
180 } else {
181 "Externally Owned Account (EOA)"
182 }
183 ));
184 }
185
186 output.push_str(&format!(
187 "- **Native balance:** {} ({})\n",
188 report.balance.formatted,
189 scope::chains::native_symbol(chain)
190 ));
191 if let Some(ref usd) = report.balance.usd {
192 output.push_str(&format!("- **USD value:** ${:.2}\n", usd));
193 }
194 output.push_str(&format!(
195 "- **Transaction count:** {}\n",
196 report.transaction_count
197 ));
198 if let Some(ref tokens) = report.tokens
199 && !tokens.is_empty()
200 {
201 output.push_str(&format!(
202 "- **Token holdings:** {} different tokens\n",
203 tokens.len()
204 ));
205 output.push_str("\n### Token Balances\n\n");
206 for tb in tokens.iter().take(10) {
207 output.push_str(&format!(
208 "- {}: {} ({})\n",
209 tb.symbol, tb.formatted_balance, tb.contract_address
210 ));
211 }
212 if tokens.len() > 10 {
213 output.push_str(&format!("\n*...and {} more*\n", tokens.len() - 10));
214 }
215 }
216
217 let risk_assessment =
219 scope::compliance::risk::RiskEngine::from_env_with_http(clients.http())
220 .assess_address(&args.target, chain)
221 .await
222 .ok();
223
224 if let Some(ref risk) = risk_assessment {
225 output.push_str(&risk_line(risk));
226 }
227
228 let meta = meta_analysis_address(
230 is_contract,
231 report.balance.usd,
232 report.tokens.as_ref().map(|t| t.len()).unwrap_or(0),
233 risk_assessment.as_ref().map(|r| r.overall_score),
234 risk_assessment.as_ref().map(|r| &r.risk_level),
235 );
236 output.push_str("\n### Synthesis\n\n");
237 output.push_str(&format!("{}\n\n", meta.synthesis));
238 output.push_str(&format!("**Key takeaway:** {}\n\n", meta.key_takeaway));
239 if !meta.recommendations.is_empty() {
240 output.push_str("**Consider:**\n");
241 for rec in &meta.recommendations {
242 output.push_str(&format!("- {}\n", rec));
243 }
244 }
245 output.push_str("\n---\n\n");
246 let full_report = if let Some(ref risk) = risk_assessment {
247 crate::cli::address_report::generate_dossier_report(&report, risk)
248 } else {
249 crate::cli::address_report::generate_address_report(&report)
250 };
251 output.push_str(&full_report);
252 }
253 InferredTarget::Transaction { chain } => {
254 output.push_str("## Observations\n\n");
255 let tx_report =
256 fetch_transaction_report(&args.target, chain, args.decode, args.trace, clients)
257 .await?;
258
259 let tx_type = classify_tx_type(
260 &tx_report.transaction.input,
261 tx_report.transaction.to.as_deref(),
262 );
263 output.push_str(&format!("- **Type:** {}\n", tx_type));
264
265 output.push_str(&format!(
266 "- **Status:** {}\n",
267 if tx_report.transaction.status {
268 "Success"
269 } else {
270 "Failed"
271 }
272 ));
273 output.push_str(&format!("- **From:** `{}`\n", tx_report.transaction.from));
274 output.push_str(&format!(
275 "- **To:** `{}`\n",
276 tx_report
277 .transaction
278 .to
279 .as_deref()
280 .unwrap_or("Contract Creation")
281 ));
282
283 let (formatted_value, high_value) =
284 format_tx_value(&tx_report.transaction.value, chain);
285 output.push_str(&format!("- **Value:** {}\n", formatted_value));
286 if high_value {
287 output.push_str("- ⚠️ **High-value transfer**\n");
288 }
289
290 output.push_str(&format!("- **Fee:** {}\n", tx_report.gas.transaction_fee));
291
292 let meta = meta_analysis_tx(
294 tx_type,
295 tx_report.transaction.status,
296 high_value,
297 &tx_report.transaction.from,
298 tx_report.transaction.to.as_deref(),
299 );
300 output.push_str("\n### Synthesis\n\n");
301 output.push_str(&format!("{}\n\n", meta.synthesis));
302 output.push_str(&format!("**Key takeaway:** {}\n\n", meta.key_takeaway));
303 if !meta.recommendations.is_empty() {
304 output.push_str("**Consider:**\n");
305 for rec in &meta.recommendations {
306 output.push_str(&format!("- {}\n", rec));
307 }
308 }
309 output.push_str("\n---\n\n");
310 output.push_str(&format_tx_markdown(&tx_report));
311 }
312 InferredTarget::Token { chain } => {
313 output.push_str("## Observations\n\n");
314 let analytics = fetch_analytics_for_input(
315 args.token_contract.as_deref().unwrap_or(&args.target),
317 chain,
318 Period::Hour24,
319 10,
320 clients,
321 Some(&sp),
322 out,
323 )
324 .await?;
325
326 let risk_summary = report::token_risk_summary(&analytics);
328 output.push_str(&format!(
329 "- **Risk:** {} {}/10 ({})\n",
330 risk_summary.emoji, risk_summary.score, risk_summary.level
331 ));
332 if !risk_summary.concerns.is_empty() {
333 for c in &risk_summary.concerns {
334 output.push_str(&format!("- ⚠️ {}\n", c));
335 }
336 }
337 if !risk_summary.positives.is_empty() {
338 for p in &risk_summary.positives {
339 output.push_str(&format!("- ✅ {}\n", p));
340 }
341 }
342
343 output.push_str(&format!(
344 "- **Token:** {} ({})\n",
345 analytics.token.symbol, analytics.token.name
346 ));
347 output.push_str(&format!(
348 "- **Address:** `{}`\n",
349 analytics.token.contract_address
350 ));
351 output.push_str(&format!(
352 "- **Price:** {}\n",
353 scope::display::format::format_price_usd(analytics.price_usd)
354 ));
355 output.push_str(&format!(
356 "- **Liquidity (24h):** {}\n",
357 scope::display::format_usd(analytics.liquidity_usd)
358 ));
359 output.push_str(&format!(
360 "- **Volume (24h):** {}\n",
361 scope::display::format_usd(analytics.volume_24h)
362 ));
363
364 if let Some(top) = analytics.holders.first() {
366 output.push_str(&format!(
367 "- **Top holder:** `{}` ({:.1}%)\n",
368 top.address, top.percentage
369 ));
370 if top.percentage > 30.0 {
371 output.push_str(" - ⚠️ High concentration risk\n");
372 }
373 }
374 output.push_str(&format!(
375 "- **Holders displayed:** {}\n",
376 analytics.holders.len()
377 ));
378
379 let mut peg_status: Option<PegStatus> = None;
381 if is_stablecoin_symbol(&analytics.token.symbol)
382 && let Ok(registry) = VenueRegistry::load()
383 {
384 let venue_id = if registry.contains(scope::market::DEFAULT_VENUE) {
386 scope::market::DEFAULT_VENUE
387 } else {
388 registry
389 .list()
390 .first()
391 .copied()
392 .unwrap_or(scope::market::DEFAULT_VENUE)
393 };
394 let thresholds = args.health.resolve(config);
395 if let (Some(desc), Ok(venue)) = (
396 registry.get(venue_id),
397 MarketVenue::resolve_with_http(venue_id, ®istry, clients.http()),
398 ) && let Ok(summary) = fetch_token_summary(&venue, &analytics, &thresholds).await
399 {
400 peg_status = Some(summary.peg_status());
401 output.push_str(&peg_line(&desc.name, &summary.pair, &summary));
402 }
403 }
404
405 let top_holder_pct = analytics.holders.first().map(|h| h.percentage);
407 let meta = meta_analysis_token(
408 &risk_summary,
409 is_stablecoin_symbol(&analytics.token.symbol),
410 peg_status,
411 top_holder_pct,
412 analytics.liquidity_usd,
413 );
414 output.push_str("\n### Synthesis\n\n");
415 output.push_str(&format!("{}\n\n", meta.synthesis));
416 output.push_str(&format!("**Key takeaway:** {}\n\n", meta.key_takeaway));
417 if !meta.recommendations.is_empty() {
418 output.push_str("**Consider:**\n");
419 for rec in &meta.recommendations {
420 output.push_str(&format!("- {}\n", rec));
421 }
422 }
423 output.push_str("\n---\n\n");
424 output.push_str(&report::generate_report(&analytics));
425 }
426 }
427
428 sp.finish("Insights complete.");
429 outln!(out, "{}", output)?;
430 Ok(())
431}
432
433fn target_type_label(target: &InferredTarget) -> &'static str {
434 match target {
435 InferredTarget::Address { .. } => "Address",
436 InferredTarget::Transaction { .. } => "Transaction",
437 InferredTarget::Token { .. } => "Token",
438 }
439}
440
441fn chain_label(target: &InferredTarget) -> &str {
442 match target {
443 InferredTarget::Address { chain } => chain,
444 InferredTarget::Transaction { chain } => chain,
445 InferredTarget::Token { chain } => chain,
446 }
447}
448
449fn classify_tx_type(input: &str, to: Option<&str>) -> &'static str {
451 if to.is_none() {
452 return "Contract Creation";
453 }
454 let selector = input
455 .trim_start_matches("0x")
456 .chars()
457 .take(8)
458 .collect::<String>();
459 let sel = selector.to_lowercase();
460 match sel.as_str() {
461 "a9059cbb" => "ERC-20 Transfer",
462 "095ea7b3" => "ERC-20 Approve",
463 "23b872dd" => "ERC-20 Transfer From",
464 "38ed1739" | "5c11d795" | "4a25d94a" | "8803dbee" | "7ff36ab5" | "18cbafe5"
465 | "fb3bdb41" | "b6f9de95" => "DEX Swap",
466 "ac9650d8" | "5ae401dc" => "Multicall",
467 _ if input.is_empty() || input == "0x" => "Native Transfer",
468 _ => "Contract Call",
469 }
470}
471
472fn format_tx_value(value_str: &str, chain: &str) -> (String, bool) {
474 let wei: u128 = if value_str.starts_with("0x") {
475 let hex_part = value_str.trim_start_matches("0x");
476 if hex_part.is_empty() {
477 0
478 } else {
479 u128::from_str_radix(hex_part, 16).unwrap_or(0)
480 }
481 } else {
482 value_str.parse().unwrap_or(0)
483 };
484 let decimals =
485 scope::chains::registry::chain(chain).map_or(18, |c| i32::from(c.native_decimals));
486 let divisor = 10_f64.powi(decimals);
487 let human = wei as f64 / divisor;
488 let symbol = native_symbol(chain);
489 let formatted = format!("≈ {:.6} {}", human, symbol);
490 let high_value = human > 10.0;
492 (formatted, high_value)
493}
494
495struct PegWords {
498 label: &'static str,
500 synthesis: Option<&'static str>,
502 takeaway: Option<&'static str>,
504 verify_elsewhere: bool,
506}
507
508fn peg_words(status: PegStatus) -> PegWords {
509 const DEVIATION_SYNTHESIS: &str =
510 "Stablecoin peg deviation detected — verify on multiple venues.";
511 const DEVIATION_TAKEAWAY: &str =
512 "Stablecoin deviating from peg — check additional venues before trading.";
513 match status {
514 PegStatus::OnPeg => PegWords {
515 label: "Peg healthy",
516 synthesis: Some("Stablecoin peg is healthy on observed venue."),
517 takeaway: None,
518 verify_elsewhere: false,
519 },
520 PegStatus::BookUnhealthy => PegWords {
521 label: "Peg on target, but market health checks fail",
522 synthesis: Some(
523 "Stablecoin peg is on target, but the market's health checks fail — verify on multiple venues.",
524 ),
525 takeaway: Some(
526 "Stablecoin market health checks fail — check additional venues before trading.",
527 ),
528 verify_elsewhere: true,
529 },
530 PegStatus::SlightDeviation => PegWords {
531 label: "Slight peg deviation",
532 synthesis: Some(DEVIATION_SYNTHESIS),
533 takeaway: Some(DEVIATION_TAKEAWAY),
534 verify_elsewhere: true,
535 },
536 PegStatus::Deviation => PegWords {
537 label: "Peg deviation",
538 synthesis: Some(DEVIATION_SYNTHESIS),
539 takeaway: Some(DEVIATION_TAKEAWAY),
540 verify_elsewhere: true,
541 },
542 PegStatus::NoPrice => PegWords {
543 label: "Peg unknown (no mid price: a side has no valid level)",
544 synthesis: None,
545 takeaway: None,
546 verify_elsewhere: true,
547 },
548 }
549}
550
551fn peg_line(venue: &str, pair: &str, summary: &MarketSummary) -> String {
554 let label = peg_words(summary.peg_status()).label;
555 let deviation = summary
556 .mid_price
557 .and_then(|mid| summary.peg_deviation_bps(mid))
558 .map(|bps| format!(" (deviation: {bps:.1} bps)"))
559 .unwrap_or_default();
560 format!("- **Market ({venue} {pair}):** {label}{deviation}\n")
561}
562
563struct MetaAnalysis {
565 synthesis: String,
566 key_takeaway: String,
567 recommendations: Vec<String>,
568}
569
570fn risk_line(risk: &scope::compliance::risk::RiskAssessment) -> String {
573 let emoji = risk.risk_level.emoji();
574 if risk.not_assessed.is_empty() {
575 return format!(
576 "\n- **Risk:** {} {:.1}/10 ({:?})\n",
577 emoji, risk.overall_score, risk.risk_level
578 );
579 }
580 let mut line = format!(
581 "\n- **Risk:** {} not assessed in full. No data for:\n",
582 emoji
583 );
584 for reason in &risk.not_assessed {
585 line.push_str(&format!(" - {}\n", reason));
586 }
587 line
588}
589
590fn meta_analysis_address(
591 is_contract: bool,
592 usd_value: Option<f64>,
593 token_count: usize,
594 risk_score: Option<f32>,
595 risk_level: Option<&scope::compliance::risk::RiskLevel>,
596) -> MetaAnalysis {
597 let mut synthesis_parts = Vec::new();
598 let profile = if is_contract {
599 "contract"
600 } else {
601 "wallet (EOA)"
602 };
603 synthesis_parts.push(format!("A {} on chain.", profile));
604
605 if let Some(usd) = usd_value {
606 if usd > 1_000_000.0 {
607 synthesis_parts.push("Significant value held.".to_string());
608 } else if usd > 10_000.0 {
609 synthesis_parts.push("Moderate value.".to_string());
610 } else if usd < 1.0 {
611 synthesis_parts.push("Minimal value.".to_string());
612 }
613 }
614
615 if token_count > 5 {
616 synthesis_parts.push("Diversified token exposure.".to_string());
617 } else if token_count == 1 && token_count > 0 {
618 synthesis_parts.push("Concentrated in a single token.".to_string());
619 }
620
621 let (risk_score, risk_level) = match risk_level {
623 Some(scope::compliance::risk::RiskLevel::Incomplete) => (None, None),
624 _ => (risk_score, risk_level),
625 };
626 if let (Some(score), Some(level)) = (risk_score, risk_level) {
627 if score >= 7.0 {
628 synthesis_parts.push(format!("Elevated risk ({:?}).", level));
629 } else if score <= 3.0 {
630 synthesis_parts.push("Low risk profile.".to_string());
631 }
632 }
633
634 let synthesis = if synthesis_parts.is_empty() {
635 "Address analyzed with available on-chain data.".to_string()
636 } else {
637 synthesis_parts.join(" ")
638 };
639
640 let key_takeaway = if let (Some(score), Some(level)) = (risk_score, risk_level) {
641 if score >= 7.0 {
642 format!(
643 "Risk assessment warrants closer scrutiny ({:.1}/10).",
644 score
645 )
646 } else {
647 format!("Overall risk: {:?} ({:.1}/10).", level, score)
648 }
649 } else if is_contract {
650 "Contract address — verify intended interaction before use.".to_string()
651 } else if usd_value.map(|u| u > 100_000.0).unwrap_or(false) {
652 "High-value wallet — standard due diligence applies.".to_string()
653 } else {
654 "Review full report for transaction and token details.".to_string()
655 };
656
657 let mut recommendations = Vec::new();
658 if risk_score.map(|s| s >= 6.0).unwrap_or(false) {
659 recommendations.push("Monitor for unusual transaction patterns.".to_string());
660 }
661 if token_count > 0 {
662 recommendations.push("Verify token contracts before large interactions.".to_string());
663 }
664 if is_contract {
665 recommendations.push("Confirm contract source and audit status.".to_string());
666 }
667
668 MetaAnalysis {
669 synthesis,
670 key_takeaway,
671 recommendations,
672 }
673}
674
675fn meta_analysis_tx(
676 tx_type: &str,
677 status: bool,
678 high_value: bool,
679 _from: &str,
680 _to: Option<&str>,
681) -> MetaAnalysis {
682 let mut synthesis_parts = Vec::new();
683
684 if !status {
685 synthesis_parts.push("Transaction failed.".to_string());
686 }
687
688 synthesis_parts.push(format!("{} between parties.", tx_type));
689
690 if high_value {
691 synthesis_parts.push("High-value transfer.".to_string());
692 }
693
694 let synthesis = synthesis_parts.join(" ");
695
696 let key_takeaway = if !status {
697 "Failed transaction — check revert reason and contract state.".to_string()
698 } else if high_value && tx_type == "Native Transfer" {
699 "Large native transfer — verify recipient and intent.".to_string()
700 } else if high_value {
701 "High-value operation — standard verification recommended.".to_string()
702 } else {
703 format!("Routine {} — review full details if needed.", tx_type)
704 };
705
706 let mut recommendations = Vec::new();
707 if !status {
708 recommendations.push("Inspect contract logs for revert reason.".to_string());
709 }
710 if high_value {
711 recommendations.push("Confirm recipient address and amount.".to_string());
712 }
713 if tx_type.contains("Approval") {
714 recommendations.push("Verify approved spender and allowance amount.".to_string());
715 }
716
717 MetaAnalysis {
718 synthesis,
719 key_takeaway,
720 recommendations,
721 }
722}
723
724fn meta_analysis_token(
725 risk_summary: &report::TokenRiskSummary,
726 is_stablecoin: bool,
727 peg: Option<PegStatus>,
728 top_holder_pct: Option<f64>,
729 liquidity_usd: f64,
730) -> MetaAnalysis {
731 let mut synthesis_parts = Vec::new();
732
733 if risk_summary.score <= 3 {
734 synthesis_parts.push("Low-risk token with healthy metrics.".to_string());
735 } else if risk_summary.score >= 7 {
736 synthesis_parts.push("Elevated risk — multiple concerns identified.".to_string());
737 } else {
738 synthesis_parts.push("Moderate risk — mixed signals.".to_string());
739 }
740
741 let peg_words = peg.filter(|_| is_stablecoin).map(peg_words);
742 if let Some(sentence) = peg_words.as_ref().and_then(|w| w.synthesis) {
743 synthesis_parts.push(sentence.to_string());
744 }
745
746 if top_holder_pct.map(|p| p > 30.0).unwrap_or(false) {
747 synthesis_parts.push("Concentration risk: top holder holds significant share.".to_string());
748 }
749
750 use scope::compliance::token_risk::{LiquidityTier, liquidity_tier};
752 let tier = liquidity_tier(liquidity_usd);
753 match tier {
754 LiquidityTier::Good => synthesis_parts.push("Strong liquidity depth.".to_string()),
755 LiquidityTier::VeryLow | LiquidityTier::Low => {
756 synthesis_parts.push("Limited liquidity — slippage risk for larger trades.".to_string())
757 }
758 LiquidityTier::Moderate => {}
759 }
760
761 let synthesis = synthesis_parts.join(" ");
762
763 let key_takeaway = if risk_summary.score >= 7 {
764 format!(
765 "High risk ({}): {} — exercise caution.",
766 risk_summary.score,
767 risk_summary
768 .concerns
769 .first()
770 .cloned()
771 .unwrap_or_else(|| "multiple factors".to_string())
772 )
773 } else if let Some(takeaway) = peg_words.as_ref().and_then(|w| w.takeaway) {
774 takeaway.to_string()
775 } else if !risk_summary.positives.is_empty() && risk_summary.concerns.is_empty() {
776 "Favorable risk profile — standard diligence applies.".to_string()
777 } else {
778 format!(
779 "Risk {}/10 ({}) — weigh concerns against use case.",
780 risk_summary.score, risk_summary.level
781 )
782 };
783
784 let mut recommendations = Vec::new();
785 if risk_summary.score >= 6 {
786 recommendations
787 .push("Consider smaller position sizes or avoid until risk clears.".to_string());
788 }
789 if top_holder_pct.map(|p| p > 25.0).unwrap_or(false) {
790 recommendations.push("Monitor top holder movements for distribution changes.".to_string());
791 }
792 if is_stablecoin && peg_words.as_ref().is_none_or(|w| w.verify_elsewhere) {
793 recommendations.push("Verify peg across multiple DEX/CEX venues.".to_string());
794 }
795 if matches!(tier, LiquidityTier::VeryLow | LiquidityTier::Low) && risk_summary.score <= 5 {
796 recommendations.push("Use limit orders or split trades to manage slippage.".to_string());
797 }
798
799 MetaAnalysis {
800 synthesis,
801 key_takeaway,
802 recommendations,
803 }
804}
805
806#[cfg(test)]
807mod tests {
808 use super::*;
809 use scope::market::{BookSource, HealthThresholds, OrderBook, OrderBookLevel};
810
811 fn summary_with_peg(peg_target: f64, mid_price: Option<f64>) -> MarketSummary {
812 let level = |price| OrderBookLevel {
813 price,
814 quantity: 1000.0,
815 };
816 let book = OrderBook {
817 pair: "X/USDT".to_string(),
818 source: BookSource::Exchange,
819 bids: vec![level(peg_target)],
820 asks: vec![level(peg_target)],
821 };
822 let thresholds = HealthThresholds {
823 peg_target,
824 ..HealthThresholds::default()
825 };
826 let mut summary = MarketSummary::from_order_book(&book, &thresholds, None);
827 summary.mid_price = mid_price;
828 summary.healthy = true;
830 summary
831 }
832
833 #[test]
834 fn test_peg_line_uses_the_summary_peg_target_not_one() {
835 let on_peg = summary_with_peg(0.5, Some(0.5003));
837 assert_eq!(on_peg.peg_status(), PegStatus::OnPeg);
838 let line = peg_line("v", "X/USDT", &on_peg);
839 assert!(
840 line.contains("Peg healthy") && line.contains("6.0 bps"),
841 "{line}"
842 );
843 assert_eq!(
844 summary_with_peg(0.5, Some(0.5020)).peg_status(),
845 PegStatus::SlightDeviation
846 );
847 assert_eq!(
848 summary_with_peg(0.5, Some(0.52)).peg_status(),
849 PegStatus::Deviation
850 );
851 }
852
853 #[test]
854 fn test_peg_line_never_says_healthy_without_a_mid() {
855 let summary = summary_with_peg(1.0, None);
856 assert_eq!(summary.peg_status(), PegStatus::NoPrice);
857 assert!(!peg_line("v", "X/USDT", &summary).contains("Peg healthy"));
858 }
859
860 #[test]
861 fn test_peg_line_never_says_healthy_when_the_market_summary_is_unhealthy() {
862 let mut summary = summary_with_peg(1.0, Some(1.0));
863 summary.healthy = false;
864 assert_eq!(summary.peg_status(), PegStatus::BookUnhealthy);
865 assert!(!peg_line("v", "X/USDT", &summary).contains("Peg healthy"));
866 }
867
868 #[test]
869 fn test_book_unhealthy_is_worded_the_same_on_every_line() {
870 let mut summary = summary_with_peg(1.0, Some(1.0));
871 summary.healthy = false;
872 let status = summary.peg_status();
873 let line = peg_line("v", "X/USDT", &summary);
874 assert!(line.contains("market health checks fail"), "{line}");
875 let risk = report::TokenRiskSummary {
876 score: 3,
877 level: "Low",
878 emoji: "🟢",
879 concerns: vec![],
880 positives: vec![],
881 };
882 let meta = meta_analysis_token(&risk, true, Some(status), None, 5_000_000.0);
883 assert!(
884 meta.synthesis.contains("health checks fail"),
885 "{}",
886 meta.synthesis
887 );
888 assert!(!meta.synthesis.contains("deviation detected"));
889 assert!(meta.key_takeaway.contains("health checks fail"));
890 assert!(meta.recommendations.iter().any(|r| r.contains("peg")));
891 }
892
893 #[test]
894 fn test_peg_bands_follow_the_configured_peg_range() {
895 let mut summary = summary_with_peg(1.0, Some(1.004));
897 summary.peg_range = 0.01;
898 assert_eq!(summary.peg_status(), PegStatus::OnPeg);
899 summary.peg_range = 0.001;
900 assert_eq!(summary.peg_status(), PegStatus::SlightDeviation);
901 }
902
903 use async_trait::async_trait;
904 use scope::chains::{
905 Balance as ChainBalance, ChainClient, ChainClientFactory, DexDataSource,
906 Token as ChainToken, TokenBalance as ChainTokenBalance, Transaction as ChainTransaction,
907 };
908
909 fn quiet() -> crate::cli::output::Output {
910 crate::cli::output::Output::capture().0
911 }
912
913 struct MockChainClient;
918
919 #[async_trait]
920 impl ChainClient for MockChainClient {
921 fn chain_name(&self) -> &str {
922 "ethereum"
923 }
924 fn native_token_symbol(&self) -> &str {
925 "ETH"
926 }
927 async fn get_balance(&self, _address: &str) -> scope::error::Result<ChainBalance> {
928 Ok(ChainBalance {
929 raw: "1000000000000000000".to_string(),
930 formatted: "1.0 ETH".to_string(),
931 decimals: 18,
932 symbol: "ETH".to_string(),
933 usd_value: Some(2500.0),
934 })
935 }
936 async fn enrich_balance_usd(&self, balance: &mut ChainBalance) {
937 balance.usd_value = Some(2500.0);
938 }
939 async fn get_transaction(&self, _hash: &str) -> scope::error::Result<ChainTransaction> {
940 Ok(ChainTransaction {
941 hash: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
942 .to_string(),
943 block_number: Some(12345678),
944 timestamp: Some(1700000000),
945 from: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
946 to: Some("0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string()),
947 value: "1000000000000000000".to_string(),
948 gas_limit: 21000,
949 gas_used: Some(21000),
950 gas_price: "20000000000".to_string(),
951 nonce: 42,
952 input: "0xa9059cbb0000000000000000000000001234".to_string(),
953 status: Some(true),
954 })
955 }
956 async fn get_transactions(
957 &self,
958 _address: &str,
959 _limit: u32,
960 ) -> scope::error::Result<Vec<ChainTransaction>> {
961 Ok(vec![])
962 }
963 async fn get_block_number(&self) -> scope::error::Result<u64> {
964 Ok(12345678)
965 }
966 async fn get_token_balances(
967 &self,
968 _address: &str,
969 ) -> scope::error::Result<Vec<ChainTokenBalance>> {
970 Ok(vec![
971 ChainTokenBalance {
972 token: ChainToken {
973 contract_address: "0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string(),
974 symbol: "USDT".to_string(),
975 name: "Tether USD".to_string(),
976 decimals: 6,
977 },
978 balance: "1000000".to_string(),
979 formatted_balance: "1.0".to_string(),
980 usd_value: Some(1.0),
981 },
982 ChainTokenBalance {
983 token: ChainToken {
984 contract_address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string(),
985 symbol: "USDC".to_string(),
986 name: "USD Coin".to_string(),
987 decimals: 6,
988 },
989 balance: "5000000".to_string(),
990 formatted_balance: "5.0".to_string(),
991 usd_value: Some(5.0),
992 },
993 ])
994 }
995 async fn get_code(&self, _address: &str) -> scope::error::Result<String> {
996 Ok("0x".to_string()) }
998 }
999
1000 struct MockFactory;
1001
1002 impl ChainClientFactory for MockFactory {
1003 fn http(&self) -> std::sync::Arc<dyn scope::http::HttpClient> {
1004 scope::http::test_transport()
1005 }
1006 fn create_chain_client(&self, _chain: &str) -> scope::error::Result<Box<dyn ChainClient>> {
1007 Ok(Box::new(MockChainClient))
1008 }
1009 fn create_dex_client(&self) -> Box<dyn DexDataSource> {
1010 let http: std::sync::Arc<dyn scope::http::HttpClient> =
1011 std::sync::Arc::new(scope::http::NativeHttpClient::new().unwrap());
1012 scope::chains::DefaultClientFactory {
1013 chains_config: Default::default(),
1014 http,
1015 }
1016 .create_dex_client()
1017 }
1018 }
1019
1020 struct MockContractClient;
1022
1023 #[async_trait]
1024 impl ChainClient for MockContractClient {
1025 fn chain_name(&self) -> &str {
1026 "ethereum"
1027 }
1028 fn native_token_symbol(&self) -> &str {
1029 "ETH"
1030 }
1031 async fn get_balance(&self, _address: &str) -> scope::error::Result<ChainBalance> {
1032 Ok(ChainBalance {
1033 raw: "0".to_string(),
1034 formatted: "0.0 ETH".to_string(),
1035 decimals: 18,
1036 symbol: "ETH".to_string(),
1037 usd_value: Some(0.0),
1038 })
1039 }
1040 async fn enrich_balance_usd(&self, _balance: &mut ChainBalance) {}
1041 async fn get_transaction(&self, hash: &str) -> scope::error::Result<ChainTransaction> {
1042 Ok(ChainTransaction {
1043 hash: hash.to_string(),
1044 block_number: Some(100),
1045 timestamp: Some(1700000000),
1046 from: "0xfrom".to_string(),
1047 to: None, value: "0".to_string(),
1049 gas_limit: 100000,
1050 gas_used: Some(80000),
1051 gas_price: "10000000000".to_string(),
1052 nonce: 0,
1053 input: "0x60806040".to_string(),
1054 status: Some(false), })
1056 }
1057 async fn get_transactions(
1058 &self,
1059 _address: &str,
1060 _limit: u32,
1061 ) -> scope::error::Result<Vec<ChainTransaction>> {
1062 Ok(vec![])
1063 }
1064 async fn get_block_number(&self) -> scope::error::Result<u64> {
1065 Ok(100)
1066 }
1067 async fn get_token_balances(
1068 &self,
1069 _address: &str,
1070 ) -> scope::error::Result<Vec<ChainTokenBalance>> {
1071 Ok(vec![])
1072 }
1073 async fn get_code(&self, _address: &str) -> scope::error::Result<String> {
1074 Ok("0x6080604052".to_string()) }
1076 }
1077
1078 struct MockContractFactory;
1079
1080 impl ChainClientFactory for MockContractFactory {
1081 fn http(&self) -> std::sync::Arc<dyn scope::http::HttpClient> {
1082 scope::http::test_transport()
1083 }
1084 fn create_chain_client(&self, _chain: &str) -> scope::error::Result<Box<dyn ChainClient>> {
1085 Ok(Box::new(MockContractClient))
1086 }
1087 fn create_dex_client(&self) -> Box<dyn DexDataSource> {
1088 let http: std::sync::Arc<dyn scope::http::HttpClient> =
1089 std::sync::Arc::new(scope::http::NativeHttpClient::new().unwrap());
1090 scope::chains::DefaultClientFactory {
1091 chains_config: Default::default(),
1092 http,
1093 }
1094 .create_dex_client()
1095 }
1096 }
1097
1098 struct MockDexDataSource;
1100
1101 #[async_trait]
1102 impl DexDataSource for MockDexDataSource {
1103 async fn get_token_price(&self, _chain: &str, _address: &str) -> Option<f64> {
1104 Some(1.0)
1105 }
1106
1107 async fn get_native_token_price(&self, _chain: &str) -> Option<f64> {
1108 Some(2500.0)
1109 }
1110
1111 async fn get_token_data(
1112 &self,
1113 _chain: &str,
1114 address: &str,
1115 ) -> scope::error::Result<scope::chains::dex::DexTokenData> {
1116 use scope::chains::{DexPair, PricePoint, VolumePoint};
1117 Ok(scope::chains::dex::DexTokenData {
1118 address: address.to_string(),
1119 symbol: "TEST".to_string(),
1120 name: "Test Token".to_string(),
1121 price_usd: 1.5,
1122 price_change_24h: 5.2,
1123 price_change_6h: 2.1,
1124 price_change_1h: 0.5,
1125 price_change_5m: 0.1,
1126 volume_24h: 1_000_000.0,
1127 volume_6h: 250_000.0,
1128 volume_1h: 50_000.0,
1129 liquidity_usd: 500_000.0,
1130 market_cap: Some(10_000_000.0),
1131 fdv: Some(12_000_000.0),
1132 pairs: vec![DexPair {
1133 dex_name: "Uniswap V3".to_string(),
1134 pair_address: "0xpair123".to_string(),
1135 base_token: "TEST".to_string(),
1136 quote_token: "USDC".to_string(),
1137 price_usd: 1.5,
1138 liquidity_usd: 500_000.0,
1139 liquidity_base: None,
1140 volume_24h: 1_000_000.0,
1141 price_change_24h: 5.2,
1142 buys_24h: 100,
1143 sells_24h: 80,
1144 buys_6h: 20,
1145 sells_6h: 15,
1146 buys_1h: 5,
1147 sells_1h: 3,
1148 pair_created_at: Some(1690000000),
1149 url: Some("https://dexscreener.com/ethereum/0xpair123".to_string()),
1150 }],
1151 price_history: vec![PricePoint {
1152 timestamp: 1690000000,
1153 price: 1.5,
1154 }],
1155 volume_history: vec![VolumePoint {
1156 timestamp: 1690000000,
1157 volume: 1_000_000.0,
1158 }],
1159 total_buys_24h: 100,
1160 total_sells_24h: 80,
1161 total_buys_6h: 20,
1162 total_sells_6h: 15,
1163 total_buys_1h: 5,
1164 total_sells_1h: 3,
1165 earliest_pair_created_at: Some(1690000000),
1166 image_url: None,
1167 websites: Vec::new(),
1168 socials: Vec::new(),
1169 dexscreener_url: Some("https://dexscreener.com/ethereum/test".to_string()),
1170 })
1171 }
1172
1173 async fn search_tokens(
1174 &self,
1175 _query: &str,
1176 _chain: Option<&str>,
1177 ) -> scope::error::Result<Vec<scope::chains::TokenSearchResult>> {
1178 Ok(vec![scope::chains::TokenSearchResult {
1179 address: "0xTEST1234567890123456789012345678901234567".to_string(),
1180 symbol: "TEST".to_string(),
1181 name: "Test Token".to_string(),
1182 chain: "ethereum".to_string(),
1183 price_usd: Some(1.5),
1184 volume_24h: 1_000_000.0,
1185 liquidity_usd: 500_000.0,
1186 market_cap: Some(10_000_000.0),
1187 }])
1188 }
1189 }
1190
1191 struct MockTokenChainClient;
1193
1194 #[async_trait]
1195 impl ChainClient for MockTokenChainClient {
1196 fn chain_name(&self) -> &str {
1197 "ethereum"
1198 }
1199 fn native_token_symbol(&self) -> &str {
1200 "ETH"
1201 }
1202 async fn get_balance(&self, _address: &str) -> scope::error::Result<ChainBalance> {
1203 Ok(ChainBalance {
1204 raw: "1000000000000000000".to_string(),
1205 formatted: "1.0 ETH".to_string(),
1206 decimals: 18,
1207 symbol: "ETH".to_string(),
1208 usd_value: Some(2500.0),
1209 })
1210 }
1211 async fn enrich_balance_usd(&self, balance: &mut ChainBalance) {
1212 balance.usd_value = Some(2500.0);
1213 }
1214 async fn get_transaction(&self, _hash: &str) -> scope::error::Result<ChainTransaction> {
1215 Ok(ChainTransaction {
1216 hash: "0xabc123".to_string(),
1217 block_number: Some(12345678),
1218 timestamp: Some(1700000000),
1219 from: "0xfrom".to_string(),
1220 to: Some("0xto".to_string()),
1221 value: "0".to_string(),
1222 gas_limit: 21000,
1223 gas_used: Some(21000),
1224 gas_price: "20000000000".to_string(),
1225 nonce: 42,
1226 input: "0x".to_string(),
1227 status: Some(true),
1228 })
1229 }
1230 async fn get_transactions(
1231 &self,
1232 _address: &str,
1233 _limit: u32,
1234 ) -> scope::error::Result<Vec<ChainTransaction>> {
1235 Ok(vec![])
1236 }
1237 async fn get_block_number(&self) -> scope::error::Result<u64> {
1238 Ok(12345678)
1239 }
1240 async fn get_token_balances(
1241 &self,
1242 _address: &str,
1243 ) -> scope::error::Result<Vec<ChainTokenBalance>> {
1244 Ok(vec![])
1245 }
1246 async fn get_code(&self, _address: &str) -> scope::error::Result<String> {
1247 Ok("0x".to_string())
1248 }
1249 async fn get_token_holders(
1250 &self,
1251 _address: &str,
1252 _limit: u32,
1253 ) -> scope::error::Result<Vec<scope::chains::TokenHolder>> {
1254 Ok(vec![
1256 scope::chains::TokenHolder {
1257 address: "0x1111111111111111111111111111111111111111".to_string(),
1258 balance: "3500000000000000000000000".to_string(),
1259 formatted_balance: "3500000.0".to_string(),
1260 percentage: 35.0, rank: 1,
1262 },
1263 scope::chains::TokenHolder {
1264 address: "0x2222222222222222222222222222222222222222".to_string(),
1265 balance: "1500000000000000000000000".to_string(),
1266 formatted_balance: "1500000.0".to_string(),
1267 percentage: 15.0,
1268 rank: 2,
1269 },
1270 scope::chains::TokenHolder {
1271 address: "0x3333333333333333333333333333333333333333".to_string(),
1272 balance: "1000000000000000000000000".to_string(),
1273 formatted_balance: "1000000.0".to_string(),
1274 percentage: 10.0,
1275 rank: 3,
1276 },
1277 ])
1278 }
1279 }
1280
1281 struct MockTokenFactory;
1283
1284 impl ChainClientFactory for MockTokenFactory {
1285 fn http(&self) -> std::sync::Arc<dyn scope::http::HttpClient> {
1286 scope::http::test_transport()
1287 }
1288 fn create_chain_client(&self, _chain: &str) -> scope::error::Result<Box<dyn ChainClient>> {
1289 Ok(Box::new(MockTokenChainClient))
1290 }
1291 fn create_dex_client(&self) -> Box<dyn DexDataSource> {
1292 Box::new(MockDexDataSource)
1293 }
1294 }
1295
1296 #[tokio::test]
1301 async fn test_run_address_eoa() {
1302 let config = Config::default();
1303 let factory = MockFactory;
1304 let args = InsightsArgs {
1305 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
1306 token_contract: None,
1307 chain: None,
1308 decode: false,
1309 trace: false,
1310 health: Default::default(),
1311 };
1312 let result = run(args, &config, &factory, &quiet()).await;
1313 assert!(result.is_ok());
1314 }
1315
1316 #[tokio::test]
1317 async fn test_run_address_contract() {
1318 let config = Config::default();
1319 let factory = MockContractFactory;
1320 let args = InsightsArgs {
1321 target: "0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string(),
1322 token_contract: None,
1323 chain: None,
1324 decode: false,
1325 trace: false,
1326 health: Default::default(),
1327 };
1328 let result = run(args, &config, &factory, &quiet()).await;
1329 assert!(result.is_ok());
1330 }
1331
1332 #[tokio::test]
1333 async fn test_run_transaction() {
1334 let config = Config::default();
1335 let factory = MockFactory;
1336 let args = InsightsArgs {
1337 target: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
1338 .to_string(),
1339 token_contract: None,
1340 chain: None,
1341 decode: false,
1342 trace: false,
1343 health: Default::default(),
1344 };
1345 let result = run(args, &config, &factory, &quiet()).await;
1346 assert!(result.is_ok());
1347 }
1348
1349 #[tokio::test]
1350 async fn test_run_transaction_failed() {
1351 let config = Config::default();
1352 let factory = MockContractFactory;
1353 let args = InsightsArgs {
1354 target: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
1355 .to_string(),
1356 token_contract: None,
1357 chain: Some("ethereum".to_string()),
1358 decode: true,
1359 trace: false,
1360 health: Default::default(),
1361 };
1362 let result = run(args, &config, &factory, &quiet()).await;
1363 assert!(result.is_ok());
1364 }
1365
1366 #[tokio::test]
1367 async fn test_run_address_with_chain_override() {
1368 let config = Config::default();
1369 let factory = MockFactory;
1370 let args = InsightsArgs {
1371 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
1372 token_contract: None,
1373 chain: Some("polygon".to_string()),
1374 decode: false,
1375 trace: false,
1376 health: Default::default(),
1377 };
1378 let result = run(args, &config, &factory, &quiet()).await;
1379 assert!(result.is_ok());
1380 }
1381
1382 #[tokio::test]
1383 async fn test_insights_run_token() {
1384 let config = Config::default();
1385 let factory = MockTokenFactory;
1386 let args = InsightsArgs {
1387 target: "TEST".to_string(),
1388 token_contract: None,
1389 chain: Some("ethereum".to_string()),
1390 decode: false,
1391 trace: false,
1392 health: Default::default(),
1393 };
1394 let (out, cap) = crate::cli::output::Output::capture();
1395 run(args, &config, &factory, &out).await.unwrap();
1396 let text = cap.out();
1398 assert!(text.contains("**Liquidity (24h):** $500.00K"), "{}", text);
1399 assert!(text.contains("**Volume (24h):** $1.00M"), "{}", text);
1400 assert!(!text.contains("$$"), "{}", text);
1401 }
1402
1403 #[tokio::test]
1404 async fn test_insights_run_token_with_concentration_warning() {
1405 let config = Config::default();
1406 let factory = MockTokenFactory;
1407 let args = InsightsArgs {
1408 target: "0xTEST1234567890123456789012345678901234567".to_string(),
1409 token_contract: None,
1410 chain: Some("ethereum".to_string()),
1411 decode: false,
1412 trace: false,
1413 health: Default::default(),
1414 };
1415 let result = run(args, &config, &factory, &quiet()).await;
1416 assert!(result.is_ok());
1417 }
1418
1419 #[test]
1424 fn test_infer_target_evm_address() {
1425 let t = infer_target("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", None);
1426 assert!(matches!(t, InferredTarget::Address { chain } if chain == "ethereum"));
1427 }
1428
1429 #[test]
1430 fn test_infer_target_tron_address() {
1431 let t = infer_target("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", None);
1432 assert!(matches!(t, InferredTarget::Address { chain } if chain == "tron"));
1433 }
1434
1435 #[test]
1436 fn test_infer_target_solana_address() {
1437 let t = infer_target("DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy", None);
1438 assert!(matches!(t, InferredTarget::Address { chain } if chain == "solana"));
1439 }
1440
1441 #[test]
1442 fn test_infer_target_evm_tx_hash() {
1443 let t = infer_target(
1444 "0xabc123def456789012345678901234567890123456789012345678901234abcd",
1445 None,
1446 );
1447 assert!(matches!(t, InferredTarget::Transaction { chain } if chain == "ethereum"));
1448 }
1449
1450 #[test]
1451 fn test_infer_target_tron_tx_hash() {
1452 let t = infer_target(
1453 "abc123def456789012345678901234567890123456789012345678901234abcd",
1454 None,
1455 );
1456 assert!(matches!(t, InferredTarget::Transaction { chain } if chain == "tron"));
1457 }
1458
1459 #[test]
1460 fn test_infer_target_token_symbol() {
1461 let t = infer_target("USDC", None);
1462 assert!(matches!(t, InferredTarget::Token { chain } if chain == "ethereum"));
1463 }
1464
1465 #[test]
1466 fn test_infer_target_chain_override() {
1467 let t = infer_target(
1468 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
1469 Some("polygon"),
1470 );
1471 assert!(matches!(t, InferredTarget::Address { chain } if chain == "polygon"));
1472 }
1473
1474 #[test]
1475 fn test_infer_target_token_with_chain_override() {
1476 let t = infer_target("USDC", Some("solana"));
1477 assert!(matches!(t, InferredTarget::Token { chain } if chain == "solana"));
1478 }
1479
1480 #[test]
1481 fn test_classify_tx_type() {
1482 assert_eq!(
1483 classify_tx_type("0xa9059cbb1234...", Some("0xto")),
1484 "ERC-20 Transfer"
1485 );
1486 assert_eq!(
1487 classify_tx_type("0x095ea7b3abcd...", Some("0xto")),
1488 "ERC-20 Approve"
1489 );
1490 assert_eq!(classify_tx_type("0x", Some("0xto")), "Native Transfer");
1491 assert_eq!(classify_tx_type("", None), "Contract Creation");
1492 }
1493
1494 #[test]
1495 fn test_format_tx_value() {
1496 let (fmt, high) = format_tx_value("0xDE0B6B3A7640000", "ethereum"); assert!(fmt.contains("1.0") && fmt.contains("ETH"));
1498 assert!(!high);
1499 let (_, high2) = format_tx_value("0x52B7D2DCC80CD2E4000000", "ethereum"); assert!(high2);
1501 }
1502
1503 #[test]
1504 fn test_is_stablecoin() {
1505 assert!(is_stablecoin_symbol("USDC"));
1506 assert!(is_stablecoin_symbol("usdt"));
1507 assert!(is_stablecoin_symbol("DAI"));
1508 assert!(is_stablecoin_symbol("BUSD"));
1509 assert!(is_stablecoin_symbol("TUSD"));
1510 assert!(is_stablecoin_symbol("USDP"));
1511 assert!(is_stablecoin_symbol("FRAX"));
1512 assert!(is_stablecoin_symbol("LUSD"));
1513 assert!(is_stablecoin_symbol("GUSD"));
1514 assert!(!is_stablecoin_symbol("ETH"));
1515 assert!(!is_stablecoin_symbol("PEPE"));
1516 assert!(!is_stablecoin_symbol("WBTC"));
1517 }
1518
1519 #[test]
1520 fn test_is_stablecoin_empty_string() {
1521 assert!(!is_stablecoin_symbol(""));
1522 }
1523
1524 #[test]
1525 fn test_is_stablecoin_case_insensitive() {
1526 assert!(is_stablecoin_symbol("UsDc"));
1528 assert!(is_stablecoin_symbol("FraX"));
1529 assert!(!is_stablecoin_symbol("SOL")); }
1531
1532 #[test]
1537 fn test_target_type_label_address() {
1538 let t = InferredTarget::Address {
1539 chain: "ethereum".to_string(),
1540 };
1541 assert_eq!(target_type_label(&t), "Address");
1542 }
1543
1544 #[test]
1545 fn test_target_type_label_transaction() {
1546 let t = InferredTarget::Transaction {
1547 chain: "ethereum".to_string(),
1548 };
1549 assert_eq!(target_type_label(&t), "Transaction");
1550 }
1551
1552 #[test]
1553 fn test_target_type_label_token() {
1554 let t = InferredTarget::Token {
1555 chain: "ethereum".to_string(),
1556 };
1557 assert_eq!(target_type_label(&t), "Token");
1558 }
1559
1560 #[test]
1561 fn test_chain_label_address() {
1562 let t = InferredTarget::Address {
1563 chain: "polygon".to_string(),
1564 };
1565 assert_eq!(chain_label(&t), "polygon");
1566 }
1567
1568 #[test]
1569 fn test_chain_label_transaction() {
1570 let t = InferredTarget::Transaction {
1571 chain: "tron".to_string(),
1572 };
1573 assert_eq!(chain_label(&t), "tron");
1574 }
1575
1576 #[test]
1577 fn test_chain_label_token() {
1578 let t = InferredTarget::Token {
1579 chain: "solana".to_string(),
1580 };
1581 assert_eq!(chain_label(&t), "solana");
1582 }
1583
1584 #[test]
1589 fn test_classify_tx_type_dex_swaps() {
1590 assert_eq!(
1591 classify_tx_type("0x38ed173900000...", Some("0xrouter")),
1592 "DEX Swap"
1593 );
1594 assert_eq!(
1595 classify_tx_type("0x5c11d79500000...", Some("0xrouter")),
1596 "DEX Swap"
1597 );
1598 assert_eq!(
1599 classify_tx_type("0x4a25d94a00000...", Some("0xrouter")),
1600 "DEX Swap"
1601 );
1602 assert_eq!(
1603 classify_tx_type("0x8803dbee00000...", Some("0xrouter")),
1604 "DEX Swap"
1605 );
1606 assert_eq!(
1607 classify_tx_type("0x7ff36ab500000...", Some("0xrouter")),
1608 "DEX Swap"
1609 );
1610 assert_eq!(
1611 classify_tx_type("0x18cbafe500000...", Some("0xrouter")),
1612 "DEX Swap"
1613 );
1614 assert_eq!(
1615 classify_tx_type("0xfb3bdb4100000...", Some("0xrouter")),
1616 "DEX Swap"
1617 );
1618 assert_eq!(
1619 classify_tx_type("0xb6f9de9500000...", Some("0xrouter")),
1620 "DEX Swap"
1621 );
1622 }
1623
1624 #[test]
1625 fn test_classify_tx_type_multicall() {
1626 assert_eq!(
1627 classify_tx_type("0xac9650d800000...", Some("0xcontract")),
1628 "Multicall"
1629 );
1630 assert_eq!(
1631 classify_tx_type("0x5ae401dc00000...", Some("0xcontract")),
1632 "Multicall"
1633 );
1634 }
1635
1636 #[test]
1637 fn test_classify_tx_type_transfer_from() {
1638 assert_eq!(
1639 classify_tx_type("0x23b872dd00000...", Some("0xtoken")),
1640 "ERC-20 Transfer From"
1641 );
1642 }
1643
1644 #[test]
1645 fn test_classify_tx_type_contract_call() {
1646 assert_eq!(
1647 classify_tx_type("0xdeadbeef00000...", Some("0xcontract")),
1648 "Contract Call"
1649 );
1650 }
1651
1652 #[test]
1653 fn test_classify_tx_type_native_transfer_empty() {
1654 assert_eq!(classify_tx_type("", Some("0xrecipient")), "Native Transfer");
1655 }
1656
1657 #[test]
1662 fn test_format_tx_value_zero() {
1663 let (fmt, high) = format_tx_value("0x0", "ethereum");
1664 assert!(fmt.contains("0.000000"));
1665 assert!(fmt.contains("ETH"));
1666 assert!(!high);
1667 }
1668
1669 #[test]
1670 fn test_format_tx_value_empty_hex() {
1671 let (fmt, high) = format_tx_value("0x", "ethereum");
1672 assert!(fmt.contains("0.000000"));
1673 assert!(!high);
1674 }
1675
1676 #[test]
1677 fn test_format_tx_value_decimal_string() {
1678 let (fmt, high) = format_tx_value("1000000000000000000", "ethereum"); assert!(fmt.contains("1.0"));
1680 assert!(fmt.contains("ETH"));
1681 assert!(!high);
1682 }
1683
1684 #[test]
1685 fn test_format_tx_value_solana() {
1686 let (fmt, high) = format_tx_value("1000000000", "solana"); assert!(fmt.contains("1.0"));
1688 assert!(fmt.contains("SOL"));
1689 assert!(!high);
1690 }
1691
1692 #[test]
1693 fn test_format_tx_value_tron() {
1694 let (fmt, high) = format_tx_value("1000000", "tron"); assert!(fmt.contains("1.0"));
1696 assert!(fmt.contains("TRX"));
1697 assert!(!high);
1698 }
1699
1700 #[test]
1701 fn test_format_tx_value_polygon() {
1702 let (fmt, _) = format_tx_value("1000000000000000000", "polygon");
1703 assert!(fmt.contains("MATIC") || fmt.contains("POL"));
1704 }
1705
1706 #[test]
1707 fn test_format_tx_value_bsc() {
1708 let (fmt, _) = format_tx_value("1000000000000000000", "bsc");
1709 assert!(fmt.contains("BNB"));
1710 }
1711
1712 #[test]
1713 fn test_format_tx_value_arbitrum() {
1714 let (fmt, _) = format_tx_value("1000000000000000000", "arbitrum");
1715 assert!(fmt.contains("ETH"));
1716 }
1717
1718 #[test]
1719 fn test_format_tx_value_optimism() {
1720 let (fmt, _) = format_tx_value("1000000000000000000", "optimism");
1721 assert!(fmt.contains("ETH"));
1722 }
1723
1724 #[test]
1725 fn test_format_tx_value_base() {
1726 let (fmt, _) = format_tx_value("1000000000000000000", "base");
1727 assert!(fmt.contains("ETH"));
1728 }
1729
1730 #[test]
1731 fn test_format_tx_value_aegis() {
1732 let (fmt, _) = format_tx_value("1000000000000000000", "aegis");
1734 assert!(fmt.contains("1.0") || fmt.contains("1.000000"));
1735 }
1736
1737 #[test]
1738 fn test_format_tx_value_unknown_chain_defaults_18_decimals() {
1739 let (fmt, _) = format_tx_value("1000000000000000000", "unknown_chain");
1740 assert!(fmt.contains("1") || fmt.contains("ETH"));
1741 }
1742
1743 #[test]
1744 fn test_format_tx_value_invalid_hex_parse() {
1745 let (fmt, high) = format_tx_value("0xZZZZ", "ethereum");
1746 assert!(fmt.contains("0.000000"));
1747 assert!(!high);
1748 }
1749
1750 #[test]
1751 fn test_format_tx_value_high_value_threshold() {
1752 let (_, high) = format_tx_value("11000000000000000000", "ethereum"); assert!(high);
1755 let (_, high2) = format_tx_value("10000000000000000000", "ethereum"); assert!(!high2); }
1758
1759 #[test]
1764 fn test_meta_analysis_address_contract_high_value() {
1765 let meta = meta_analysis_address(true, Some(2_000_000.0), 10, None, None);
1766 assert!(meta.synthesis.contains("contract"));
1767 assert!(meta.synthesis.contains("Significant value"));
1768 assert!(meta.synthesis.contains("Diversified"));
1769 assert!(meta.recommendations.iter().any(|r| r.contains("contract")));
1770 }
1771
1772 #[test]
1773 fn test_risk_line_gives_a_score_only_for_a_complete_assessment() {
1774 use scope::compliance::risk::{RiskAssessment, RiskLevel};
1775 let mut risk = RiskAssessment {
1776 address: "0x1".into(),
1777 chain: "ethereum".into(),
1778 overall_score: 2.4,
1779 risk_level: RiskLevel::Low,
1780 factors: Vec::new(),
1781 not_assessed: Vec::new(),
1782 assessed_at: chrono::Utc::now(),
1783 recommendations: Vec::new(),
1784 };
1785 assert!(
1786 risk_line(&risk).contains("2.4/10 (Low)"),
1787 "{}",
1788 risk_line(&risk)
1789 );
1790
1791 risk.risk_level = RiskLevel::Incomplete;
1792 risk.not_assessed = vec!["Source of Funds: no Etherscan API key".into()];
1793 let line = risk_line(&risk);
1794 assert!(!line.contains("/10"), "{line}");
1795 assert!(line.contains("not assessed in full"), "{line}");
1796 assert!(
1797 line.contains(" - Source of Funds: no Etherscan API key"),
1798 "{line}"
1799 );
1800 }
1801
1802 #[test]
1803 fn test_meta_analysis_does_not_call_an_incomplete_assessment_low() {
1804 let level = scope::compliance::risk::RiskLevel::Incomplete;
1806 let meta = meta_analysis_address(false, Some(50.0), 0, Some(2.0), Some(&level));
1807 assert!(!meta.synthesis.contains("Low risk"), "{}", meta.synthesis);
1808 assert!(
1809 !meta.key_takeaway.contains("Overall risk"),
1810 "{}",
1811 meta.key_takeaway
1812 );
1813
1814 let low = scope::compliance::risk::RiskLevel::Low;
1815 let meta = meta_analysis_address(false, Some(50.0), 0, Some(2.0), Some(&low));
1816 assert!(meta.synthesis.contains("Low risk"), "{}", meta.synthesis);
1817 }
1818
1819 #[test]
1820 fn test_meta_analysis_address_eoa_moderate_value() {
1821 let meta = meta_analysis_address(false, Some(50_000.0), 3, None, None);
1822 assert!(meta.synthesis.contains("wallet (EOA)"));
1823 assert!(meta.synthesis.contains("Moderate value"));
1824 }
1825
1826 #[test]
1827 fn test_meta_analysis_address_minimal_value() {
1828 let meta = meta_analysis_address(false, Some(0.5), 0, None, None);
1829 assert!(meta.synthesis.contains("Minimal value"));
1830 }
1831
1832 #[test]
1833 fn test_meta_analysis_address_single_token() {
1834 let meta = meta_analysis_address(false, None, 1, None, None);
1835 assert!(meta.synthesis.contains("Concentrated in a single token"));
1836 }
1837
1838 #[test]
1839 fn test_meta_analysis_address_high_risk() {
1840 use scope::compliance::risk::RiskLevel;
1841 let level = RiskLevel::High;
1842 let meta = meta_analysis_address(false, None, 0, Some(8.5), Some(&level));
1843 assert!(meta.synthesis.contains("Elevated risk"));
1844 assert!(meta.key_takeaway.contains("scrutiny"));
1845 assert!(
1846 meta.recommendations
1847 .iter()
1848 .any(|r| r.contains("unusual transaction"))
1849 );
1850 }
1851
1852 #[test]
1853 fn test_meta_analysis_address_low_risk() {
1854 use scope::compliance::risk::RiskLevel;
1855 let level = RiskLevel::Low;
1856 let meta = meta_analysis_address(false, None, 0, Some(2.0), Some(&level));
1857 assert!(meta.synthesis.contains("Low risk"));
1858 }
1859
1860 #[test]
1861 fn test_meta_analysis_address_contract_no_value() {
1862 let meta = meta_analysis_address(true, None, 0, None, None);
1863 assert!(meta.key_takeaway.contains("Contract address"));
1864 assert!(
1865 meta.recommendations
1866 .iter()
1867 .any(|r| r.contains("Confirm contract"))
1868 );
1869 }
1870
1871 #[test]
1872 fn test_meta_analysis_address_high_value_wallet() {
1873 let meta = meta_analysis_address(false, Some(150_000.0), 0, None, None);
1874 assert!(meta.key_takeaway.contains("High-value wallet"));
1875 }
1876
1877 #[test]
1878 fn test_meta_analysis_address_default_takeaway() {
1879 let meta = meta_analysis_address(false, Some(5_000.0), 0, None, None);
1880 assert!(meta.key_takeaway.contains("Review full report"));
1881 }
1882
1883 #[test]
1884 fn test_meta_analysis_address_empty_synthesis() {
1885 let meta = meta_analysis_address(
1886 false,
1887 Some(5_000.0), 2, None,
1890 None,
1891 );
1892 assert!(meta.synthesis.contains("wallet (EOA)"));
1893 }
1894
1895 #[test]
1896 fn test_meta_analysis_address_synthesis_parts_joined() {
1897 let meta = meta_analysis_address(false, None, 0, None, None);
1898 assert!(!meta.synthesis.is_empty());
1899 assert!(meta.synthesis.contains("Address analyzed") || meta.synthesis.contains("wallet"));
1900 }
1901
1902 #[test]
1903 fn test_meta_analysis_address_with_tokens_recommendation() {
1904 let meta = meta_analysis_address(false, None, 3, None, None);
1905 assert!(
1906 meta.recommendations
1907 .iter()
1908 .any(|r| r.contains("Verify token contracts"))
1909 );
1910 }
1911
1912 #[test]
1917 fn test_meta_analysis_tx_successful_native_transfer() {
1918 let meta = meta_analysis_tx("Native Transfer", true, false, "0xfrom", Some("0xto"));
1919 assert!(meta.synthesis.contains("Native Transfer"));
1920 assert!(meta.key_takeaway.contains("Routine"));
1921 assert!(meta.recommendations.is_empty());
1922 }
1923
1924 #[test]
1925 fn test_meta_analysis_tx_failed() {
1926 let meta = meta_analysis_tx("Contract Call", false, false, "0xfrom", Some("0xto"));
1927 assert!(meta.synthesis.contains("failed"));
1928 assert!(meta.key_takeaway.contains("Failed transaction"));
1929 assert!(meta.recommendations.iter().any(|r| r.contains("revert")));
1930 }
1931
1932 #[test]
1933 fn test_meta_analysis_tx_high_value_native() {
1934 let meta = meta_analysis_tx("Native Transfer", true, true, "0xfrom", Some("0xto"));
1935 assert!(meta.synthesis.contains("High-value"));
1936 assert!(meta.key_takeaway.contains("Large native transfer"));
1937 assert!(meta.recommendations.iter().any(|r| r.contains("recipient")));
1938 }
1939
1940 #[test]
1941 fn test_meta_analysis_tx_high_value_contract_call() {
1942 let meta = meta_analysis_tx("DEX Swap", true, true, "0xfrom", Some("0xto"));
1943 assert!(meta.key_takeaway.contains("High-value operation"));
1944 }
1945
1946 #[test]
1947 fn test_meta_analysis_tx_erc20_approve() {
1948 let meta = meta_analysis_tx("ERC-20 Approval", true, false, "0xfrom", Some("0xto"));
1949 assert!(meta.recommendations.iter().any(|r| r.contains("spender")));
1950 }
1951
1952 #[test]
1953 fn test_meta_analysis_tx_failed_high_value() {
1954 let meta = meta_analysis_tx("Contract Call", false, true, "0xfrom", Some("0xto"));
1955 assert!(meta.synthesis.contains("failed"));
1956 assert!(meta.synthesis.contains("High-value"));
1957 assert!(meta.recommendations.len() >= 2);
1958 }
1959
1960 #[test]
1965 fn test_meta_analysis_token_low_risk() {
1966 let summary = report::TokenRiskSummary {
1967 score: 2,
1968 level: "Low",
1969 emoji: "🟢",
1970 concerns: vec![],
1971 positives: vec!["Good liquidity".to_string()],
1972 };
1973 let meta = meta_analysis_token(&summary, false, None, None, 2_000_000.0);
1974 assert!(meta.synthesis.contains("Low-risk"));
1975 assert!(meta.synthesis.contains("Strong liquidity"));
1976 assert!(meta.key_takeaway.contains("Favorable"));
1977 }
1978
1979 #[test]
1980 fn test_meta_analysis_token_high_risk() {
1981 let summary = report::TokenRiskSummary {
1982 score: 8,
1983 level: "High",
1984 emoji: "🔴",
1985 concerns: vec!["Low liquidity".to_string()],
1986 positives: vec![],
1987 };
1988 let meta = meta_analysis_token(&summary, false, None, None, 10_000.0);
1989 assert!(meta.synthesis.contains("Elevated risk"));
1990 assert!(meta.synthesis.contains("Limited liquidity"));
1991 assert!(meta.key_takeaway.contains("High risk"));
1992 assert!(
1993 meta.recommendations
1994 .iter()
1995 .any(|r| r.contains("smaller position"))
1996 );
1997 }
1998
1999 #[test]
2000 fn test_meta_analysis_token_moderate_risk() {
2001 let summary = report::TokenRiskSummary {
2002 score: 5,
2003 level: "Medium",
2004 emoji: "🟡",
2005 concerns: vec!["Some concern".to_string()],
2006 positives: vec!["Some positive".to_string()],
2007 };
2008 let meta = meta_analysis_token(&summary, false, None, None, 500_000.0);
2009 assert!(meta.synthesis.contains("Moderate risk"));
2010 assert!(meta.key_takeaway.contains("Risk 5/10"));
2011 }
2012
2013 #[test]
2014 fn test_meta_analysis_token_stablecoin_healthy_peg() {
2015 let summary = report::TokenRiskSummary {
2016 score: 2,
2017 level: "Low",
2018 emoji: "🟢",
2019 concerns: vec![],
2020 positives: vec!["Stable peg".to_string()],
2021 };
2022 let meta = meta_analysis_token(&summary, true, Some(PegStatus::OnPeg), None, 5_000_000.0);
2023 assert!(meta.synthesis.contains("Stablecoin peg is healthy"));
2024 }
2025
2026 #[test]
2027 fn test_meta_analysis_token_stablecoin_unhealthy_peg() {
2028 let summary = report::TokenRiskSummary {
2029 score: 4,
2030 level: "Medium",
2031 emoji: "🟡",
2032 concerns: vec![],
2033 positives: vec![],
2034 };
2035 let meta = meta_analysis_token(&summary, true, Some(PegStatus::Deviation), None, 500_000.0);
2036 assert!(meta.synthesis.contains("peg deviation"));
2037 assert!(meta.key_takeaway.contains("deviating from peg"));
2038 assert!(meta.recommendations.iter().any(|r| r.contains("peg")));
2039 }
2040
2041 #[test]
2042 fn test_meta_analysis_token_concentration_risk() {
2043 let summary = report::TokenRiskSummary {
2044 score: 5,
2045 level: "Medium",
2046 emoji: "🟡",
2047 concerns: vec![],
2048 positives: vec![],
2049 };
2050 let meta = meta_analysis_token(&summary, false, None, Some(45.0), 500_000.0);
2051 assert!(meta.synthesis.contains("Concentration risk"));
2052 assert!(
2053 meta.recommendations
2054 .iter()
2055 .any(|r| r.contains("top holder"))
2056 );
2057 }
2058
2059 #[test]
2061 fn test_meta_analysis_names_low_liquidity_as_the_report_does() {
2062 let summary = report::TokenRiskSummary {
2063 score: 3,
2064 level: "Low",
2065 emoji: "🟢",
2066 concerns: vec![],
2067 positives: vec![],
2068 };
2069 let meta = meta_analysis_token(&summary, false, None, None, 75_000.0);
2070 assert!(
2071 meta.synthesis.contains("Limited liquidity"),
2072 "{}",
2073 meta.synthesis
2074 );
2075 }
2076
2077 #[test]
2078 fn test_meta_analysis_token_low_liquidity_low_risk() {
2079 let summary = report::TokenRiskSummary {
2080 score: 3,
2081 level: "Low",
2082 emoji: "🟢",
2083 concerns: vec![],
2084 positives: vec![],
2085 };
2086 let meta = meta_analysis_token(&summary, false, None, None, 50_000.0);
2087 assert!(
2088 meta.recommendations
2089 .iter()
2090 .any(|r| r.contains("limit orders") || r.contains("slippage"))
2091 );
2092 }
2093
2094 #[test]
2095 fn test_meta_analysis_token_stablecoin_no_peg_data() {
2096 let summary = report::TokenRiskSummary {
2097 score: 3,
2098 level: "Low",
2099 emoji: "🟢",
2100 concerns: vec![],
2101 positives: vec![],
2102 };
2103 let meta = meta_analysis_token(&summary, true, None, None, 1_000_000.0);
2104 assert!(meta.recommendations.iter().any(|r| r.contains("peg")));
2106 }
2107
2108 #[test]
2109 fn test_meta_analysis_token_mixed_signals_key_takeaway() {
2110 let summary = report::TokenRiskSummary {
2111 score: 5,
2112 level: "Medium",
2113 emoji: "🟡",
2114 concerns: vec!["Some concern".to_string()],
2115 positives: vec!["Some positive".to_string()],
2116 };
2117 let meta = meta_analysis_token(&summary, false, None, None, 500_000.0);
2118 assert!(meta.key_takeaway.contains("Risk 5/10"));
2119 assert!(meta.key_takeaway.contains("Medium"));
2120 }
2121
2122 #[test]
2127 fn test_infer_target_tx_hash_with_chain_override() {
2128 let t = infer_target(
2129 "0xabc123def456789012345678901234567890123456789012345678901234abcd",
2130 Some("polygon"),
2131 );
2132 assert!(matches!(t, InferredTarget::Transaction { chain } if chain == "polygon"));
2133 }
2134
2135 #[test]
2136 fn test_infer_target_whitespace_trimming() {
2137 let t = infer_target(" USDC ", None);
2138 assert!(matches!(t, InferredTarget::Token { .. }));
2139 }
2140
2141 #[test]
2142 fn test_infer_target_long_token_name() {
2143 let t = infer_target("some-random-token-name", None);
2144 assert!(matches!(t, InferredTarget::Token { chain } if chain == "ethereum"));
2145 }
2146
2147 #[test]
2152 fn test_insights_args_debug() {
2153 let args = InsightsArgs {
2154 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
2155 token_contract: None,
2156 chain: Some("ethereum".to_string()),
2157 decode: true,
2158 trace: false,
2159 health: Default::default(),
2160 };
2161 let debug_str = format!("{:?}", args);
2162 assert!(debug_str.contains("InsightsArgs"));
2163 assert!(debug_str.contains("0x742d"));
2164 }
2165
2166 #[test]
2171 fn test_classify_tx_type_contract_creation() {
2172 assert_eq!(classify_tx_type("0xa9059cbb...", None), "Contract Creation");
2173 }
2174
2175 #[test]
2176 fn test_classify_tx_type_erc20_transfer() {
2177 assert_eq!(
2178 classify_tx_type("0xa9059cbb00000000", Some("0x1234")),
2179 "ERC-20 Transfer"
2180 );
2181 }
2182
2183 #[test]
2184 fn test_classify_tx_type_erc20_approve() {
2185 assert_eq!(
2186 classify_tx_type("0x095ea7b3...", Some("0x1234")),
2187 "ERC-20 Approve"
2188 );
2189 }
2190
2191 #[test]
2192 fn test_classify_tx_type_erc20_transfer_from() {
2193 assert_eq!(
2194 classify_tx_type("0x23b872dd...", Some("0x1234")),
2195 "ERC-20 Transfer From"
2196 );
2197 }
2198
2199 #[test]
2200 fn test_classify_tx_type_dex_swap() {
2201 assert_eq!(
2202 classify_tx_type("0x38ed1739...", Some("0x1234")),
2203 "DEX Swap"
2204 );
2205 assert_eq!(
2206 classify_tx_type("0x7ff36ab5...", Some("0x1234")),
2207 "DEX Swap"
2208 );
2209 }
2210
2211 #[test]
2212 fn test_classify_tx_type_native_transfer() {
2213 assert_eq!(classify_tx_type("0x", Some("0x1234")), "Native Transfer");
2214 assert_eq!(classify_tx_type("", Some("0x1234")), "Native Transfer");
2215 }
2216
2217 #[test]
2218 fn test_classify_tx_type_unknown_contract_call() {
2219 assert_eq!(
2220 classify_tx_type("0xdeadbeef12345678", Some("0x1234")),
2221 "Contract Call"
2222 );
2223 }
2224
2225 #[test]
2230 fn test_format_tx_value_ethereum_wei() {
2231 let (fmt, high) = format_tx_value("1000000000000000000", "ethereum");
2232 assert!(fmt.contains("1.000000"));
2233 assert!(fmt.contains("ETH"));
2234 assert!(!high); }
2236
2237 #[test]
2238 fn test_format_tx_value_hex() {
2239 let (fmt, _) = format_tx_value("0xde0b6b3a7640000", "ethereum");
2240 assert!(fmt.contains("ETH"));
2242 }
2243
2244 #[test]
2245 fn test_format_tx_value_high_value() {
2246 let (_, high) = format_tx_value("100000000000000000000", "ethereum");
2248 assert!(high); }
2250
2251 #[test]
2252 fn test_format_tx_value_zero_decimal() {
2253 let (fmt, high) = format_tx_value("0", "ethereum");
2254 assert!(fmt.contains("0.000000"));
2255 assert!(!high);
2256 }
2257
2258 #[test]
2259 fn test_format_tx_value_solana_additional() {
2260 let (fmt, _) = format_tx_value("1000000000", "solana"); assert!(fmt.contains("SOL"));
2262 }
2263
2264 #[test]
2265 fn test_format_tx_value_tron_additional() {
2266 let (fmt, _) = format_tx_value("1000000", "tron"); assert!(fmt.contains("TRX"));
2268 }
2269
2270 #[test]
2271 fn test_format_tx_value_empty_hex_additional() {
2272 let (fmt, _) = format_tx_value("0x", "ethereum");
2273 assert!(fmt.contains("0.000000"));
2274 }
2275
2276 #[test]
2281 fn test_target_type_label_combined() {
2282 assert_eq!(
2283 target_type_label(&InferredTarget::Address {
2284 chain: "eth".to_string()
2285 }),
2286 "Address"
2287 );
2288 assert_eq!(
2289 target_type_label(&InferredTarget::Transaction {
2290 chain: "eth".to_string()
2291 }),
2292 "Transaction"
2293 );
2294 assert_eq!(
2295 target_type_label(&InferredTarget::Token {
2296 chain: "eth".to_string()
2297 }),
2298 "Token"
2299 );
2300 }
2301
2302 #[test]
2303 fn test_chain_label_combined() {
2304 assert_eq!(
2305 chain_label(&InferredTarget::Address {
2306 chain: "ethereum".to_string()
2307 }),
2308 "ethereum"
2309 );
2310 assert_eq!(
2311 chain_label(&InferredTarget::Transaction {
2312 chain: "polygon".to_string()
2313 }),
2314 "polygon"
2315 );
2316 assert_eq!(
2317 chain_label(&InferredTarget::Token {
2318 chain: "solana".to_string()
2319 }),
2320 "solana"
2321 );
2322 }
2323
2324 #[test]
2329 fn test_meta_analysis_address_contract_high_risk() {
2330 use scope::compliance::risk::RiskLevel;
2331 let meta = meta_analysis_address(
2332 true,
2333 Some(2_000_000.0),
2334 10,
2335 Some(8.0),
2336 Some(&RiskLevel::High),
2337 );
2338 assert!(meta.synthesis.contains("contract"));
2339 assert!(meta.synthesis.contains("Significant value"));
2340 assert!(meta.key_takeaway.contains("scrutiny"));
2341 assert!(!meta.recommendations.is_empty());
2342 }
2343
2344 #[test]
2345 fn test_meta_analysis_address_wallet_low_risk() {
2346 use scope::compliance::risk::RiskLevel;
2347 let meta = meta_analysis_address(false, Some(0.5), 0, Some(2.0), Some(&RiskLevel::Low));
2348 assert!(meta.synthesis.contains("wallet"));
2349 assert!(meta.synthesis.contains("Minimal value"));
2350 }
2351
2352 #[test]
2353 fn test_meta_analysis_address_no_risk_data() {
2354 let meta = meta_analysis_address(false, None, 0, None, None);
2355 assert!(!meta.synthesis.is_empty());
2356 assert!(meta.key_takeaway.contains("Review full report"));
2357 }
2358
2359 #[test]
2364 fn test_meta_analysis_tx_failed_additional() {
2365 let meta = meta_analysis_tx("Contract Call", false, false, "0x...", Some("0x..."));
2366 assert!(meta.synthesis.contains("failed"));
2367 assert!(meta.key_takeaway.contains("Failed"));
2368 }
2369
2370 #[test]
2371 fn test_meta_analysis_tx_high_value_native_additional() {
2372 let meta = meta_analysis_tx("Native Transfer", true, true, "0x...", Some("0x..."));
2373 assert!(meta.synthesis.contains("High-value"));
2374 assert!(meta.key_takeaway.contains("Large native transfer"));
2375 }
2376
2377 #[test]
2378 fn test_meta_analysis_tx_routine() {
2379 let meta = meta_analysis_tx("ERC-20 Transfer", true, false, "0x...", Some("0x..."));
2380 assert!(meta.key_takeaway.contains("Routine"));
2381 }
2382
2383 #[test]
2388 fn test_meta_analysis_token_high_risk_additional() {
2389 let risk = report::TokenRiskSummary {
2390 score: 8,
2391 level: "High",
2392 emoji: "🔴",
2393 concerns: vec!["Low liquidity".to_string()],
2394 positives: vec![],
2395 };
2396 let meta = meta_analysis_token(&risk, false, None, None, 10_000.0);
2397 assert!(meta.synthesis.contains("Elevated risk"));
2398 assert!(meta.key_takeaway.contains("High risk"));
2399 }
2400
2401 #[test]
2402 fn test_meta_analysis_token_stablecoin_peg_healthy() {
2403 let risk = report::TokenRiskSummary {
2404 score: 2,
2405 level: "Low",
2406 emoji: "🟢",
2407 concerns: vec![],
2408 positives: vec!["Strong liquidity".to_string()],
2409 };
2410 let meta = meta_analysis_token(&risk, true, Some(PegStatus::OnPeg), Some(5.0), 5_000_000.0);
2411 assert!(meta.synthesis.contains("peg is healthy"));
2412 assert!(meta.synthesis.contains("Strong liquidity"));
2413 }
2414
2415 #[test]
2416 fn test_meta_analysis_token_stablecoin_peg_unhealthy() {
2417 let risk = report::TokenRiskSummary {
2418 score: 5,
2419 level: "Medium",
2420 emoji: "🟡",
2421 concerns: vec!["Peg deviation".to_string()],
2422 positives: vec![],
2423 };
2424 let meta = meta_analysis_token(
2425 &risk,
2426 true,
2427 Some(PegStatus::Deviation),
2428 Some(40.0),
2429 100_000.0,
2430 );
2431 assert!(meta.synthesis.contains("peg deviation"));
2432 assert!(meta.synthesis.contains("Concentration risk"));
2433 }
2434
2435 #[test]
2436 fn test_meta_analysis_token_high_risk_empty_concerns_uses_multiple_factors() {
2437 let risk = report::TokenRiskSummary {
2438 score: 8,
2439 level: "High",
2440 emoji: "🔴",
2441 concerns: vec![],
2442 positives: vec![],
2443 };
2444 let meta = meta_analysis_token(&risk, false, None, None, 50_000.0);
2445 assert!(
2446 meta.key_takeaway.contains("multiple factors")
2447 || meta.key_takeaway.contains("High risk")
2448 );
2449 }
2450
2451 #[test]
2452 fn test_infer_target_chain_override_with_address() {
2453 let t = infer_target(
2454 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
2455 Some("arbitrum"),
2456 );
2457 assert!(matches!(t, InferredTarget::Address { chain } if chain == "arbitrum"));
2458 }
2459
2460 #[test]
2461 fn test_meta_analysis_tx_approval_contains_approval_in_match() {
2462 let meta = meta_analysis_tx("ERC-20 Approval", true, false, "0xfrom", Some("0xto"));
2463 assert!(
2464 meta.recommendations
2465 .iter()
2466 .any(|r| r.contains("spender") || r.contains("allowance"))
2467 );
2468 }
2469}