Skip to main content

scope/cli/
insights.rs

1//! # Insights Command
2//!
3//! Infers the type of blockchain target (address, token, transaction) from input,
4//! auto-detects chain, and runs relevant Scope analyses to produce unified insights.
5
6use crate::chains::{
7    ChainClientFactory, infer_chain_from_address, infer_chain_from_hash, native_symbol,
8};
9use crate::cli::address::{self, AddressArgs};
10use crate::cli::crawl::{Period, fetch_analytics_for_input};
11use crate::cli::tx::{fetch_transaction_report, format_tx_markdown};
12use crate::config::Config;
13use crate::display::report;
14use crate::error::Result;
15use crate::market::{HealthThresholds, MarketSummary, VenueRegistry};
16use crate::tokens::TokenAliases;
17use clap::Args;
18
19/// Target type inferred from user input.
20#[derive(Debug, Clone)]
21pub enum InferredTarget {
22    /// Blockchain address (EVM, Tron, or Solana).
23    Address { chain: String },
24    /// Transaction hash.
25    Transaction { chain: String },
26    /// Token symbol, name, or contract address.
27    Token { chain: String },
28}
29
30/// Arguments for the insights command.
31#[derive(Debug, Args)]
32pub struct InsightsArgs {
33    /// Target to analyze: address, transaction hash, or token (symbol/name/address).
34    ///
35    /// Scope infers the type and chain from format:
36    /// - `0x...` (42 chars) = EVM address → ethereum
37    /// - `T...` (34 chars) = Tron address → tron
38    /// - Base58 (32–44 chars) = Solana address → solana
39    /// - `0x...` (66 chars) = EVM tx hash
40    /// - 64 hex chars = Tron tx hash
41    /// - Base58 (80–90 chars) = Solana signature
42    /// - Otherwise = token symbol/name (e.g. USDC, WETH)
43    pub target: String,
44
45    /// Override detected chain (ethereum, polygon, solana, tron, etc.).
46    #[arg(short, long)]
47    pub chain: Option<String>,
48
49    /// Include decoded transaction input (for tx targets).
50    #[arg(long)]
51    pub decode: bool,
52
53    /// Include internal transaction trace (for tx targets).
54    #[arg(long)]
55    pub trace: bool,
56}
57
58/// Infers the target type and chain from the input string.
59pub fn infer_target(input: &str, chain_override: Option<&str>) -> InferredTarget {
60    let trimmed = input.trim();
61
62    if let Some(chain) = chain_override {
63        let chain = chain.to_lowercase();
64        // With override, we still need to infer type
65        if infer_chain_from_hash(trimmed).is_some() {
66            return InferredTarget::Transaction { chain };
67        }
68        if TokenAliases::is_address(trimmed) {
69            return InferredTarget::Address { chain };
70        }
71        return InferredTarget::Token { chain };
72    }
73
74    // Transaction hash (format implies chain)
75    if let Some(chain) = infer_chain_from_hash(trimmed) {
76        return InferredTarget::Transaction {
77            chain: chain.to_string(),
78        };
79    }
80
81    // Address (format implies chain)
82    if TokenAliases::is_address(trimmed) {
83        let chain = infer_chain_from_address(trimmed).unwrap_or("ethereum");
84        return InferredTarget::Address {
85            chain: chain.to_string(),
86        };
87    }
88
89    // Default: token (symbol or name)
90    InferredTarget::Token {
91        chain: "ethereum".to_string(),
92    }
93}
94
95/// Runs the insights command.
96pub async fn run(
97    mut args: InsightsArgs,
98    config: &Config,
99    clients: &dyn ChainClientFactory,
100) -> Result<()> {
101    // Resolve address book label → address + chain
102    if let Some((address, chain)) =
103        crate::cli::address_book::resolve_address_book_input(&args.target, config)?
104    {
105        args.target = address;
106        if args.chain.is_none() {
107            args.chain = Some(chain);
108        }
109    }
110
111    let chain_override = args.chain.as_deref();
112    let target = infer_target(&args.target, chain_override);
113
114    let sp = crate::cli::progress::Spinner::new(&format!(
115        "Analyzing {} on {}...",
116        target_type_label(&target),
117        chain_label(&target)
118    ));
119
120    let mut output = String::new();
121    output.push_str("# Scope Insights\n\n");
122    output.push_str(&format!("**Target:** `{}`\n\n", args.target));
123    output.push_str(&format!(
124        "**Detected:** {} on {}\n\n",
125        target_type_label(&target),
126        chain_label(&target)
127    ));
128    output.push_str("---\n\n");
129
130    match &target {
131        InferredTarget::Address { chain } => {
132            output.push_str("## Observations\n\n");
133            let addr_args = AddressArgs {
134                address: args.target.clone(),
135                chain: chain.clone(),
136                format: Some(crate::config::OutputFormat::Markdown),
137                include_txs: false,
138                include_tokens: true,
139                limit: 10,
140                report: None,
141                dossier: false,
142            };
143            let client = clients.create_chain_client(chain)?;
144            let report = address::analyze_address(&addr_args, client.as_ref()).await?;
145
146            // Contract vs EOA (EVM chains support get_code)
147            let code_result = client.get_code(&args.target).await;
148            let is_contract = code_result
149                .as_ref()
150                .is_ok_and(|c| !c.is_empty() && c != "0x");
151            if code_result.is_ok() {
152                output.push_str(&format!(
153                    "- **Type:** {}\n",
154                    if is_contract {
155                        "Contract"
156                    } else {
157                        "Externally Owned Account (EOA)"
158                    }
159                ));
160            }
161
162            output.push_str(&format!(
163                "- **Native balance:** {} ({})\n",
164                report.balance.formatted,
165                crate::chains::native_symbol(chain)
166            ));
167            if let Some(ref usd) = report.balance.usd {
168                output.push_str(&format!("- **USD value:** ${:.2}\n", usd));
169            }
170            output.push_str(&format!(
171                "- **Transaction count:** {}\n",
172                report.transaction_count
173            ));
174            if let Some(ref tokens) = report.tokens
175                && !tokens.is_empty()
176            {
177                output.push_str(&format!(
178                    "- **Token holdings:** {} different tokens\n",
179                    tokens.len()
180                ));
181                output.push_str("\n### Token Balances\n\n");
182                for tb in tokens.iter().take(10) {
183                    output.push_str(&format!(
184                        "- {}: {} ({})\n",
185                        tb.symbol, tb.formatted_balance, tb.contract_address
186                    ));
187                }
188                if tokens.len() > 10 {
189                    output.push_str(&format!("\n*...and {} more*\n", tokens.len() - 10));
190                }
191            }
192
193            // Risk assessment (compliance engine)
194            let risk_assessment =
195                match crate::compliance::datasource::BlockchainDataClient::from_env_opt() {
196                    Some(data_client) => {
197                        crate::compliance::risk::RiskEngine::with_data_client(data_client)
198                            .assess_address(&args.target, chain)
199                            .await
200                            .ok()
201                    }
202                    None => crate::compliance::risk::RiskEngine::new()
203                        .assess_address(&args.target, chain)
204                        .await
205                        .ok(),
206                };
207
208            if let Some(ref risk) = risk_assessment {
209                output.push_str(&format!(
210                    "\n- **Risk:** {} {:.1}/10 ({:?})\n",
211                    risk.risk_level.emoji(),
212                    risk.overall_score,
213                    risk.risk_level
214                ));
215            }
216
217            // Meta analysis
218            let meta = meta_analysis_address(
219                is_contract,
220                report.balance.usd,
221                report.tokens.as_ref().map(|t| t.len()).unwrap_or(0),
222                risk_assessment.as_ref().map(|r| r.overall_score),
223                risk_assessment.as_ref().map(|r| &r.risk_level),
224            );
225            output.push_str("\n### Synthesis\n\n");
226            output.push_str(&format!("{}\n\n", meta.synthesis));
227            output.push_str(&format!("**Key takeaway:** {}\n\n", meta.key_takeaway));
228            if !meta.recommendations.is_empty() {
229                output.push_str("**Consider:**\n");
230                for rec in &meta.recommendations {
231                    output.push_str(&format!("- {}\n", rec));
232                }
233            }
234            output.push_str("\n---\n\n");
235            let full_report = if let Some(ref risk) = risk_assessment {
236                crate::cli::address_report::generate_dossier_report(&report, risk)
237            } else {
238                crate::cli::address_report::generate_address_report(&report)
239            };
240            output.push_str(&full_report);
241        }
242        InferredTarget::Transaction { chain } => {
243            output.push_str("## Observations\n\n");
244            let tx_report =
245                fetch_transaction_report(&args.target, chain, args.decode, args.trace, clients)
246                    .await?;
247
248            let tx_type = classify_tx_type(
249                &tx_report.transaction.input,
250                tx_report.transaction.to.as_deref(),
251            );
252            output.push_str(&format!("- **Type:** {}\n", tx_type));
253
254            output.push_str(&format!(
255                "- **Status:** {}\n",
256                if tx_report.transaction.status {
257                    "Success"
258                } else {
259                    "Failed"
260                }
261            ));
262            output.push_str(&format!("- **From:** `{}`\n", tx_report.transaction.from));
263            output.push_str(&format!(
264                "- **To:** `{}`\n",
265                tx_report
266                    .transaction
267                    .to
268                    .as_deref()
269                    .unwrap_or("Contract Creation")
270            ));
271
272            let (formatted_value, high_value) =
273                format_tx_value(&tx_report.transaction.value, chain);
274            output.push_str(&format!("- **Value:** {}\n", formatted_value));
275            if high_value {
276                output.push_str("- ⚠️ **High-value transfer**\n");
277            }
278
279            output.push_str(&format!("- **Fee:** {}\n", tx_report.gas.transaction_fee));
280
281            // Meta analysis
282            let meta = meta_analysis_tx(
283                tx_type,
284                tx_report.transaction.status,
285                high_value,
286                &tx_report.transaction.from,
287                tx_report.transaction.to.as_deref(),
288            );
289            output.push_str("\n### Synthesis\n\n");
290            output.push_str(&format!("{}\n\n", meta.synthesis));
291            output.push_str(&format!("**Key takeaway:** {}\n\n", meta.key_takeaway));
292            if !meta.recommendations.is_empty() {
293                output.push_str("**Consider:**\n");
294                for rec in &meta.recommendations {
295                    output.push_str(&format!("- {}\n", rec));
296                }
297            }
298            output.push_str("\n---\n\n");
299            output.push_str(&format_tx_markdown(&tx_report));
300        }
301        InferredTarget::Token { chain } => {
302            output.push_str("## Observations\n\n");
303            let analytics = fetch_analytics_for_input(
304                &args.target,
305                chain,
306                Period::Hour24,
307                10,
308                clients,
309                Some(&sp),
310            )
311            .await?;
312
313            // Token risk summary (interpretive bullets)
314            let risk_summary = report::token_risk_summary(&analytics);
315            output.push_str(&format!(
316                "- **Risk:** {} {}/10 ({})\n",
317                risk_summary.emoji, risk_summary.score, risk_summary.level
318            ));
319            if !risk_summary.concerns.is_empty() {
320                for c in &risk_summary.concerns {
321                    output.push_str(&format!("- ⚠️ {}\n", c));
322                }
323            }
324            if !risk_summary.positives.is_empty() {
325                for p in &risk_summary.positives {
326                    output.push_str(&format!("- ✅ {}\n", p));
327                }
328            }
329
330            output.push_str(&format!(
331                "- **Token:** {} ({})\n",
332                analytics.token.symbol, analytics.token.name
333            ));
334            output.push_str(&format!(
335                "- **Address:** `{}`\n",
336                analytics.token.contract_address
337            ));
338            output.push_str(&format!("- **Price:** ${:.6}\n", analytics.price_usd));
339            output.push_str(&format!(
340                "- **Liquidity (24h):** ${}\n",
341                crate::display::format_usd(analytics.liquidity_usd)
342            ));
343            output.push_str(&format!(
344                "- **Volume (24h):** ${}\n",
345                crate::display::format_usd(analytics.volume_24h)
346            ));
347
348            // Top holder context
349            if let Some(top) = analytics.holders.first() {
350                output.push_str(&format!(
351                    "- **Top holder:** `{}` ({:.1}%)\n",
352                    top.address, top.percentage
353                ));
354                if top.percentage > 30.0 {
355                    output.push_str("  - ⚠️ High concentration risk\n");
356                }
357            }
358            output.push_str(&format!(
359                "- **Holders displayed:** {}\n",
360                analytics.holders.len()
361            ));
362
363            // Stablecoin: auto-include market/peg via venue registry
364            let mut peg_healthy: Option<bool> = None;
365            if is_stablecoin(&analytics.token.symbol)
366                && let Ok(registry) = VenueRegistry::load()
367            {
368                // Try binance first, fall back to any available CEX
369                let venue_id = if registry.contains("binance") {
370                    "binance"
371                } else {
372                    registry.list().first().copied().unwrap_or("binance")
373                };
374                if let Ok(exchange) = registry.create_exchange_client(venue_id) {
375                    let pair = exchange.format_pair(&analytics.token.symbol);
376                    if let Ok(book) = exchange.fetch_order_book(&pair).await {
377                        let thresholds = HealthThresholds {
378                            peg_target: 1.0,
379                            peg_range: 0.001,
380                            min_levels: 6,
381                            min_depth: 3000.0,
382                            min_bid_ask_ratio: 0.2,
383                            max_bid_ask_ratio: 5.0,
384                        };
385                        let volume_24h = if exchange.has_ticker() {
386                            exchange
387                                .fetch_ticker(&pair)
388                                .await
389                                .ok()
390                                .and_then(|t| t.quote_volume_24h.or(t.volume_24h))
391                        } else {
392                            None
393                        };
394                        let summary =
395                            MarketSummary::from_order_book(&book, 1.0, &thresholds, volume_24h);
396                        let deviation_bps = summary
397                            .mid_price
398                            .map(|m| (m - 1.0) * 10_000.0)
399                            .unwrap_or(0.0);
400                        peg_healthy = Some(deviation_bps.abs() < 10.0);
401                        let peg_status = if peg_healthy.unwrap_or(false) {
402                            "Peg healthy"
403                        } else if deviation_bps.abs() < 50.0 {
404                            "Slight peg deviation"
405                        } else {
406                            "Peg deviation"
407                        };
408                        output.push_str(&format!(
409                            "- **Market ({} {}):** {} (deviation: {:.1} bps)\n",
410                            exchange.venue_name(),
411                            pair,
412                            peg_status,
413                            deviation_bps
414                        ));
415                    }
416                }
417            }
418
419            // Meta analysis
420            let top_holder_pct = analytics.holders.first().map(|h| h.percentage);
421            let meta = meta_analysis_token(
422                &risk_summary,
423                is_stablecoin(&analytics.token.symbol),
424                peg_healthy,
425                top_holder_pct,
426                analytics.liquidity_usd,
427            );
428            output.push_str("\n### Synthesis\n\n");
429            output.push_str(&format!("{}\n\n", meta.synthesis));
430            output.push_str(&format!("**Key takeaway:** {}\n\n", meta.key_takeaway));
431            if !meta.recommendations.is_empty() {
432                output.push_str("**Consider:**\n");
433                for rec in &meta.recommendations {
434                    output.push_str(&format!("- {}\n", rec));
435                }
436            }
437            output.push_str("\n---\n\n");
438            output.push_str(&report::generate_report(&analytics));
439        }
440    }
441
442    sp.finish("Insights complete.");
443    println!("{}", output);
444    Ok(())
445}
446
447fn target_type_label(target: &InferredTarget) -> &'static str {
448    match target {
449        InferredTarget::Address { .. } => "Address",
450        InferredTarget::Transaction { .. } => "Transaction",
451        InferredTarget::Token { .. } => "Token",
452    }
453}
454
455fn chain_label(target: &InferredTarget) -> &str {
456    match target {
457        InferredTarget::Address { chain } => chain,
458        InferredTarget::Transaction { chain } => chain,
459        InferredTarget::Token { chain } => chain,
460    }
461}
462
463/// Classifies EVM transaction from input data selector.
464fn classify_tx_type(input: &str, to: Option<&str>) -> &'static str {
465    if to.is_none() {
466        return "Contract Creation";
467    }
468    let selector = input
469        .trim_start_matches("0x")
470        .chars()
471        .take(8)
472        .collect::<String>();
473    let sel = selector.to_lowercase();
474    match sel.as_str() {
475        "a9059cbb" => "ERC-20 Transfer",
476        "095ea7b3" => "ERC-20 Approve",
477        "23b872dd" => "ERC-20 Transfer From",
478        "38ed1739" | "5c11d795" | "4a25d94a" | "8803dbee" | "7ff36ab5" | "18cbafe5"
479        | "fb3bdb41" | "b6f9de95" => "DEX Swap",
480        "ac9650d8" | "5ae401dc" => "Multicall",
481        _ if input.is_empty() || input == "0x" => "Native Transfer",
482        _ => "Contract Call",
483    }
484}
485
486/// Formats raw value to human-readable (e.g. wei → ETH).
487fn format_tx_value(value_str: &str, chain: &str) -> (String, bool) {
488    let wei: u128 = if value_str.starts_with("0x") {
489        let hex_part = value_str.trim_start_matches("0x");
490        if hex_part.is_empty() {
491            0
492        } else {
493            u128::from_str_radix(hex_part, 16).unwrap_or(0)
494        }
495    } else {
496        value_str.parse().unwrap_or(0)
497    };
498    let decimals = match chain.to_lowercase().as_str() {
499        "ethereum" | "polygon" | "arbitrum" | "optimism" | "base" | "bsc" | "aegis" => 18,
500        "solana" => 9,
501        "tron" => 6,
502        _ => 18,
503    };
504    let divisor = 10_f64.powi(decimals);
505    let human = wei as f64 / divisor;
506    let symbol = native_symbol(chain);
507    let formatted = format!("≈ {:.6} {}", human, symbol);
508    // "High value" threshold: > 10 native units
509    let high_value = human > 10.0;
510    (formatted, high_value)
511}
512
513/// Common stablecoin symbols for auto-including market/peg analysis.
514fn is_stablecoin(symbol: &str) -> bool {
515    matches!(
516        symbol.to_uppercase().as_str(),
517        "USDC" | "USDT" | "DAI" | "BUSD" | "TUSD" | "USDP" | "FRAX" | "LUSD" | "PUSD" | "GUSD"
518    )
519}
520
521/// Meta-analysis: synthesizes observations into an executive summary, key takeaway, and recommendations.
522struct MetaAnalysis {
523    synthesis: String,
524    key_takeaway: String,
525    recommendations: Vec<String>,
526}
527
528fn meta_analysis_address(
529    is_contract: bool,
530    usd_value: Option<f64>,
531    token_count: usize,
532    risk_score: Option<f32>,
533    risk_level: Option<&crate::compliance::risk::RiskLevel>,
534) -> MetaAnalysis {
535    let mut synthesis_parts = Vec::new();
536    let profile = if is_contract {
537        "contract"
538    } else {
539        "wallet (EOA)"
540    };
541    synthesis_parts.push(format!("A {} on chain.", profile));
542
543    if let Some(usd) = usd_value {
544        if usd > 1_000_000.0 {
545            synthesis_parts.push("Significant value held.".to_string());
546        } else if usd > 10_000.0 {
547            synthesis_parts.push("Moderate value.".to_string());
548        } else if usd < 1.0 {
549            synthesis_parts.push("Minimal value.".to_string());
550        }
551    }
552
553    if token_count > 5 {
554        synthesis_parts.push("Diversified token exposure.".to_string());
555    } else if token_count == 1 && token_count > 0 {
556        synthesis_parts.push("Concentrated in a single token.".to_string());
557    }
558
559    if let (Some(score), Some(level)) = (risk_score, risk_level) {
560        if score >= 7.0 {
561            synthesis_parts.push(format!("Elevated risk ({:?}).", level));
562        } else if score <= 3.0 {
563            synthesis_parts.push("Low risk profile.".to_string());
564        }
565    }
566
567    let synthesis = if synthesis_parts.is_empty() {
568        "Address analyzed with available on-chain data.".to_string()
569    } else {
570        synthesis_parts.join(" ")
571    };
572
573    let key_takeaway = if let (Some(score), Some(level)) = (risk_score, risk_level) {
574        if score >= 7.0 {
575            format!(
576                "Risk assessment warrants closer scrutiny ({:.1}/10).",
577                score
578            )
579        } else {
580            format!("Overall risk: {:?} ({:.1}/10).", level, score)
581        }
582    } else if is_contract {
583        "Contract address — verify intended interaction before use.".to_string()
584    } else if usd_value.map(|u| u > 100_000.0).unwrap_or(false) {
585        "High-value wallet — standard due diligence applies.".to_string()
586    } else {
587        "Review full report for transaction and token details.".to_string()
588    };
589
590    let mut recommendations = Vec::new();
591    if risk_score.map(|s| s >= 6.0).unwrap_or(false) {
592        recommendations.push("Monitor for unusual transaction patterns.".to_string());
593    }
594    if token_count > 0 {
595        recommendations.push("Verify token contracts before large interactions.".to_string());
596    }
597    if is_contract {
598        recommendations.push("Confirm contract source and audit status.".to_string());
599    }
600
601    MetaAnalysis {
602        synthesis,
603        key_takeaway,
604        recommendations,
605    }
606}
607
608fn meta_analysis_tx(
609    tx_type: &str,
610    status: bool,
611    high_value: bool,
612    _from: &str,
613    _to: Option<&str>,
614) -> MetaAnalysis {
615    let mut synthesis_parts = Vec::new();
616
617    if !status {
618        synthesis_parts.push("Transaction failed.".to_string());
619    }
620
621    synthesis_parts.push(format!("{} between parties.", tx_type));
622
623    if high_value {
624        synthesis_parts.push("High-value transfer.".to_string());
625    }
626
627    let synthesis = synthesis_parts.join(" ");
628
629    let key_takeaway = if !status {
630        "Failed transaction — check revert reason and contract state.".to_string()
631    } else if high_value && tx_type == "Native Transfer" {
632        "Large native transfer — verify recipient and intent.".to_string()
633    } else if high_value {
634        "High-value operation — standard verification recommended.".to_string()
635    } else {
636        format!("Routine {} — review full details if needed.", tx_type)
637    };
638
639    let mut recommendations = Vec::new();
640    if !status {
641        recommendations.push("Inspect contract logs for revert reason.".to_string());
642    }
643    if high_value {
644        recommendations.push("Confirm recipient address and amount.".to_string());
645    }
646    if tx_type.contains("Approval") {
647        recommendations.push("Verify approved spender and allowance amount.".to_string());
648    }
649
650    MetaAnalysis {
651        synthesis,
652        key_takeaway,
653        recommendations,
654    }
655}
656
657fn meta_analysis_token(
658    risk_summary: &report::TokenRiskSummary,
659    is_stablecoin: bool,
660    peg_healthy: Option<bool>,
661    top_holder_pct: Option<f64>,
662    liquidity_usd: f64,
663) -> MetaAnalysis {
664    let mut synthesis_parts = Vec::new();
665
666    if risk_summary.score <= 3 {
667        synthesis_parts.push("Low-risk token with healthy metrics.".to_string());
668    } else if risk_summary.score >= 7 {
669        synthesis_parts.push("Elevated risk — multiple concerns identified.".to_string());
670    } else {
671        synthesis_parts.push("Moderate risk — mixed signals.".to_string());
672    }
673
674    if is_stablecoin && let Some(healthy) = peg_healthy {
675        if healthy {
676            synthesis_parts.push("Stablecoin peg is healthy on observed venue.".to_string());
677        } else {
678            synthesis_parts
679                .push("Stablecoin peg deviation detected — verify on multiple venues.".to_string());
680        }
681    }
682
683    if top_holder_pct.map(|p| p > 30.0).unwrap_or(false) {
684        synthesis_parts.push("Concentration risk: top holder holds significant share.".to_string());
685    }
686
687    if liquidity_usd > 1_000_000.0 {
688        synthesis_parts.push("Strong liquidity depth.".to_string());
689    } else if liquidity_usd < 50_000.0 {
690        synthesis_parts.push("Limited liquidity — slippage risk for larger trades.".to_string());
691    }
692
693    let synthesis = synthesis_parts.join(" ");
694
695    let key_takeaway = if risk_summary.score >= 7 {
696        format!(
697            "High risk ({}): {} — exercise caution.",
698            risk_summary.score,
699            risk_summary
700                .concerns
701                .first()
702                .cloned()
703                .unwrap_or_else(|| "multiple factors".to_string())
704        )
705    } else if is_stablecoin && peg_healthy == Some(false) {
706        "Stablecoin deviating from peg — check additional venues before trading.".to_string()
707    } else if !risk_summary.positives.is_empty() && risk_summary.concerns.is_empty() {
708        "Favorable risk profile — standard diligence applies.".to_string()
709    } else {
710        format!(
711            "Risk {}/10 ({}) — weigh concerns against use case.",
712            risk_summary.score, risk_summary.level
713        )
714    };
715
716    let mut recommendations = Vec::new();
717    if risk_summary.score >= 6 {
718        recommendations
719            .push("Consider smaller position sizes or avoid until risk clears.".to_string());
720    }
721    if top_holder_pct.map(|p| p > 25.0).unwrap_or(false) {
722        recommendations.push("Monitor top holder movements for distribution changes.".to_string());
723    }
724    if is_stablecoin && peg_healthy != Some(true) {
725        recommendations.push("Verify peg across multiple DEX/CEX venues.".to_string());
726    }
727    if liquidity_usd < 100_000.0 && risk_summary.score <= 5 {
728        recommendations.push("Use limit orders or split trades to manage slippage.".to_string());
729    }
730
731    MetaAnalysis {
732        synthesis,
733        key_takeaway,
734        recommendations,
735    }
736}
737
738#[cfg(test)]
739mod tests {
740    use super::*;
741    use crate::chains::{
742        Balance as ChainBalance, ChainClient, ChainClientFactory, DexDataSource,
743        Token as ChainToken, TokenBalance as ChainTokenBalance, Transaction as ChainTransaction,
744    };
745    use async_trait::async_trait;
746
747    // ====================================================================
748    // Mock Chain Client for testing run() paths
749    // ====================================================================
750
751    struct MockChainClient;
752
753    #[async_trait]
754    impl ChainClient for MockChainClient {
755        fn chain_name(&self) -> &str {
756            "ethereum"
757        }
758        fn native_token_symbol(&self) -> &str {
759            "ETH"
760        }
761        async fn get_balance(&self, _address: &str) -> crate::error::Result<ChainBalance> {
762            Ok(ChainBalance {
763                raw: "1000000000000000000".to_string(),
764                formatted: "1.0 ETH".to_string(),
765                decimals: 18,
766                symbol: "ETH".to_string(),
767                usd_value: Some(2500.0),
768            })
769        }
770        async fn enrich_balance_usd(&self, balance: &mut ChainBalance) {
771            balance.usd_value = Some(2500.0);
772        }
773        async fn get_transaction(&self, _hash: &str) -> crate::error::Result<ChainTransaction> {
774            Ok(ChainTransaction {
775                hash: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
776                    .to_string(),
777                block_number: Some(12345678),
778                timestamp: Some(1700000000),
779                from: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
780                to: Some("0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string()),
781                value: "1000000000000000000".to_string(),
782                gas_limit: 21000,
783                gas_used: Some(21000),
784                gas_price: "20000000000".to_string(),
785                nonce: 42,
786                input: "0xa9059cbb0000000000000000000000001234".to_string(),
787                status: Some(true),
788            })
789        }
790        async fn get_transactions(
791            &self,
792            _address: &str,
793            _limit: u32,
794        ) -> crate::error::Result<Vec<ChainTransaction>> {
795            Ok(vec![])
796        }
797        async fn get_block_number(&self) -> crate::error::Result<u64> {
798            Ok(12345678)
799        }
800        async fn get_token_balances(
801            &self,
802            _address: &str,
803        ) -> crate::error::Result<Vec<ChainTokenBalance>> {
804            Ok(vec![
805                ChainTokenBalance {
806                    token: ChainToken {
807                        contract_address: "0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string(),
808                        symbol: "USDT".to_string(),
809                        name: "Tether USD".to_string(),
810                        decimals: 6,
811                    },
812                    balance: "1000000".to_string(),
813                    formatted_balance: "1.0".to_string(),
814                    usd_value: Some(1.0),
815                },
816                ChainTokenBalance {
817                    token: ChainToken {
818                        contract_address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string(),
819                        symbol: "USDC".to_string(),
820                        name: "USD Coin".to_string(),
821                        decimals: 6,
822                    },
823                    balance: "5000000".to_string(),
824                    formatted_balance: "5.0".to_string(),
825                    usd_value: Some(5.0),
826                },
827            ])
828        }
829        async fn get_code(&self, _address: &str) -> crate::error::Result<String> {
830            Ok("0x".to_string()) // EOA
831        }
832    }
833
834    struct MockFactory;
835
836    impl ChainClientFactory for MockFactory {
837        fn create_chain_client(&self, _chain: &str) -> crate::error::Result<Box<dyn ChainClient>> {
838            Ok(Box::new(MockChainClient))
839        }
840        fn create_dex_client(&self) -> Box<dyn DexDataSource> {
841            crate::chains::DefaultClientFactory {
842                chains_config: Default::default(),
843            }
844            .create_dex_client()
845        }
846    }
847
848    // Mock that returns a contract address
849    struct MockContractClient;
850
851    #[async_trait]
852    impl ChainClient for MockContractClient {
853        fn chain_name(&self) -> &str {
854            "ethereum"
855        }
856        fn native_token_symbol(&self) -> &str {
857            "ETH"
858        }
859        async fn get_balance(&self, _address: &str) -> crate::error::Result<ChainBalance> {
860            Ok(ChainBalance {
861                raw: "0".to_string(),
862                formatted: "0.0 ETH".to_string(),
863                decimals: 18,
864                symbol: "ETH".to_string(),
865                usd_value: Some(0.0),
866            })
867        }
868        async fn enrich_balance_usd(&self, _balance: &mut ChainBalance) {}
869        async fn get_transaction(&self, hash: &str) -> crate::error::Result<ChainTransaction> {
870            Ok(ChainTransaction {
871                hash: hash.to_string(),
872                block_number: Some(100),
873                timestamp: Some(1700000000),
874                from: "0xfrom".to_string(),
875                to: None, // contract creation
876                value: "0".to_string(),
877                gas_limit: 100000,
878                gas_used: Some(80000),
879                gas_price: "10000000000".to_string(),
880                nonce: 0,
881                input: "0x60806040".to_string(),
882                status: Some(false), // failed tx
883            })
884        }
885        async fn get_transactions(
886            &self,
887            _address: &str,
888            _limit: u32,
889        ) -> crate::error::Result<Vec<ChainTransaction>> {
890            Ok(vec![])
891        }
892        async fn get_block_number(&self) -> crate::error::Result<u64> {
893            Ok(100)
894        }
895        async fn get_token_balances(
896            &self,
897            _address: &str,
898        ) -> crate::error::Result<Vec<ChainTokenBalance>> {
899            Ok(vec![])
900        }
901        async fn get_code(&self, _address: &str) -> crate::error::Result<String> {
902            Ok("0x6080604052".to_string()) // contract
903        }
904    }
905
906    struct MockContractFactory;
907
908    impl ChainClientFactory for MockContractFactory {
909        fn create_chain_client(&self, _chain: &str) -> crate::error::Result<Box<dyn ChainClient>> {
910            Ok(Box::new(MockContractClient))
911        }
912        fn create_dex_client(&self) -> Box<dyn DexDataSource> {
913            crate::chains::DefaultClientFactory {
914                chains_config: Default::default(),
915            }
916            .create_dex_client()
917        }
918    }
919
920    // Mock DexDataSource for token tests
921    struct MockDexDataSource;
922
923    #[async_trait]
924    impl DexDataSource for MockDexDataSource {
925        async fn get_token_price(&self, _chain: &str, _address: &str) -> Option<f64> {
926            Some(1.0)
927        }
928
929        async fn get_native_token_price(&self, _chain: &str) -> Option<f64> {
930            Some(2500.0)
931        }
932
933        async fn get_token_data(
934            &self,
935            _chain: &str,
936            address: &str,
937        ) -> crate::error::Result<crate::chains::dex::DexTokenData> {
938            use crate::chains::{DexPair, PricePoint, VolumePoint};
939            Ok(crate::chains::dex::DexTokenData {
940                address: address.to_string(),
941                symbol: "TEST".to_string(),
942                name: "Test Token".to_string(),
943                price_usd: 1.5,
944                price_change_24h: 5.2,
945                price_change_6h: 2.1,
946                price_change_1h: 0.5,
947                price_change_5m: 0.1,
948                volume_24h: 1_000_000.0,
949                volume_6h: 250_000.0,
950                volume_1h: 50_000.0,
951                liquidity_usd: 500_000.0,
952                market_cap: Some(10_000_000.0),
953                fdv: Some(12_000_000.0),
954                pairs: vec![DexPair {
955                    dex_name: "Uniswap V3".to_string(),
956                    pair_address: "0xpair123".to_string(),
957                    base_token: "TEST".to_string(),
958                    quote_token: "USDC".to_string(),
959                    price_usd: 1.5,
960                    liquidity_usd: 500_000.0,
961                    volume_24h: 1_000_000.0,
962                    price_change_24h: 5.2,
963                    buys_24h: 100,
964                    sells_24h: 80,
965                    buys_6h: 20,
966                    sells_6h: 15,
967                    buys_1h: 5,
968                    sells_1h: 3,
969                    pair_created_at: Some(1690000000),
970                    url: Some("https://dexscreener.com/ethereum/0xpair123".to_string()),
971                }],
972                price_history: vec![PricePoint {
973                    timestamp: 1690000000,
974                    price: 1.5,
975                }],
976                volume_history: vec![VolumePoint {
977                    timestamp: 1690000000,
978                    volume: 1_000_000.0,
979                }],
980                total_buys_24h: 100,
981                total_sells_24h: 80,
982                total_buys_6h: 20,
983                total_sells_6h: 15,
984                total_buys_1h: 5,
985                total_sells_1h: 3,
986                earliest_pair_created_at: Some(1690000000),
987                image_url: None,
988                websites: Vec::new(),
989                socials: Vec::new(),
990                dexscreener_url: Some("https://dexscreener.com/ethereum/test".to_string()),
991            })
992        }
993
994        async fn search_tokens(
995            &self,
996            _query: &str,
997            _chain: Option<&str>,
998        ) -> crate::error::Result<Vec<crate::chains::TokenSearchResult>> {
999            Ok(vec![crate::chains::TokenSearchResult {
1000                address: "0xTEST1234567890123456789012345678901234567".to_string(),
1001                symbol: "TEST".to_string(),
1002                name: "Test Token".to_string(),
1003                chain: "ethereum".to_string(),
1004                price_usd: Some(1.5),
1005                volume_24h: 1_000_000.0,
1006                liquidity_usd: 500_000.0,
1007                market_cap: Some(10_000_000.0),
1008            }])
1009        }
1010    }
1011
1012    // Mock ChainClient that returns holders with high concentration
1013    struct MockTokenChainClient;
1014
1015    #[async_trait]
1016    impl ChainClient for MockTokenChainClient {
1017        fn chain_name(&self) -> &str {
1018            "ethereum"
1019        }
1020        fn native_token_symbol(&self) -> &str {
1021            "ETH"
1022        }
1023        async fn get_balance(&self, _address: &str) -> crate::error::Result<ChainBalance> {
1024            Ok(ChainBalance {
1025                raw: "1000000000000000000".to_string(),
1026                formatted: "1.0 ETH".to_string(),
1027                decimals: 18,
1028                symbol: "ETH".to_string(),
1029                usd_value: Some(2500.0),
1030            })
1031        }
1032        async fn enrich_balance_usd(&self, balance: &mut ChainBalance) {
1033            balance.usd_value = Some(2500.0);
1034        }
1035        async fn get_transaction(&self, _hash: &str) -> crate::error::Result<ChainTransaction> {
1036            Ok(ChainTransaction {
1037                hash: "0xabc123".to_string(),
1038                block_number: Some(12345678),
1039                timestamp: Some(1700000000),
1040                from: "0xfrom".to_string(),
1041                to: Some("0xto".to_string()),
1042                value: "0".to_string(),
1043                gas_limit: 21000,
1044                gas_used: Some(21000),
1045                gas_price: "20000000000".to_string(),
1046                nonce: 42,
1047                input: "0x".to_string(),
1048                status: Some(true),
1049            })
1050        }
1051        async fn get_transactions(
1052            &self,
1053            _address: &str,
1054            _limit: u32,
1055        ) -> crate::error::Result<Vec<ChainTransaction>> {
1056            Ok(vec![])
1057        }
1058        async fn get_block_number(&self) -> crate::error::Result<u64> {
1059            Ok(12345678)
1060        }
1061        async fn get_token_balances(
1062            &self,
1063            _address: &str,
1064        ) -> crate::error::Result<Vec<ChainTokenBalance>> {
1065            Ok(vec![])
1066        }
1067        async fn get_code(&self, _address: &str) -> crate::error::Result<String> {
1068            Ok("0x".to_string())
1069        }
1070        async fn get_token_holders(
1071            &self,
1072            _address: &str,
1073            _limit: u32,
1074        ) -> crate::error::Result<Vec<crate::chains::TokenHolder>> {
1075            // Return holders with high concentration (>30%) to trigger warning
1076            Ok(vec![
1077                crate::chains::TokenHolder {
1078                    address: "0x1111111111111111111111111111111111111111".to_string(),
1079                    balance: "3500000000000000000000000".to_string(),
1080                    formatted_balance: "3500000.0".to_string(),
1081                    percentage: 35.0, // >30% triggers concentration warning
1082                    rank: 1,
1083                },
1084                crate::chains::TokenHolder {
1085                    address: "0x2222222222222222222222222222222222222222".to_string(),
1086                    balance: "1500000000000000000000000".to_string(),
1087                    formatted_balance: "1500000.0".to_string(),
1088                    percentage: 15.0,
1089                    rank: 2,
1090                },
1091                crate::chains::TokenHolder {
1092                    address: "0x3333333333333333333333333333333333333333".to_string(),
1093                    balance: "1000000000000000000000000".to_string(),
1094                    formatted_balance: "1000000.0".to_string(),
1095                    percentage: 10.0,
1096                    rank: 3,
1097                },
1098            ])
1099        }
1100    }
1101
1102    // Factory for token tests with mocks
1103    struct MockTokenFactory;
1104
1105    impl ChainClientFactory for MockTokenFactory {
1106        fn create_chain_client(&self, _chain: &str) -> crate::error::Result<Box<dyn ChainClient>> {
1107            Ok(Box::new(MockTokenChainClient))
1108        }
1109        fn create_dex_client(&self) -> Box<dyn DexDataSource> {
1110            Box::new(MockDexDataSource)
1111        }
1112    }
1113
1114    // ====================================================================
1115    // run() function tests with mocks
1116    // ====================================================================
1117
1118    #[tokio::test]
1119    async fn test_run_address_eoa() {
1120        let config = Config::default();
1121        let factory = MockFactory;
1122        let args = InsightsArgs {
1123            target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
1124            chain: None,
1125            decode: false,
1126            trace: false,
1127        };
1128        let result = run(args, &config, &factory).await;
1129        assert!(result.is_ok());
1130    }
1131
1132    #[tokio::test]
1133    async fn test_run_address_contract() {
1134        let config = Config::default();
1135        let factory = MockContractFactory;
1136        let args = InsightsArgs {
1137            target: "0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string(),
1138            chain: None,
1139            decode: false,
1140            trace: false,
1141        };
1142        let result = run(args, &config, &factory).await;
1143        assert!(result.is_ok());
1144    }
1145
1146    #[tokio::test]
1147    async fn test_run_transaction() {
1148        let config = Config::default();
1149        let factory = MockFactory;
1150        let args = InsightsArgs {
1151            target: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
1152                .to_string(),
1153            chain: None,
1154            decode: false,
1155            trace: false,
1156        };
1157        let result = run(args, &config, &factory).await;
1158        assert!(result.is_ok());
1159    }
1160
1161    #[tokio::test]
1162    async fn test_run_transaction_failed() {
1163        let config = Config::default();
1164        let factory = MockContractFactory;
1165        let args = InsightsArgs {
1166            target: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
1167                .to_string(),
1168            chain: Some("ethereum".to_string()),
1169            decode: true,
1170            trace: false,
1171        };
1172        let result = run(args, &config, &factory).await;
1173        assert!(result.is_ok());
1174    }
1175
1176    #[tokio::test]
1177    async fn test_run_address_with_chain_override() {
1178        let config = Config::default();
1179        let factory = MockFactory;
1180        let args = InsightsArgs {
1181            target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
1182            chain: Some("polygon".to_string()),
1183            decode: false,
1184            trace: false,
1185        };
1186        let result = run(args, &config, &factory).await;
1187        assert!(result.is_ok());
1188    }
1189
1190    #[tokio::test]
1191    async fn test_insights_run_token() {
1192        let config = Config::default();
1193        let factory = MockTokenFactory;
1194        let args = InsightsArgs {
1195            target: "TEST".to_string(),
1196            chain: Some("ethereum".to_string()),
1197            decode: false,
1198            trace: false,
1199        };
1200        let result = run(args, &config, &factory).await;
1201        assert!(result.is_ok());
1202    }
1203
1204    #[tokio::test]
1205    async fn test_insights_run_token_with_concentration_warning() {
1206        let config = Config::default();
1207        let factory = MockTokenFactory;
1208        let args = InsightsArgs {
1209            target: "0xTEST1234567890123456789012345678901234567".to_string(),
1210            chain: Some("ethereum".to_string()),
1211            decode: false,
1212            trace: false,
1213        };
1214        let result = run(args, &config, &factory).await;
1215        assert!(result.is_ok());
1216    }
1217
1218    // ====================================================================
1219    // Existing tests below
1220    // ====================================================================
1221
1222    #[test]
1223    fn test_infer_target_evm_address() {
1224        let t = infer_target("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", None);
1225        assert!(matches!(t, InferredTarget::Address { chain } if chain == "ethereum"));
1226    }
1227
1228    #[test]
1229    fn test_infer_target_tron_address() {
1230        let t = infer_target("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", None);
1231        assert!(matches!(t, InferredTarget::Address { chain } if chain == "tron"));
1232    }
1233
1234    #[test]
1235    fn test_infer_target_solana_address() {
1236        let t = infer_target("DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy", None);
1237        assert!(matches!(t, InferredTarget::Address { chain } if chain == "solana"));
1238    }
1239
1240    #[test]
1241    fn test_infer_target_evm_tx_hash() {
1242        let t = infer_target(
1243            "0xabc123def456789012345678901234567890123456789012345678901234abcd",
1244            None,
1245        );
1246        assert!(matches!(t, InferredTarget::Transaction { chain } if chain == "ethereum"));
1247    }
1248
1249    #[test]
1250    fn test_infer_target_tron_tx_hash() {
1251        let t = infer_target(
1252            "abc123def456789012345678901234567890123456789012345678901234abcd",
1253            None,
1254        );
1255        assert!(matches!(t, InferredTarget::Transaction { chain } if chain == "tron"));
1256    }
1257
1258    #[test]
1259    fn test_infer_target_token_symbol() {
1260        let t = infer_target("USDC", None);
1261        assert!(matches!(t, InferredTarget::Token { chain } if chain == "ethereum"));
1262    }
1263
1264    #[test]
1265    fn test_infer_target_chain_override() {
1266        let t = infer_target(
1267            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
1268            Some("polygon"),
1269        );
1270        assert!(matches!(t, InferredTarget::Address { chain } if chain == "polygon"));
1271    }
1272
1273    #[test]
1274    fn test_infer_target_token_with_chain_override() {
1275        let t = infer_target("USDC", Some("solana"));
1276        assert!(matches!(t, InferredTarget::Token { chain } if chain == "solana"));
1277    }
1278
1279    #[test]
1280    fn test_classify_tx_type() {
1281        assert_eq!(
1282            classify_tx_type("0xa9059cbb1234...", Some("0xto")),
1283            "ERC-20 Transfer"
1284        );
1285        assert_eq!(
1286            classify_tx_type("0x095ea7b3abcd...", Some("0xto")),
1287            "ERC-20 Approve"
1288        );
1289        assert_eq!(classify_tx_type("0x", Some("0xto")), "Native Transfer");
1290        assert_eq!(classify_tx_type("", None), "Contract Creation");
1291    }
1292
1293    #[test]
1294    fn test_format_tx_value() {
1295        let (fmt, high) = format_tx_value("0xDE0B6B3A7640000", "ethereum"); // 1 ETH
1296        assert!(fmt.contains("1.0") && fmt.contains("ETH"));
1297        assert!(!high);
1298        let (_, high2) = format_tx_value("0x52B7D2DCC80CD2E4000000", "ethereum"); // 100 ETH
1299        assert!(high2);
1300    }
1301
1302    #[test]
1303    fn test_is_stablecoin() {
1304        assert!(is_stablecoin("USDC"));
1305        assert!(is_stablecoin("usdt"));
1306        assert!(is_stablecoin("DAI"));
1307        assert!(is_stablecoin("BUSD"));
1308        assert!(is_stablecoin("TUSD"));
1309        assert!(is_stablecoin("USDP"));
1310        assert!(is_stablecoin("FRAX"));
1311        assert!(is_stablecoin("LUSD"));
1312        assert!(is_stablecoin("PUSD"));
1313        assert!(is_stablecoin("GUSD"));
1314        assert!(!is_stablecoin("ETH"));
1315        assert!(!is_stablecoin("PEPE"));
1316        assert!(!is_stablecoin("WBTC"));
1317    }
1318
1319    #[test]
1320    fn test_is_stablecoin_empty_string() {
1321        assert!(!is_stablecoin(""));
1322    }
1323
1324    #[test]
1325    fn test_is_stablecoin_case_insensitive() {
1326        // to_uppercase() makes comparison case-insensitive
1327        assert!(is_stablecoin("UsDc"));
1328        assert!(is_stablecoin("FraX"));
1329        assert!(!is_stablecoin("SOL")); // SOL is not a stablecoin
1330    }
1331
1332    // ====================================================================
1333    // target_type_label and chain_label tests
1334    // ====================================================================
1335
1336    #[test]
1337    fn test_target_type_label_address() {
1338        let t = InferredTarget::Address {
1339            chain: "ethereum".to_string(),
1340        };
1341        assert_eq!(target_type_label(&t), "Address");
1342    }
1343
1344    #[test]
1345    fn test_target_type_label_transaction() {
1346        let t = InferredTarget::Transaction {
1347            chain: "ethereum".to_string(),
1348        };
1349        assert_eq!(target_type_label(&t), "Transaction");
1350    }
1351
1352    #[test]
1353    fn test_target_type_label_token() {
1354        let t = InferredTarget::Token {
1355            chain: "ethereum".to_string(),
1356        };
1357        assert_eq!(target_type_label(&t), "Token");
1358    }
1359
1360    #[test]
1361    fn test_chain_label_address() {
1362        let t = InferredTarget::Address {
1363            chain: "polygon".to_string(),
1364        };
1365        assert_eq!(chain_label(&t), "polygon");
1366    }
1367
1368    #[test]
1369    fn test_chain_label_transaction() {
1370        let t = InferredTarget::Transaction {
1371            chain: "tron".to_string(),
1372        };
1373        assert_eq!(chain_label(&t), "tron");
1374    }
1375
1376    #[test]
1377    fn test_chain_label_token() {
1378        let t = InferredTarget::Token {
1379            chain: "solana".to_string(),
1380        };
1381        assert_eq!(chain_label(&t), "solana");
1382    }
1383
1384    // ====================================================================
1385    // classify_tx_type — expanded edge cases
1386    // ====================================================================
1387
1388    #[test]
1389    fn test_classify_tx_type_dex_swaps() {
1390        assert_eq!(
1391            classify_tx_type("0x38ed173900000...", Some("0xrouter")),
1392            "DEX Swap"
1393        );
1394        assert_eq!(
1395            classify_tx_type("0x5c11d79500000...", Some("0xrouter")),
1396            "DEX Swap"
1397        );
1398        assert_eq!(
1399            classify_tx_type("0x4a25d94a00000...", Some("0xrouter")),
1400            "DEX Swap"
1401        );
1402        assert_eq!(
1403            classify_tx_type("0x8803dbee00000...", Some("0xrouter")),
1404            "DEX Swap"
1405        );
1406        assert_eq!(
1407            classify_tx_type("0x7ff36ab500000...", Some("0xrouter")),
1408            "DEX Swap"
1409        );
1410        assert_eq!(
1411            classify_tx_type("0x18cbafe500000...", Some("0xrouter")),
1412            "DEX Swap"
1413        );
1414        assert_eq!(
1415            classify_tx_type("0xfb3bdb4100000...", Some("0xrouter")),
1416            "DEX Swap"
1417        );
1418        assert_eq!(
1419            classify_tx_type("0xb6f9de9500000...", Some("0xrouter")),
1420            "DEX Swap"
1421        );
1422    }
1423
1424    #[test]
1425    fn test_classify_tx_type_multicall() {
1426        assert_eq!(
1427            classify_tx_type("0xac9650d800000...", Some("0xcontract")),
1428            "Multicall"
1429        );
1430        assert_eq!(
1431            classify_tx_type("0x5ae401dc00000...", Some("0xcontract")),
1432            "Multicall"
1433        );
1434    }
1435
1436    #[test]
1437    fn test_classify_tx_type_transfer_from() {
1438        assert_eq!(
1439            classify_tx_type("0x23b872dd00000...", Some("0xtoken")),
1440            "ERC-20 Transfer From"
1441        );
1442    }
1443
1444    #[test]
1445    fn test_classify_tx_type_contract_call() {
1446        assert_eq!(
1447            classify_tx_type("0xdeadbeef00000...", Some("0xcontract")),
1448            "Contract Call"
1449        );
1450    }
1451
1452    #[test]
1453    fn test_classify_tx_type_native_transfer_empty() {
1454        assert_eq!(classify_tx_type("", Some("0xrecipient")), "Native Transfer");
1455    }
1456
1457    // ====================================================================
1458    // format_tx_value — expanded edge cases
1459    // ====================================================================
1460
1461    #[test]
1462    fn test_format_tx_value_zero() {
1463        let (fmt, high) = format_tx_value("0x0", "ethereum");
1464        assert!(fmt.contains("0.000000"));
1465        assert!(fmt.contains("ETH"));
1466        assert!(!high);
1467    }
1468
1469    #[test]
1470    fn test_format_tx_value_empty_hex() {
1471        let (fmt, high) = format_tx_value("0x", "ethereum");
1472        assert!(fmt.contains("0.000000"));
1473        assert!(!high);
1474    }
1475
1476    #[test]
1477    fn test_format_tx_value_decimal_string() {
1478        let (fmt, high) = format_tx_value("1000000000000000000", "ethereum"); // 1 ETH
1479        assert!(fmt.contains("1.0"));
1480        assert!(fmt.contains("ETH"));
1481        assert!(!high);
1482    }
1483
1484    #[test]
1485    fn test_format_tx_value_solana() {
1486        let (fmt, high) = format_tx_value("1000000000", "solana"); // 1 SOL (9 decimals)
1487        assert!(fmt.contains("1.0"));
1488        assert!(fmt.contains("SOL"));
1489        assert!(!high);
1490    }
1491
1492    #[test]
1493    fn test_format_tx_value_tron() {
1494        let (fmt, high) = format_tx_value("1000000", "tron"); // 1 TRX (6 decimals)
1495        assert!(fmt.contains("1.0"));
1496        assert!(fmt.contains("TRX"));
1497        assert!(!high);
1498    }
1499
1500    #[test]
1501    fn test_format_tx_value_polygon() {
1502        let (fmt, _) = format_tx_value("1000000000000000000", "polygon");
1503        assert!(fmt.contains("MATIC") || fmt.contains("POL"));
1504    }
1505
1506    #[test]
1507    fn test_format_tx_value_bsc() {
1508        let (fmt, _) = format_tx_value("1000000000000000000", "bsc");
1509        assert!(fmt.contains("BNB"));
1510    }
1511
1512    #[test]
1513    fn test_format_tx_value_high_value_threshold() {
1514        // > 10 native units = high value
1515        let (_, high) = format_tx_value("11000000000000000000", "ethereum"); // 11 ETH
1516        assert!(high);
1517        let (_, high2) = format_tx_value("10000000000000000000", "ethereum"); // 10 ETH
1518        assert!(!high2); // exactly 10 is not > 10
1519    }
1520
1521    // ====================================================================
1522    // meta_analysis_address tests
1523    // ====================================================================
1524
1525    #[test]
1526    fn test_meta_analysis_address_contract_high_value() {
1527        let meta = meta_analysis_address(true, Some(2_000_000.0), 10, None, None);
1528        assert!(meta.synthesis.contains("contract"));
1529        assert!(meta.synthesis.contains("Significant value"));
1530        assert!(meta.synthesis.contains("Diversified"));
1531        assert!(meta.recommendations.iter().any(|r| r.contains("contract")));
1532    }
1533
1534    #[test]
1535    fn test_meta_analysis_address_eoa_moderate_value() {
1536        let meta = meta_analysis_address(false, Some(50_000.0), 3, None, None);
1537        assert!(meta.synthesis.contains("wallet (EOA)"));
1538        assert!(meta.synthesis.contains("Moderate value"));
1539    }
1540
1541    #[test]
1542    fn test_meta_analysis_address_minimal_value() {
1543        let meta = meta_analysis_address(false, Some(0.5), 0, None, None);
1544        assert!(meta.synthesis.contains("Minimal value"));
1545    }
1546
1547    #[test]
1548    fn test_meta_analysis_address_single_token() {
1549        let meta = meta_analysis_address(false, None, 1, None, None);
1550        assert!(meta.synthesis.contains("Concentrated in a single token"));
1551    }
1552
1553    #[test]
1554    fn test_meta_analysis_address_high_risk() {
1555        use crate::compliance::risk::RiskLevel;
1556        let level = RiskLevel::High;
1557        let meta = meta_analysis_address(false, None, 0, Some(8.5), Some(&level));
1558        assert!(meta.synthesis.contains("Elevated risk"));
1559        assert!(meta.key_takeaway.contains("scrutiny"));
1560        assert!(
1561            meta.recommendations
1562                .iter()
1563                .any(|r| r.contains("unusual transaction"))
1564        );
1565    }
1566
1567    #[test]
1568    fn test_meta_analysis_address_low_risk() {
1569        use crate::compliance::risk::RiskLevel;
1570        let level = RiskLevel::Low;
1571        let meta = meta_analysis_address(false, None, 0, Some(2.0), Some(&level));
1572        assert!(meta.synthesis.contains("Low risk"));
1573    }
1574
1575    #[test]
1576    fn test_meta_analysis_address_contract_no_value() {
1577        let meta = meta_analysis_address(true, None, 0, None, None);
1578        assert!(meta.key_takeaway.contains("Contract address"));
1579        assert!(
1580            meta.recommendations
1581                .iter()
1582                .any(|r| r.contains("Confirm contract"))
1583        );
1584    }
1585
1586    #[test]
1587    fn test_meta_analysis_address_high_value_wallet() {
1588        let meta = meta_analysis_address(false, Some(150_000.0), 0, None, None);
1589        assert!(meta.key_takeaway.contains("High-value wallet"));
1590    }
1591
1592    #[test]
1593    fn test_meta_analysis_address_default_takeaway() {
1594        let meta = meta_analysis_address(false, Some(5_000.0), 0, None, None);
1595        assert!(meta.key_takeaway.contains("Review full report"));
1596    }
1597
1598    #[test]
1599    fn test_meta_analysis_address_with_tokens_recommendation() {
1600        let meta = meta_analysis_address(false, None, 3, None, None);
1601        assert!(
1602            meta.recommendations
1603                .iter()
1604                .any(|r| r.contains("Verify token contracts"))
1605        );
1606    }
1607
1608    // ====================================================================
1609    // meta_analysis_tx tests
1610    // ====================================================================
1611
1612    #[test]
1613    fn test_meta_analysis_tx_successful_native_transfer() {
1614        let meta = meta_analysis_tx("Native Transfer", true, false, "0xfrom", Some("0xto"));
1615        assert!(meta.synthesis.contains("Native Transfer"));
1616        assert!(meta.key_takeaway.contains("Routine"));
1617        assert!(meta.recommendations.is_empty());
1618    }
1619
1620    #[test]
1621    fn test_meta_analysis_tx_failed() {
1622        let meta = meta_analysis_tx("Contract Call", false, false, "0xfrom", Some("0xto"));
1623        assert!(meta.synthesis.contains("failed"));
1624        assert!(meta.key_takeaway.contains("Failed transaction"));
1625        assert!(meta.recommendations.iter().any(|r| r.contains("revert")));
1626    }
1627
1628    #[test]
1629    fn test_meta_analysis_tx_high_value_native() {
1630        let meta = meta_analysis_tx("Native Transfer", true, true, "0xfrom", Some("0xto"));
1631        assert!(meta.synthesis.contains("High-value"));
1632        assert!(meta.key_takeaway.contains("Large native transfer"));
1633        assert!(meta.recommendations.iter().any(|r| r.contains("recipient")));
1634    }
1635
1636    #[test]
1637    fn test_meta_analysis_tx_high_value_contract_call() {
1638        let meta = meta_analysis_tx("DEX Swap", true, true, "0xfrom", Some("0xto"));
1639        assert!(meta.key_takeaway.contains("High-value operation"));
1640    }
1641
1642    #[test]
1643    fn test_meta_analysis_tx_erc20_approve() {
1644        let meta = meta_analysis_tx("ERC-20 Approval", true, false, "0xfrom", Some("0xto"));
1645        assert!(meta.recommendations.iter().any(|r| r.contains("spender")));
1646    }
1647
1648    #[test]
1649    fn test_meta_analysis_tx_failed_high_value() {
1650        let meta = meta_analysis_tx("Contract Call", false, true, "0xfrom", Some("0xto"));
1651        assert!(meta.synthesis.contains("failed"));
1652        assert!(meta.synthesis.contains("High-value"));
1653        assert!(meta.recommendations.len() >= 2);
1654    }
1655
1656    // ====================================================================
1657    // meta_analysis_token tests
1658    // ====================================================================
1659
1660    #[test]
1661    fn test_meta_analysis_token_low_risk() {
1662        let summary = report::TokenRiskSummary {
1663            score: 2,
1664            level: "Low",
1665            emoji: "🟢",
1666            concerns: vec![],
1667            positives: vec!["Good liquidity".to_string()],
1668        };
1669        let meta = meta_analysis_token(&summary, false, None, None, 2_000_000.0);
1670        assert!(meta.synthesis.contains("Low-risk"));
1671        assert!(meta.synthesis.contains("Strong liquidity"));
1672        assert!(meta.key_takeaway.contains("Favorable"));
1673    }
1674
1675    #[test]
1676    fn test_meta_analysis_token_high_risk() {
1677        let summary = report::TokenRiskSummary {
1678            score: 8,
1679            level: "High",
1680            emoji: "🔴",
1681            concerns: vec!["Low liquidity".to_string()],
1682            positives: vec![],
1683        };
1684        let meta = meta_analysis_token(&summary, false, None, None, 10_000.0);
1685        assert!(meta.synthesis.contains("Elevated risk"));
1686        assert!(meta.synthesis.contains("Limited liquidity"));
1687        assert!(meta.key_takeaway.contains("High risk"));
1688        assert!(
1689            meta.recommendations
1690                .iter()
1691                .any(|r| r.contains("smaller position"))
1692        );
1693    }
1694
1695    #[test]
1696    fn test_meta_analysis_token_moderate_risk() {
1697        let summary = report::TokenRiskSummary {
1698            score: 5,
1699            level: "Medium",
1700            emoji: "🟡",
1701            concerns: vec!["Some concern".to_string()],
1702            positives: vec!["Some positive".to_string()],
1703        };
1704        let meta = meta_analysis_token(&summary, false, None, None, 500_000.0);
1705        assert!(meta.synthesis.contains("Moderate risk"));
1706        assert!(meta.key_takeaway.contains("Risk 5/10"));
1707    }
1708
1709    #[test]
1710    fn test_meta_analysis_token_stablecoin_healthy_peg() {
1711        let summary = report::TokenRiskSummary {
1712            score: 2,
1713            level: "Low",
1714            emoji: "🟢",
1715            concerns: vec![],
1716            positives: vec!["Stable peg".to_string()],
1717        };
1718        let meta = meta_analysis_token(&summary, true, Some(true), None, 5_000_000.0);
1719        assert!(meta.synthesis.contains("Stablecoin peg is healthy"));
1720    }
1721
1722    #[test]
1723    fn test_meta_analysis_token_stablecoin_unhealthy_peg() {
1724        let summary = report::TokenRiskSummary {
1725            score: 4,
1726            level: "Medium",
1727            emoji: "🟡",
1728            concerns: vec![],
1729            positives: vec![],
1730        };
1731        let meta = meta_analysis_token(&summary, true, Some(false), None, 500_000.0);
1732        assert!(meta.synthesis.contains("peg deviation"));
1733        assert!(meta.key_takeaway.contains("deviating from peg"));
1734        assert!(meta.recommendations.iter().any(|r| r.contains("peg")));
1735    }
1736
1737    #[test]
1738    fn test_meta_analysis_token_concentration_risk() {
1739        let summary = report::TokenRiskSummary {
1740            score: 5,
1741            level: "Medium",
1742            emoji: "🟡",
1743            concerns: vec![],
1744            positives: vec![],
1745        };
1746        let meta = meta_analysis_token(&summary, false, None, Some(45.0), 500_000.0);
1747        assert!(meta.synthesis.contains("Concentration risk"));
1748        assert!(
1749            meta.recommendations
1750                .iter()
1751                .any(|r| r.contains("top holder"))
1752        );
1753    }
1754
1755    #[test]
1756    fn test_meta_analysis_token_low_liquidity_low_risk() {
1757        let summary = report::TokenRiskSummary {
1758            score: 3,
1759            level: "Low",
1760            emoji: "🟢",
1761            concerns: vec![],
1762            positives: vec![],
1763        };
1764        let meta = meta_analysis_token(&summary, false, None, None, 50_000.0);
1765        assert!(
1766            meta.recommendations
1767                .iter()
1768                .any(|r| r.contains("limit orders") || r.contains("slippage"))
1769        );
1770    }
1771
1772    #[test]
1773    fn test_meta_analysis_token_stablecoin_no_peg_data() {
1774        let summary = report::TokenRiskSummary {
1775            score: 3,
1776            level: "Low",
1777            emoji: "🟢",
1778            concerns: vec![],
1779            positives: vec![],
1780        };
1781        let meta = meta_analysis_token(&summary, true, None, None, 1_000_000.0);
1782        // When peg_healthy is None, recommendation should still suggest verifying peg
1783        assert!(meta.recommendations.iter().any(|r| r.contains("peg")));
1784    }
1785
1786    // ====================================================================
1787    // infer_target — additional edge cases
1788    // ====================================================================
1789
1790    #[test]
1791    fn test_infer_target_tx_hash_with_chain_override() {
1792        let t = infer_target(
1793            "0xabc123def456789012345678901234567890123456789012345678901234abcd",
1794            Some("polygon"),
1795        );
1796        assert!(matches!(t, InferredTarget::Transaction { chain } if chain == "polygon"));
1797    }
1798
1799    #[test]
1800    fn test_infer_target_whitespace_trimming() {
1801        let t = infer_target("  USDC  ", None);
1802        assert!(matches!(t, InferredTarget::Token { .. }));
1803    }
1804
1805    #[test]
1806    fn test_infer_target_long_token_name() {
1807        let t = infer_target("some-random-token-name", None);
1808        assert!(matches!(t, InferredTarget::Token { chain } if chain == "ethereum"));
1809    }
1810
1811    // ====================================================================
1812    // InsightsArgs struct validation
1813    // ====================================================================
1814
1815    #[test]
1816    fn test_insights_args_debug() {
1817        let args = InsightsArgs {
1818            target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
1819            chain: Some("ethereum".to_string()),
1820            decode: true,
1821            trace: false,
1822        };
1823        let debug_str = format!("{:?}", args);
1824        assert!(debug_str.contains("InsightsArgs"));
1825        assert!(debug_str.contains("0x742d"));
1826    }
1827
1828    // ====================================================================
1829    // Additional tests for classify_tx_type — selector matches and edge cases
1830    // ====================================================================
1831
1832    #[test]
1833    fn test_classify_tx_type_contract_creation() {
1834        assert_eq!(classify_tx_type("0xa9059cbb...", None), "Contract Creation");
1835    }
1836
1837    #[test]
1838    fn test_classify_tx_type_erc20_transfer() {
1839        assert_eq!(
1840            classify_tx_type("0xa9059cbb00000000", Some("0x1234")),
1841            "ERC-20 Transfer"
1842        );
1843    }
1844
1845    #[test]
1846    fn test_classify_tx_type_erc20_approve() {
1847        assert_eq!(
1848            classify_tx_type("0x095ea7b3...", Some("0x1234")),
1849            "ERC-20 Approve"
1850        );
1851    }
1852
1853    #[test]
1854    fn test_classify_tx_type_erc20_transfer_from() {
1855        assert_eq!(
1856            classify_tx_type("0x23b872dd...", Some("0x1234")),
1857            "ERC-20 Transfer From"
1858        );
1859    }
1860
1861    #[test]
1862    fn test_classify_tx_type_dex_swap() {
1863        assert_eq!(
1864            classify_tx_type("0x38ed1739...", Some("0x1234")),
1865            "DEX Swap"
1866        );
1867        assert_eq!(
1868            classify_tx_type("0x7ff36ab5...", Some("0x1234")),
1869            "DEX Swap"
1870        );
1871    }
1872
1873    #[test]
1874    fn test_classify_tx_type_native_transfer() {
1875        assert_eq!(classify_tx_type("0x", Some("0x1234")), "Native Transfer");
1876        assert_eq!(classify_tx_type("", Some("0x1234")), "Native Transfer");
1877    }
1878
1879    #[test]
1880    fn test_classify_tx_type_unknown_contract_call() {
1881        assert_eq!(
1882            classify_tx_type("0xdeadbeef12345678", Some("0x1234")),
1883            "Contract Call"
1884        );
1885    }
1886
1887    // ====================================================================
1888    // Additional tests for format_tx_value
1889    // ====================================================================
1890
1891    #[test]
1892    fn test_format_tx_value_ethereum_wei() {
1893        let (fmt, high) = format_tx_value("1000000000000000000", "ethereum");
1894        assert!(fmt.contains("1.000000"));
1895        assert!(fmt.contains("ETH"));
1896        assert!(!high); // 1 ETH < 10 threshold
1897    }
1898
1899    #[test]
1900    fn test_format_tx_value_hex() {
1901        let (fmt, _) = format_tx_value("0xde0b6b3a7640000", "ethereum");
1902        // 0xde0b6b3a7640000 = 10^18 = 1 ETH
1903        assert!(fmt.contains("ETH"));
1904    }
1905
1906    #[test]
1907    fn test_format_tx_value_high_value() {
1908        // 100 ETH in wei = 100000000000000000000
1909        let (_, high) = format_tx_value("100000000000000000000", "ethereum");
1910        assert!(high); // 100 ETH > 10
1911    }
1912
1913    #[test]
1914    fn test_format_tx_value_zero_decimal() {
1915        let (fmt, high) = format_tx_value("0", "ethereum");
1916        assert!(fmt.contains("0.000000"));
1917        assert!(!high);
1918    }
1919
1920    #[test]
1921    fn test_format_tx_value_solana_additional() {
1922        let (fmt, _) = format_tx_value("1000000000", "solana"); // 1 SOL
1923        assert!(fmt.contains("SOL"));
1924    }
1925
1926    #[test]
1927    fn test_format_tx_value_tron_additional() {
1928        let (fmt, _) = format_tx_value("1000000", "tron"); // 1 TRX
1929        assert!(fmt.contains("TRX"));
1930    }
1931
1932    #[test]
1933    fn test_format_tx_value_empty_hex_additional() {
1934        let (fmt, _) = format_tx_value("0x", "ethereum");
1935        assert!(fmt.contains("0.000000"));
1936    }
1937
1938    // ====================================================================
1939    // Combined tests for target_type_label and chain_label
1940    // ====================================================================
1941
1942    #[test]
1943    fn test_target_type_label_combined() {
1944        assert_eq!(
1945            target_type_label(&InferredTarget::Address {
1946                chain: "eth".to_string()
1947            }),
1948            "Address"
1949        );
1950        assert_eq!(
1951            target_type_label(&InferredTarget::Transaction {
1952                chain: "eth".to_string()
1953            }),
1954            "Transaction"
1955        );
1956        assert_eq!(
1957            target_type_label(&InferredTarget::Token {
1958                chain: "eth".to_string()
1959            }),
1960            "Token"
1961        );
1962    }
1963
1964    #[test]
1965    fn test_chain_label_combined() {
1966        assert_eq!(
1967            chain_label(&InferredTarget::Address {
1968                chain: "ethereum".to_string()
1969            }),
1970            "ethereum"
1971        );
1972        assert_eq!(
1973            chain_label(&InferredTarget::Transaction {
1974                chain: "polygon".to_string()
1975            }),
1976            "polygon"
1977        );
1978        assert_eq!(
1979            chain_label(&InferredTarget::Token {
1980                chain: "solana".to_string()
1981            }),
1982            "solana"
1983        );
1984    }
1985
1986    // ====================================================================
1987    // Additional tests for meta_analysis_address
1988    // ====================================================================
1989
1990    #[test]
1991    fn test_meta_analysis_address_contract_high_risk() {
1992        use crate::compliance::risk::RiskLevel;
1993        let meta = meta_analysis_address(
1994            true,
1995            Some(2_000_000.0),
1996            10,
1997            Some(8.0),
1998            Some(&RiskLevel::High),
1999        );
2000        assert!(meta.synthesis.contains("contract"));
2001        assert!(meta.synthesis.contains("Significant value"));
2002        assert!(meta.key_takeaway.contains("scrutiny"));
2003        assert!(!meta.recommendations.is_empty());
2004    }
2005
2006    #[test]
2007    fn test_meta_analysis_address_wallet_low_risk() {
2008        use crate::compliance::risk::RiskLevel;
2009        let meta = meta_analysis_address(false, Some(0.5), 0, Some(2.0), Some(&RiskLevel::Low));
2010        assert!(meta.synthesis.contains("wallet"));
2011        assert!(meta.synthesis.contains("Minimal value"));
2012    }
2013
2014    #[test]
2015    fn test_meta_analysis_address_no_risk_data() {
2016        let meta = meta_analysis_address(false, None, 0, None, None);
2017        assert!(!meta.synthesis.is_empty());
2018        assert!(meta.key_takeaway.contains("Review full report"));
2019    }
2020
2021    // ====================================================================
2022    // Additional tests for meta_analysis_tx
2023    // ====================================================================
2024
2025    #[test]
2026    fn test_meta_analysis_tx_failed_additional() {
2027        let meta = meta_analysis_tx("Contract Call", false, false, "0x...", Some("0x..."));
2028        assert!(meta.synthesis.contains("failed"));
2029        assert!(meta.key_takeaway.contains("Failed"));
2030    }
2031
2032    #[test]
2033    fn test_meta_analysis_tx_high_value_native_additional() {
2034        let meta = meta_analysis_tx("Native Transfer", true, true, "0x...", Some("0x..."));
2035        assert!(meta.synthesis.contains("High-value"));
2036        assert!(meta.key_takeaway.contains("Large native transfer"));
2037    }
2038
2039    #[test]
2040    fn test_meta_analysis_tx_routine() {
2041        let meta = meta_analysis_tx("ERC-20 Transfer", true, false, "0x...", Some("0x..."));
2042        assert!(meta.key_takeaway.contains("Routine"));
2043    }
2044
2045    // ====================================================================
2046    // Additional tests for meta_analysis_token
2047    // ====================================================================
2048
2049    #[test]
2050    fn test_meta_analysis_token_high_risk_additional() {
2051        let risk = report::TokenRiskSummary {
2052            score: 8,
2053            level: "High",
2054            emoji: "🔴",
2055            concerns: vec!["Low liquidity".to_string()],
2056            positives: vec![],
2057        };
2058        let meta = meta_analysis_token(&risk, false, None, None, 10_000.0);
2059        assert!(meta.synthesis.contains("Elevated risk"));
2060        assert!(meta.key_takeaway.contains("High risk"));
2061    }
2062
2063    #[test]
2064    fn test_meta_analysis_token_stablecoin_peg_healthy() {
2065        let risk = report::TokenRiskSummary {
2066            score: 2,
2067            level: "Low",
2068            emoji: "🟢",
2069            concerns: vec![],
2070            positives: vec!["Strong liquidity".to_string()],
2071        };
2072        let meta = meta_analysis_token(&risk, true, Some(true), Some(5.0), 5_000_000.0);
2073        assert!(meta.synthesis.contains("peg is healthy"));
2074        assert!(meta.synthesis.contains("Strong liquidity"));
2075    }
2076
2077    #[test]
2078    fn test_meta_analysis_token_stablecoin_peg_unhealthy() {
2079        let risk = report::TokenRiskSummary {
2080            score: 5,
2081            level: "Medium",
2082            emoji: "🟡",
2083            concerns: vec!["Peg deviation".to_string()],
2084            positives: vec![],
2085        };
2086        let meta = meta_analysis_token(&risk, true, Some(false), Some(40.0), 100_000.0);
2087        assert!(meta.synthesis.contains("peg deviation"));
2088        assert!(meta.synthesis.contains("Concentration risk"));
2089    }
2090}