Skip to main content

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