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