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            let http: std::sync::Arc<dyn crate::http::HttpClient> =
849                std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
850            crate::chains::DefaultClientFactory {
851                chains_config: Default::default(),
852                http,
853            }
854            .create_dex_client()
855        }
856    }
857
858    // Mock that returns a contract address
859    struct MockContractClient;
860
861    #[async_trait]
862    impl ChainClient for MockContractClient {
863        fn chain_name(&self) -> &str {
864            "ethereum"
865        }
866        fn native_token_symbol(&self) -> &str {
867            "ETH"
868        }
869        async fn get_balance(&self, _address: &str) -> crate::error::Result<ChainBalance> {
870            Ok(ChainBalance {
871                raw: "0".to_string(),
872                formatted: "0.0 ETH".to_string(),
873                decimals: 18,
874                symbol: "ETH".to_string(),
875                usd_value: Some(0.0),
876            })
877        }
878        async fn enrich_balance_usd(&self, _balance: &mut ChainBalance) {}
879        async fn get_transaction(&self, hash: &str) -> crate::error::Result<ChainTransaction> {
880            Ok(ChainTransaction {
881                hash: hash.to_string(),
882                block_number: Some(100),
883                timestamp: Some(1700000000),
884                from: "0xfrom".to_string(),
885                to: None, // contract creation
886                value: "0".to_string(),
887                gas_limit: 100000,
888                gas_used: Some(80000),
889                gas_price: "10000000000".to_string(),
890                nonce: 0,
891                input: "0x60806040".to_string(),
892                status: Some(false), // failed tx
893            })
894        }
895        async fn get_transactions(
896            &self,
897            _address: &str,
898            _limit: u32,
899        ) -> crate::error::Result<Vec<ChainTransaction>> {
900            Ok(vec![])
901        }
902        async fn get_block_number(&self) -> crate::error::Result<u64> {
903            Ok(100)
904        }
905        async fn get_token_balances(
906            &self,
907            _address: &str,
908        ) -> crate::error::Result<Vec<ChainTokenBalance>> {
909            Ok(vec![])
910        }
911        async fn get_code(&self, _address: &str) -> crate::error::Result<String> {
912            Ok("0x6080604052".to_string()) // contract
913        }
914    }
915
916    struct MockContractFactory;
917
918    impl ChainClientFactory for MockContractFactory {
919        fn create_chain_client(&self, _chain: &str) -> crate::error::Result<Box<dyn ChainClient>> {
920            Ok(Box::new(MockContractClient))
921        }
922        fn create_dex_client(&self) -> Box<dyn DexDataSource> {
923            let http: std::sync::Arc<dyn crate::http::HttpClient> =
924                std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
925            crate::chains::DefaultClientFactory {
926                chains_config: Default::default(),
927                http,
928            }
929            .create_dex_client()
930        }
931    }
932
933    // Mock DexDataSource for token tests
934    struct MockDexDataSource;
935
936    #[async_trait]
937    impl DexDataSource for MockDexDataSource {
938        async fn get_token_price(&self, _chain: &str, _address: &str) -> Option<f64> {
939            Some(1.0)
940        }
941
942        async fn get_native_token_price(&self, _chain: &str) -> Option<f64> {
943            Some(2500.0)
944        }
945
946        async fn get_token_data(
947            &self,
948            _chain: &str,
949            address: &str,
950        ) -> crate::error::Result<crate::chains::dex::DexTokenData> {
951            use crate::chains::{DexPair, PricePoint, VolumePoint};
952            Ok(crate::chains::dex::DexTokenData {
953                address: address.to_string(),
954                symbol: "TEST".to_string(),
955                name: "Test Token".to_string(),
956                price_usd: 1.5,
957                price_change_24h: 5.2,
958                price_change_6h: 2.1,
959                price_change_1h: 0.5,
960                price_change_5m: 0.1,
961                volume_24h: 1_000_000.0,
962                volume_6h: 250_000.0,
963                volume_1h: 50_000.0,
964                liquidity_usd: 500_000.0,
965                market_cap: Some(10_000_000.0),
966                fdv: Some(12_000_000.0),
967                pairs: vec![DexPair {
968                    dex_name: "Uniswap V3".to_string(),
969                    pair_address: "0xpair123".to_string(),
970                    base_token: "TEST".to_string(),
971                    quote_token: "USDC".to_string(),
972                    price_usd: 1.5,
973                    liquidity_usd: 500_000.0,
974                    volume_24h: 1_000_000.0,
975                    price_change_24h: 5.2,
976                    buys_24h: 100,
977                    sells_24h: 80,
978                    buys_6h: 20,
979                    sells_6h: 15,
980                    buys_1h: 5,
981                    sells_1h: 3,
982                    pair_created_at: Some(1690000000),
983                    url: Some("https://dexscreener.com/ethereum/0xpair123".to_string()),
984                }],
985                price_history: vec![PricePoint {
986                    timestamp: 1690000000,
987                    price: 1.5,
988                }],
989                volume_history: vec![VolumePoint {
990                    timestamp: 1690000000,
991                    volume: 1_000_000.0,
992                }],
993                total_buys_24h: 100,
994                total_sells_24h: 80,
995                total_buys_6h: 20,
996                total_sells_6h: 15,
997                total_buys_1h: 5,
998                total_sells_1h: 3,
999                earliest_pair_created_at: Some(1690000000),
1000                image_url: None,
1001                websites: Vec::new(),
1002                socials: Vec::new(),
1003                dexscreener_url: Some("https://dexscreener.com/ethereum/test".to_string()),
1004            })
1005        }
1006
1007        async fn search_tokens(
1008            &self,
1009            _query: &str,
1010            _chain: Option<&str>,
1011        ) -> crate::error::Result<Vec<crate::chains::TokenSearchResult>> {
1012            Ok(vec![crate::chains::TokenSearchResult {
1013                address: "0xTEST1234567890123456789012345678901234567".to_string(),
1014                symbol: "TEST".to_string(),
1015                name: "Test Token".to_string(),
1016                chain: "ethereum".to_string(),
1017                price_usd: Some(1.5),
1018                volume_24h: 1_000_000.0,
1019                liquidity_usd: 500_000.0,
1020                market_cap: Some(10_000_000.0),
1021            }])
1022        }
1023    }
1024
1025    // Mock ChainClient that returns holders with high concentration
1026    struct MockTokenChainClient;
1027
1028    #[async_trait]
1029    impl ChainClient for MockTokenChainClient {
1030        fn chain_name(&self) -> &str {
1031            "ethereum"
1032        }
1033        fn native_token_symbol(&self) -> &str {
1034            "ETH"
1035        }
1036        async fn get_balance(&self, _address: &str) -> crate::error::Result<ChainBalance> {
1037            Ok(ChainBalance {
1038                raw: "1000000000000000000".to_string(),
1039                formatted: "1.0 ETH".to_string(),
1040                decimals: 18,
1041                symbol: "ETH".to_string(),
1042                usd_value: Some(2500.0),
1043            })
1044        }
1045        async fn enrich_balance_usd(&self, balance: &mut ChainBalance) {
1046            balance.usd_value = Some(2500.0);
1047        }
1048        async fn get_transaction(&self, _hash: &str) -> crate::error::Result<ChainTransaction> {
1049            Ok(ChainTransaction {
1050                hash: "0xabc123".to_string(),
1051                block_number: Some(12345678),
1052                timestamp: Some(1700000000),
1053                from: "0xfrom".to_string(),
1054                to: Some("0xto".to_string()),
1055                value: "0".to_string(),
1056                gas_limit: 21000,
1057                gas_used: Some(21000),
1058                gas_price: "20000000000".to_string(),
1059                nonce: 42,
1060                input: "0x".to_string(),
1061                status: Some(true),
1062            })
1063        }
1064        async fn get_transactions(
1065            &self,
1066            _address: &str,
1067            _limit: u32,
1068        ) -> crate::error::Result<Vec<ChainTransaction>> {
1069            Ok(vec![])
1070        }
1071        async fn get_block_number(&self) -> crate::error::Result<u64> {
1072            Ok(12345678)
1073        }
1074        async fn get_token_balances(
1075            &self,
1076            _address: &str,
1077        ) -> crate::error::Result<Vec<ChainTokenBalance>> {
1078            Ok(vec![])
1079        }
1080        async fn get_code(&self, _address: &str) -> crate::error::Result<String> {
1081            Ok("0x".to_string())
1082        }
1083        async fn get_token_holders(
1084            &self,
1085            _address: &str,
1086            _limit: u32,
1087        ) -> crate::error::Result<Vec<crate::chains::TokenHolder>> {
1088            // Return holders with high concentration (>30%) to trigger warning
1089            Ok(vec![
1090                crate::chains::TokenHolder {
1091                    address: "0x1111111111111111111111111111111111111111".to_string(),
1092                    balance: "3500000000000000000000000".to_string(),
1093                    formatted_balance: "3500000.0".to_string(),
1094                    percentage: 35.0, // >30% triggers concentration warning
1095                    rank: 1,
1096                },
1097                crate::chains::TokenHolder {
1098                    address: "0x2222222222222222222222222222222222222222".to_string(),
1099                    balance: "1500000000000000000000000".to_string(),
1100                    formatted_balance: "1500000.0".to_string(),
1101                    percentage: 15.0,
1102                    rank: 2,
1103                },
1104                crate::chains::TokenHolder {
1105                    address: "0x3333333333333333333333333333333333333333".to_string(),
1106                    balance: "1000000000000000000000000".to_string(),
1107                    formatted_balance: "1000000.0".to_string(),
1108                    percentage: 10.0,
1109                    rank: 3,
1110                },
1111            ])
1112        }
1113    }
1114
1115    // Factory for token tests with mocks
1116    struct MockTokenFactory;
1117
1118    impl ChainClientFactory for MockTokenFactory {
1119        fn create_chain_client(&self, _chain: &str) -> crate::error::Result<Box<dyn ChainClient>> {
1120            Ok(Box::new(MockTokenChainClient))
1121        }
1122        fn create_dex_client(&self) -> Box<dyn DexDataSource> {
1123            Box::new(MockDexDataSource)
1124        }
1125    }
1126
1127    // ====================================================================
1128    // run() function tests with mocks
1129    // ====================================================================
1130
1131    #[tokio::test]
1132    async fn test_run_address_eoa() {
1133        let config = Config::default();
1134        let factory = MockFactory;
1135        let args = InsightsArgs {
1136            target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
1137            chain: None,
1138            decode: false,
1139            trace: false,
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        };
1155        let result = run(args, &config, &factory).await;
1156        assert!(result.is_ok());
1157    }
1158
1159    #[tokio::test]
1160    async fn test_run_transaction() {
1161        let config = Config::default();
1162        let factory = MockFactory;
1163        let args = InsightsArgs {
1164            target: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
1165                .to_string(),
1166            chain: None,
1167            decode: false,
1168            trace: false,
1169        };
1170        let result = run(args, &config, &factory).await;
1171        assert!(result.is_ok());
1172    }
1173
1174    #[tokio::test]
1175    async fn test_run_transaction_failed() {
1176        let config = Config::default();
1177        let factory = MockContractFactory;
1178        let args = InsightsArgs {
1179            target: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
1180                .to_string(),
1181            chain: Some("ethereum".to_string()),
1182            decode: true,
1183            trace: false,
1184        };
1185        let result = run(args, &config, &factory).await;
1186        assert!(result.is_ok());
1187    }
1188
1189    #[tokio::test]
1190    async fn test_run_address_with_chain_override() {
1191        let config = Config::default();
1192        let factory = MockFactory;
1193        let args = InsightsArgs {
1194            target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
1195            chain: Some("polygon".to_string()),
1196            decode: false,
1197            trace: false,
1198        };
1199        let result = run(args, &config, &factory).await;
1200        assert!(result.is_ok());
1201    }
1202
1203    #[tokio::test]
1204    async fn test_insights_run_token() {
1205        let config = Config::default();
1206        let factory = MockTokenFactory;
1207        let args = InsightsArgs {
1208            target: "TEST".to_string(),
1209            chain: Some("ethereum".to_string()),
1210            decode: false,
1211            trace: false,
1212        };
1213        let result = run(args, &config, &factory).await;
1214        assert!(result.is_ok());
1215    }
1216
1217    #[tokio::test]
1218    async fn test_insights_run_token_with_concentration_warning() {
1219        let config = Config::default();
1220        let factory = MockTokenFactory;
1221        let args = InsightsArgs {
1222            target: "0xTEST1234567890123456789012345678901234567".to_string(),
1223            chain: Some("ethereum".to_string()),
1224            decode: false,
1225            trace: false,
1226        };
1227        let result = run(args, &config, &factory).await;
1228        assert!(result.is_ok());
1229    }
1230
1231    // ====================================================================
1232    // Existing tests below
1233    // ====================================================================
1234
1235    #[test]
1236    fn test_infer_target_evm_address() {
1237        let t = infer_target("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", None);
1238        assert!(matches!(t, InferredTarget::Address { chain } if chain == "ethereum"));
1239    }
1240
1241    #[test]
1242    fn test_infer_target_tron_address() {
1243        let t = infer_target("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", None);
1244        assert!(matches!(t, InferredTarget::Address { chain } if chain == "tron"));
1245    }
1246
1247    #[test]
1248    fn test_infer_target_solana_address() {
1249        let t = infer_target("DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy", None);
1250        assert!(matches!(t, InferredTarget::Address { chain } if chain == "solana"));
1251    }
1252
1253    #[test]
1254    fn test_infer_target_evm_tx_hash() {
1255        let t = infer_target(
1256            "0xabc123def456789012345678901234567890123456789012345678901234abcd",
1257            None,
1258        );
1259        assert!(matches!(t, InferredTarget::Transaction { chain } if chain == "ethereum"));
1260    }
1261
1262    #[test]
1263    fn test_infer_target_tron_tx_hash() {
1264        let t = infer_target(
1265            "abc123def456789012345678901234567890123456789012345678901234abcd",
1266            None,
1267        );
1268        assert!(matches!(t, InferredTarget::Transaction { chain } if chain == "tron"));
1269    }
1270
1271    #[test]
1272    fn test_infer_target_token_symbol() {
1273        let t = infer_target("USDC", None);
1274        assert!(matches!(t, InferredTarget::Token { chain } if chain == "ethereum"));
1275    }
1276
1277    #[test]
1278    fn test_infer_target_chain_override() {
1279        let t = infer_target(
1280            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
1281            Some("polygon"),
1282        );
1283        assert!(matches!(t, InferredTarget::Address { chain } if chain == "polygon"));
1284    }
1285
1286    #[test]
1287    fn test_infer_target_token_with_chain_override() {
1288        let t = infer_target("USDC", Some("solana"));
1289        assert!(matches!(t, InferredTarget::Token { chain } if chain == "solana"));
1290    }
1291
1292    #[test]
1293    fn test_classify_tx_type() {
1294        assert_eq!(
1295            classify_tx_type("0xa9059cbb1234...", Some("0xto")),
1296            "ERC-20 Transfer"
1297        );
1298        assert_eq!(
1299            classify_tx_type("0x095ea7b3abcd...", Some("0xto")),
1300            "ERC-20 Approve"
1301        );
1302        assert_eq!(classify_tx_type("0x", Some("0xto")), "Native Transfer");
1303        assert_eq!(classify_tx_type("", None), "Contract Creation");
1304    }
1305
1306    #[test]
1307    fn test_format_tx_value() {
1308        let (fmt, high) = format_tx_value("0xDE0B6B3A7640000", "ethereum"); // 1 ETH
1309        assert!(fmt.contains("1.0") && fmt.contains("ETH"));
1310        assert!(!high);
1311        let (_, high2) = format_tx_value("0x52B7D2DCC80CD2E4000000", "ethereum"); // 100 ETH
1312        assert!(high2);
1313    }
1314
1315    #[test]
1316    fn test_is_stablecoin() {
1317        assert!(is_stablecoin("USDC"));
1318        assert!(is_stablecoin("usdt"));
1319        assert!(is_stablecoin("DAI"));
1320        assert!(is_stablecoin("BUSD"));
1321        assert!(is_stablecoin("TUSD"));
1322        assert!(is_stablecoin("USDP"));
1323        assert!(is_stablecoin("FRAX"));
1324        assert!(is_stablecoin("LUSD"));
1325        assert!(is_stablecoin("GUSD"));
1326        assert!(!is_stablecoin("ETH"));
1327        assert!(!is_stablecoin("PEPE"));
1328        assert!(!is_stablecoin("WBTC"));
1329    }
1330
1331    #[test]
1332    fn test_is_stablecoin_empty_string() {
1333        assert!(!is_stablecoin(""));
1334    }
1335
1336    #[test]
1337    fn test_is_stablecoin_case_insensitive() {
1338        // to_uppercase() makes comparison case-insensitive
1339        assert!(is_stablecoin("UsDc"));
1340        assert!(is_stablecoin("FraX"));
1341        assert!(!is_stablecoin("SOL")); // SOL is not a stablecoin
1342    }
1343
1344    // ====================================================================
1345    // target_type_label and chain_label tests
1346    // ====================================================================
1347
1348    #[test]
1349    fn test_target_type_label_address() {
1350        let t = InferredTarget::Address {
1351            chain: "ethereum".to_string(),
1352        };
1353        assert_eq!(target_type_label(&t), "Address");
1354    }
1355
1356    #[test]
1357    fn test_target_type_label_transaction() {
1358        let t = InferredTarget::Transaction {
1359            chain: "ethereum".to_string(),
1360        };
1361        assert_eq!(target_type_label(&t), "Transaction");
1362    }
1363
1364    #[test]
1365    fn test_target_type_label_token() {
1366        let t = InferredTarget::Token {
1367            chain: "ethereum".to_string(),
1368        };
1369        assert_eq!(target_type_label(&t), "Token");
1370    }
1371
1372    #[test]
1373    fn test_chain_label_address() {
1374        let t = InferredTarget::Address {
1375            chain: "polygon".to_string(),
1376        };
1377        assert_eq!(chain_label(&t), "polygon");
1378    }
1379
1380    #[test]
1381    fn test_chain_label_transaction() {
1382        let t = InferredTarget::Transaction {
1383            chain: "tron".to_string(),
1384        };
1385        assert_eq!(chain_label(&t), "tron");
1386    }
1387
1388    #[test]
1389    fn test_chain_label_token() {
1390        let t = InferredTarget::Token {
1391            chain: "solana".to_string(),
1392        };
1393        assert_eq!(chain_label(&t), "solana");
1394    }
1395
1396    // ====================================================================
1397    // classify_tx_type — expanded edge cases
1398    // ====================================================================
1399
1400    #[test]
1401    fn test_classify_tx_type_dex_swaps() {
1402        assert_eq!(
1403            classify_tx_type("0x38ed173900000...", Some("0xrouter")),
1404            "DEX Swap"
1405        );
1406        assert_eq!(
1407            classify_tx_type("0x5c11d79500000...", Some("0xrouter")),
1408            "DEX Swap"
1409        );
1410        assert_eq!(
1411            classify_tx_type("0x4a25d94a00000...", Some("0xrouter")),
1412            "DEX Swap"
1413        );
1414        assert_eq!(
1415            classify_tx_type("0x8803dbee00000...", Some("0xrouter")),
1416            "DEX Swap"
1417        );
1418        assert_eq!(
1419            classify_tx_type("0x7ff36ab500000...", Some("0xrouter")),
1420            "DEX Swap"
1421        );
1422        assert_eq!(
1423            classify_tx_type("0x18cbafe500000...", Some("0xrouter")),
1424            "DEX Swap"
1425        );
1426        assert_eq!(
1427            classify_tx_type("0xfb3bdb4100000...", Some("0xrouter")),
1428            "DEX Swap"
1429        );
1430        assert_eq!(
1431            classify_tx_type("0xb6f9de9500000...", Some("0xrouter")),
1432            "DEX Swap"
1433        );
1434    }
1435
1436    #[test]
1437    fn test_classify_tx_type_multicall() {
1438        assert_eq!(
1439            classify_tx_type("0xac9650d800000...", Some("0xcontract")),
1440            "Multicall"
1441        );
1442        assert_eq!(
1443            classify_tx_type("0x5ae401dc00000...", Some("0xcontract")),
1444            "Multicall"
1445        );
1446    }
1447
1448    #[test]
1449    fn test_classify_tx_type_transfer_from() {
1450        assert_eq!(
1451            classify_tx_type("0x23b872dd00000...", Some("0xtoken")),
1452            "ERC-20 Transfer From"
1453        );
1454    }
1455
1456    #[test]
1457    fn test_classify_tx_type_contract_call() {
1458        assert_eq!(
1459            classify_tx_type("0xdeadbeef00000...", Some("0xcontract")),
1460            "Contract Call"
1461        );
1462    }
1463
1464    #[test]
1465    fn test_classify_tx_type_native_transfer_empty() {
1466        assert_eq!(classify_tx_type("", Some("0xrecipient")), "Native Transfer");
1467    }
1468
1469    // ====================================================================
1470    // format_tx_value — expanded edge cases
1471    // ====================================================================
1472
1473    #[test]
1474    fn test_format_tx_value_zero() {
1475        let (fmt, high) = format_tx_value("0x0", "ethereum");
1476        assert!(fmt.contains("0.000000"));
1477        assert!(fmt.contains("ETH"));
1478        assert!(!high);
1479    }
1480
1481    #[test]
1482    fn test_format_tx_value_empty_hex() {
1483        let (fmt, high) = format_tx_value("0x", "ethereum");
1484        assert!(fmt.contains("0.000000"));
1485        assert!(!high);
1486    }
1487
1488    #[test]
1489    fn test_format_tx_value_decimal_string() {
1490        let (fmt, high) = format_tx_value("1000000000000000000", "ethereum"); // 1 ETH
1491        assert!(fmt.contains("1.0"));
1492        assert!(fmt.contains("ETH"));
1493        assert!(!high);
1494    }
1495
1496    #[test]
1497    fn test_format_tx_value_solana() {
1498        let (fmt, high) = format_tx_value("1000000000", "solana"); // 1 SOL (9 decimals)
1499        assert!(fmt.contains("1.0"));
1500        assert!(fmt.contains("SOL"));
1501        assert!(!high);
1502    }
1503
1504    #[test]
1505    fn test_format_tx_value_tron() {
1506        let (fmt, high) = format_tx_value("1000000", "tron"); // 1 TRX (6 decimals)
1507        assert!(fmt.contains("1.0"));
1508        assert!(fmt.contains("TRX"));
1509        assert!(!high);
1510    }
1511
1512    #[test]
1513    fn test_format_tx_value_polygon() {
1514        let (fmt, _) = format_tx_value("1000000000000000000", "polygon");
1515        assert!(fmt.contains("MATIC") || fmt.contains("POL"));
1516    }
1517
1518    #[test]
1519    fn test_format_tx_value_bsc() {
1520        let (fmt, _) = format_tx_value("1000000000000000000", "bsc");
1521        assert!(fmt.contains("BNB"));
1522    }
1523
1524    #[test]
1525    fn test_format_tx_value_arbitrum() {
1526        let (fmt, _) = format_tx_value("1000000000000000000", "arbitrum");
1527        assert!(fmt.contains("ETH"));
1528    }
1529
1530    #[test]
1531    fn test_format_tx_value_optimism() {
1532        let (fmt, _) = format_tx_value("1000000000000000000", "optimism");
1533        assert!(fmt.contains("ETH"));
1534    }
1535
1536    #[test]
1537    fn test_format_tx_value_base() {
1538        let (fmt, _) = format_tx_value("1000000000000000000", "base");
1539        assert!(fmt.contains("ETH"));
1540    }
1541
1542    #[test]
1543    fn test_format_tx_value_aegis() {
1544        // aegis uses 18 decimals; native_symbol returns "???" for unknown chains
1545        let (fmt, _) = format_tx_value("1000000000000000000", "aegis");
1546        assert!(fmt.contains("1.0") || fmt.contains("1.000000"));
1547    }
1548
1549    #[test]
1550    fn test_format_tx_value_unknown_chain_defaults_18_decimals() {
1551        let (fmt, _) = format_tx_value("1000000000000000000", "unknown_chain");
1552        assert!(fmt.contains("1") || fmt.contains("ETH"));
1553    }
1554
1555    #[test]
1556    fn test_format_tx_value_invalid_hex_parse() {
1557        let (fmt, high) = format_tx_value("0xZZZZ", "ethereum");
1558        assert!(fmt.contains("0.000000"));
1559        assert!(!high);
1560    }
1561
1562    #[test]
1563    fn test_format_tx_value_high_value_threshold() {
1564        // > 10 native units = high value
1565        let (_, high) = format_tx_value("11000000000000000000", "ethereum"); // 11 ETH
1566        assert!(high);
1567        let (_, high2) = format_tx_value("10000000000000000000", "ethereum"); // 10 ETH
1568        assert!(!high2); // exactly 10 is not > 10
1569    }
1570
1571    // ====================================================================
1572    // meta_analysis_address tests
1573    // ====================================================================
1574
1575    #[test]
1576    fn test_meta_analysis_address_contract_high_value() {
1577        let meta = meta_analysis_address(true, Some(2_000_000.0), 10, None, None);
1578        assert!(meta.synthesis.contains("contract"));
1579        assert!(meta.synthesis.contains("Significant value"));
1580        assert!(meta.synthesis.contains("Diversified"));
1581        assert!(meta.recommendations.iter().any(|r| r.contains("contract")));
1582    }
1583
1584    #[test]
1585    fn test_meta_analysis_address_eoa_moderate_value() {
1586        let meta = meta_analysis_address(false, Some(50_000.0), 3, None, None);
1587        assert!(meta.synthesis.contains("wallet (EOA)"));
1588        assert!(meta.synthesis.contains("Moderate value"));
1589    }
1590
1591    #[test]
1592    fn test_meta_analysis_address_minimal_value() {
1593        let meta = meta_analysis_address(false, Some(0.5), 0, None, None);
1594        assert!(meta.synthesis.contains("Minimal value"));
1595    }
1596
1597    #[test]
1598    fn test_meta_analysis_address_single_token() {
1599        let meta = meta_analysis_address(false, None, 1, None, None);
1600        assert!(meta.synthesis.contains("Concentrated in a single token"));
1601    }
1602
1603    #[test]
1604    fn test_meta_analysis_address_high_risk() {
1605        use crate::compliance::risk::RiskLevel;
1606        let level = RiskLevel::High;
1607        let meta = meta_analysis_address(false, None, 0, Some(8.5), Some(&level));
1608        assert!(meta.synthesis.contains("Elevated risk"));
1609        assert!(meta.key_takeaway.contains("scrutiny"));
1610        assert!(
1611            meta.recommendations
1612                .iter()
1613                .any(|r| r.contains("unusual transaction"))
1614        );
1615    }
1616
1617    #[test]
1618    fn test_meta_analysis_address_low_risk() {
1619        use crate::compliance::risk::RiskLevel;
1620        let level = RiskLevel::Low;
1621        let meta = meta_analysis_address(false, None, 0, Some(2.0), Some(&level));
1622        assert!(meta.synthesis.contains("Low risk"));
1623    }
1624
1625    #[test]
1626    fn test_meta_analysis_address_contract_no_value() {
1627        let meta = meta_analysis_address(true, None, 0, None, None);
1628        assert!(meta.key_takeaway.contains("Contract address"));
1629        assert!(
1630            meta.recommendations
1631                .iter()
1632                .any(|r| r.contains("Confirm contract"))
1633        );
1634    }
1635
1636    #[test]
1637    fn test_meta_analysis_address_high_value_wallet() {
1638        let meta = meta_analysis_address(false, Some(150_000.0), 0, None, None);
1639        assert!(meta.key_takeaway.contains("High-value wallet"));
1640    }
1641
1642    #[test]
1643    fn test_meta_analysis_address_default_takeaway() {
1644        let meta = meta_analysis_address(false, Some(5_000.0), 0, None, None);
1645        assert!(meta.key_takeaway.contains("Review full report"));
1646    }
1647
1648    #[test]
1649    fn test_meta_analysis_address_empty_synthesis() {
1650        let meta = meta_analysis_address(
1651            false,
1652            Some(5_000.0), // moderate value, not significant/minimal
1653            2,             // 2 tokens, not 1, not >5
1654            None,
1655            None,
1656        );
1657        assert!(meta.synthesis.contains("wallet (EOA)"));
1658    }
1659
1660    #[test]
1661    fn test_meta_analysis_address_synthesis_parts_joined() {
1662        let meta = meta_analysis_address(false, None, 0, None, None);
1663        assert!(!meta.synthesis.is_empty());
1664        assert!(meta.synthesis.contains("Address analyzed") || meta.synthesis.contains("wallet"));
1665    }
1666
1667    #[test]
1668    fn test_meta_analysis_address_with_tokens_recommendation() {
1669        let meta = meta_analysis_address(false, None, 3, None, None);
1670        assert!(
1671            meta.recommendations
1672                .iter()
1673                .any(|r| r.contains("Verify token contracts"))
1674        );
1675    }
1676
1677    // ====================================================================
1678    // meta_analysis_tx tests
1679    // ====================================================================
1680
1681    #[test]
1682    fn test_meta_analysis_tx_successful_native_transfer() {
1683        let meta = meta_analysis_tx("Native Transfer", true, false, "0xfrom", Some("0xto"));
1684        assert!(meta.synthesis.contains("Native Transfer"));
1685        assert!(meta.key_takeaway.contains("Routine"));
1686        assert!(meta.recommendations.is_empty());
1687    }
1688
1689    #[test]
1690    fn test_meta_analysis_tx_failed() {
1691        let meta = meta_analysis_tx("Contract Call", false, false, "0xfrom", Some("0xto"));
1692        assert!(meta.synthesis.contains("failed"));
1693        assert!(meta.key_takeaway.contains("Failed transaction"));
1694        assert!(meta.recommendations.iter().any(|r| r.contains("revert")));
1695    }
1696
1697    #[test]
1698    fn test_meta_analysis_tx_high_value_native() {
1699        let meta = meta_analysis_tx("Native Transfer", true, true, "0xfrom", Some("0xto"));
1700        assert!(meta.synthesis.contains("High-value"));
1701        assert!(meta.key_takeaway.contains("Large native transfer"));
1702        assert!(meta.recommendations.iter().any(|r| r.contains("recipient")));
1703    }
1704
1705    #[test]
1706    fn test_meta_analysis_tx_high_value_contract_call() {
1707        let meta = meta_analysis_tx("DEX Swap", true, true, "0xfrom", Some("0xto"));
1708        assert!(meta.key_takeaway.contains("High-value operation"));
1709    }
1710
1711    #[test]
1712    fn test_meta_analysis_tx_erc20_approve() {
1713        let meta = meta_analysis_tx("ERC-20 Approval", true, false, "0xfrom", Some("0xto"));
1714        assert!(meta.recommendations.iter().any(|r| r.contains("spender")));
1715    }
1716
1717    #[test]
1718    fn test_meta_analysis_tx_failed_high_value() {
1719        let meta = meta_analysis_tx("Contract Call", false, true, "0xfrom", Some("0xto"));
1720        assert!(meta.synthesis.contains("failed"));
1721        assert!(meta.synthesis.contains("High-value"));
1722        assert!(meta.recommendations.len() >= 2);
1723    }
1724
1725    // ====================================================================
1726    // meta_analysis_token tests
1727    // ====================================================================
1728
1729    #[test]
1730    fn test_meta_analysis_token_low_risk() {
1731        let summary = report::TokenRiskSummary {
1732            score: 2,
1733            level: "Low",
1734            emoji: "🟢",
1735            concerns: vec![],
1736            positives: vec!["Good liquidity".to_string()],
1737        };
1738        let meta = meta_analysis_token(&summary, false, None, None, 2_000_000.0);
1739        assert!(meta.synthesis.contains("Low-risk"));
1740        assert!(meta.synthesis.contains("Strong liquidity"));
1741        assert!(meta.key_takeaway.contains("Favorable"));
1742    }
1743
1744    #[test]
1745    fn test_meta_analysis_token_high_risk() {
1746        let summary = report::TokenRiskSummary {
1747            score: 8,
1748            level: "High",
1749            emoji: "🔴",
1750            concerns: vec!["Low liquidity".to_string()],
1751            positives: vec![],
1752        };
1753        let meta = meta_analysis_token(&summary, false, None, None, 10_000.0);
1754        assert!(meta.synthesis.contains("Elevated risk"));
1755        assert!(meta.synthesis.contains("Limited liquidity"));
1756        assert!(meta.key_takeaway.contains("High risk"));
1757        assert!(
1758            meta.recommendations
1759                .iter()
1760                .any(|r| r.contains("smaller position"))
1761        );
1762    }
1763
1764    #[test]
1765    fn test_meta_analysis_token_moderate_risk() {
1766        let summary = report::TokenRiskSummary {
1767            score: 5,
1768            level: "Medium",
1769            emoji: "🟡",
1770            concerns: vec!["Some concern".to_string()],
1771            positives: vec!["Some positive".to_string()],
1772        };
1773        let meta = meta_analysis_token(&summary, false, None, None, 500_000.0);
1774        assert!(meta.synthesis.contains("Moderate risk"));
1775        assert!(meta.key_takeaway.contains("Risk 5/10"));
1776    }
1777
1778    #[test]
1779    fn test_meta_analysis_token_stablecoin_healthy_peg() {
1780        let summary = report::TokenRiskSummary {
1781            score: 2,
1782            level: "Low",
1783            emoji: "🟢",
1784            concerns: vec![],
1785            positives: vec!["Stable peg".to_string()],
1786        };
1787        let meta = meta_analysis_token(&summary, true, Some(true), None, 5_000_000.0);
1788        assert!(meta.synthesis.contains("Stablecoin peg is healthy"));
1789    }
1790
1791    #[test]
1792    fn test_meta_analysis_token_stablecoin_unhealthy_peg() {
1793        let summary = report::TokenRiskSummary {
1794            score: 4,
1795            level: "Medium",
1796            emoji: "🟡",
1797            concerns: vec![],
1798            positives: vec![],
1799        };
1800        let meta = meta_analysis_token(&summary, true, Some(false), None, 500_000.0);
1801        assert!(meta.synthesis.contains("peg deviation"));
1802        assert!(meta.key_takeaway.contains("deviating from peg"));
1803        assert!(meta.recommendations.iter().any(|r| r.contains("peg")));
1804    }
1805
1806    #[test]
1807    fn test_meta_analysis_token_concentration_risk() {
1808        let summary = report::TokenRiskSummary {
1809            score: 5,
1810            level: "Medium",
1811            emoji: "🟡",
1812            concerns: vec![],
1813            positives: vec![],
1814        };
1815        let meta = meta_analysis_token(&summary, false, None, Some(45.0), 500_000.0);
1816        assert!(meta.synthesis.contains("Concentration risk"));
1817        assert!(
1818            meta.recommendations
1819                .iter()
1820                .any(|r| r.contains("top holder"))
1821        );
1822    }
1823
1824    #[test]
1825    fn test_meta_analysis_token_low_liquidity_low_risk() {
1826        let summary = report::TokenRiskSummary {
1827            score: 3,
1828            level: "Low",
1829            emoji: "🟢",
1830            concerns: vec![],
1831            positives: vec![],
1832        };
1833        let meta = meta_analysis_token(&summary, false, None, None, 50_000.0);
1834        assert!(
1835            meta.recommendations
1836                .iter()
1837                .any(|r| r.contains("limit orders") || r.contains("slippage"))
1838        );
1839    }
1840
1841    #[test]
1842    fn test_meta_analysis_token_stablecoin_no_peg_data() {
1843        let summary = report::TokenRiskSummary {
1844            score: 3,
1845            level: "Low",
1846            emoji: "🟢",
1847            concerns: vec![],
1848            positives: vec![],
1849        };
1850        let meta = meta_analysis_token(&summary, true, None, None, 1_000_000.0);
1851        // When peg_healthy is None, recommendation should still suggest verifying peg
1852        assert!(meta.recommendations.iter().any(|r| r.contains("peg")));
1853    }
1854
1855    #[test]
1856    fn test_meta_analysis_token_mixed_signals_key_takeaway() {
1857        let summary = report::TokenRiskSummary {
1858            score: 5,
1859            level: "Medium",
1860            emoji: "🟡",
1861            concerns: vec!["Some concern".to_string()],
1862            positives: vec!["Some positive".to_string()],
1863        };
1864        let meta = meta_analysis_token(&summary, false, None, None, 500_000.0);
1865        assert!(meta.key_takeaway.contains("Risk 5/10"));
1866        assert!(meta.key_takeaway.contains("Medium"));
1867    }
1868
1869    // ====================================================================
1870    // infer_target — additional edge cases
1871    // ====================================================================
1872
1873    #[test]
1874    fn test_infer_target_tx_hash_with_chain_override() {
1875        let t = infer_target(
1876            "0xabc123def456789012345678901234567890123456789012345678901234abcd",
1877            Some("polygon"),
1878        );
1879        assert!(matches!(t, InferredTarget::Transaction { chain } if chain == "polygon"));
1880    }
1881
1882    #[test]
1883    fn test_infer_target_whitespace_trimming() {
1884        let t = infer_target("  USDC  ", None);
1885        assert!(matches!(t, InferredTarget::Token { .. }));
1886    }
1887
1888    #[test]
1889    fn test_infer_target_long_token_name() {
1890        let t = infer_target("some-random-token-name", None);
1891        assert!(matches!(t, InferredTarget::Token { chain } if chain == "ethereum"));
1892    }
1893
1894    // ====================================================================
1895    // InsightsArgs struct validation
1896    // ====================================================================
1897
1898    #[test]
1899    fn test_insights_args_debug() {
1900        let args = InsightsArgs {
1901            target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
1902            chain: Some("ethereum".to_string()),
1903            decode: true,
1904            trace: false,
1905        };
1906        let debug_str = format!("{:?}", args);
1907        assert!(debug_str.contains("InsightsArgs"));
1908        assert!(debug_str.contains("0x742d"));
1909    }
1910
1911    // ====================================================================
1912    // Additional tests for classify_tx_type — selector matches and edge cases
1913    // ====================================================================
1914
1915    #[test]
1916    fn test_classify_tx_type_contract_creation() {
1917        assert_eq!(classify_tx_type("0xa9059cbb...", None), "Contract Creation");
1918    }
1919
1920    #[test]
1921    fn test_classify_tx_type_erc20_transfer() {
1922        assert_eq!(
1923            classify_tx_type("0xa9059cbb00000000", Some("0x1234")),
1924            "ERC-20 Transfer"
1925        );
1926    }
1927
1928    #[test]
1929    fn test_classify_tx_type_erc20_approve() {
1930        assert_eq!(
1931            classify_tx_type("0x095ea7b3...", Some("0x1234")),
1932            "ERC-20 Approve"
1933        );
1934    }
1935
1936    #[test]
1937    fn test_classify_tx_type_erc20_transfer_from() {
1938        assert_eq!(
1939            classify_tx_type("0x23b872dd...", Some("0x1234")),
1940            "ERC-20 Transfer From"
1941        );
1942    }
1943
1944    #[test]
1945    fn test_classify_tx_type_dex_swap() {
1946        assert_eq!(
1947            classify_tx_type("0x38ed1739...", Some("0x1234")),
1948            "DEX Swap"
1949        );
1950        assert_eq!(
1951            classify_tx_type("0x7ff36ab5...", Some("0x1234")),
1952            "DEX Swap"
1953        );
1954    }
1955
1956    #[test]
1957    fn test_classify_tx_type_native_transfer() {
1958        assert_eq!(classify_tx_type("0x", Some("0x1234")), "Native Transfer");
1959        assert_eq!(classify_tx_type("", Some("0x1234")), "Native Transfer");
1960    }
1961
1962    #[test]
1963    fn test_classify_tx_type_unknown_contract_call() {
1964        assert_eq!(
1965            classify_tx_type("0xdeadbeef12345678", Some("0x1234")),
1966            "Contract Call"
1967        );
1968    }
1969
1970    // ====================================================================
1971    // Additional tests for format_tx_value
1972    // ====================================================================
1973
1974    #[test]
1975    fn test_format_tx_value_ethereum_wei() {
1976        let (fmt, high) = format_tx_value("1000000000000000000", "ethereum");
1977        assert!(fmt.contains("1.000000"));
1978        assert!(fmt.contains("ETH"));
1979        assert!(!high); // 1 ETH < 10 threshold
1980    }
1981
1982    #[test]
1983    fn test_format_tx_value_hex() {
1984        let (fmt, _) = format_tx_value("0xde0b6b3a7640000", "ethereum");
1985        // 0xde0b6b3a7640000 = 10^18 = 1 ETH
1986        assert!(fmt.contains("ETH"));
1987    }
1988
1989    #[test]
1990    fn test_format_tx_value_high_value() {
1991        // 100 ETH in wei = 100000000000000000000
1992        let (_, high) = format_tx_value("100000000000000000000", "ethereum");
1993        assert!(high); // 100 ETH > 10
1994    }
1995
1996    #[test]
1997    fn test_format_tx_value_zero_decimal() {
1998        let (fmt, high) = format_tx_value("0", "ethereum");
1999        assert!(fmt.contains("0.000000"));
2000        assert!(!high);
2001    }
2002
2003    #[test]
2004    fn test_format_tx_value_solana_additional() {
2005        let (fmt, _) = format_tx_value("1000000000", "solana"); // 1 SOL
2006        assert!(fmt.contains("SOL"));
2007    }
2008
2009    #[test]
2010    fn test_format_tx_value_tron_additional() {
2011        let (fmt, _) = format_tx_value("1000000", "tron"); // 1 TRX
2012        assert!(fmt.contains("TRX"));
2013    }
2014
2015    #[test]
2016    fn test_format_tx_value_empty_hex_additional() {
2017        let (fmt, _) = format_tx_value("0x", "ethereum");
2018        assert!(fmt.contains("0.000000"));
2019    }
2020
2021    // ====================================================================
2022    // Combined tests for target_type_label and chain_label
2023    // ====================================================================
2024
2025    #[test]
2026    fn test_target_type_label_combined() {
2027        assert_eq!(
2028            target_type_label(&InferredTarget::Address {
2029                chain: "eth".to_string()
2030            }),
2031            "Address"
2032        );
2033        assert_eq!(
2034            target_type_label(&InferredTarget::Transaction {
2035                chain: "eth".to_string()
2036            }),
2037            "Transaction"
2038        );
2039        assert_eq!(
2040            target_type_label(&InferredTarget::Token {
2041                chain: "eth".to_string()
2042            }),
2043            "Token"
2044        );
2045    }
2046
2047    #[test]
2048    fn test_chain_label_combined() {
2049        assert_eq!(
2050            chain_label(&InferredTarget::Address {
2051                chain: "ethereum".to_string()
2052            }),
2053            "ethereum"
2054        );
2055        assert_eq!(
2056            chain_label(&InferredTarget::Transaction {
2057                chain: "polygon".to_string()
2058            }),
2059            "polygon"
2060        );
2061        assert_eq!(
2062            chain_label(&InferredTarget::Token {
2063                chain: "solana".to_string()
2064            }),
2065            "solana"
2066        );
2067    }
2068
2069    // ====================================================================
2070    // Additional tests for meta_analysis_address
2071    // ====================================================================
2072
2073    #[test]
2074    fn test_meta_analysis_address_contract_high_risk() {
2075        use crate::compliance::risk::RiskLevel;
2076        let meta = meta_analysis_address(
2077            true,
2078            Some(2_000_000.0),
2079            10,
2080            Some(8.0),
2081            Some(&RiskLevel::High),
2082        );
2083        assert!(meta.synthesis.contains("contract"));
2084        assert!(meta.synthesis.contains("Significant value"));
2085        assert!(meta.key_takeaway.contains("scrutiny"));
2086        assert!(!meta.recommendations.is_empty());
2087    }
2088
2089    #[test]
2090    fn test_meta_analysis_address_wallet_low_risk() {
2091        use crate::compliance::risk::RiskLevel;
2092        let meta = meta_analysis_address(false, Some(0.5), 0, Some(2.0), Some(&RiskLevel::Low));
2093        assert!(meta.synthesis.contains("wallet"));
2094        assert!(meta.synthesis.contains("Minimal value"));
2095    }
2096
2097    #[test]
2098    fn test_meta_analysis_address_no_risk_data() {
2099        let meta = meta_analysis_address(false, None, 0, None, None);
2100        assert!(!meta.synthesis.is_empty());
2101        assert!(meta.key_takeaway.contains("Review full report"));
2102    }
2103
2104    // ====================================================================
2105    // Additional tests for meta_analysis_tx
2106    // ====================================================================
2107
2108    #[test]
2109    fn test_meta_analysis_tx_failed_additional() {
2110        let meta = meta_analysis_tx("Contract Call", false, false, "0x...", Some("0x..."));
2111        assert!(meta.synthesis.contains("failed"));
2112        assert!(meta.key_takeaway.contains("Failed"));
2113    }
2114
2115    #[test]
2116    fn test_meta_analysis_tx_high_value_native_additional() {
2117        let meta = meta_analysis_tx("Native Transfer", true, true, "0x...", Some("0x..."));
2118        assert!(meta.synthesis.contains("High-value"));
2119        assert!(meta.key_takeaway.contains("Large native transfer"));
2120    }
2121
2122    #[test]
2123    fn test_meta_analysis_tx_routine() {
2124        let meta = meta_analysis_tx("ERC-20 Transfer", true, false, "0x...", Some("0x..."));
2125        assert!(meta.key_takeaway.contains("Routine"));
2126    }
2127
2128    // ====================================================================
2129    // Additional tests for meta_analysis_token
2130    // ====================================================================
2131
2132    #[test]
2133    fn test_meta_analysis_token_high_risk_additional() {
2134        let risk = report::TokenRiskSummary {
2135            score: 8,
2136            level: "High",
2137            emoji: "🔴",
2138            concerns: vec!["Low liquidity".to_string()],
2139            positives: vec![],
2140        };
2141        let meta = meta_analysis_token(&risk, false, None, None, 10_000.0);
2142        assert!(meta.synthesis.contains("Elevated risk"));
2143        assert!(meta.key_takeaway.contains("High risk"));
2144    }
2145
2146    #[test]
2147    fn test_meta_analysis_token_stablecoin_peg_healthy() {
2148        let risk = report::TokenRiskSummary {
2149            score: 2,
2150            level: "Low",
2151            emoji: "🟢",
2152            concerns: vec![],
2153            positives: vec!["Strong liquidity".to_string()],
2154        };
2155        let meta = meta_analysis_token(&risk, true, Some(true), Some(5.0), 5_000_000.0);
2156        assert!(meta.synthesis.contains("peg is healthy"));
2157        assert!(meta.synthesis.contains("Strong liquidity"));
2158    }
2159
2160    #[test]
2161    fn test_meta_analysis_token_stablecoin_peg_unhealthy() {
2162        let risk = report::TokenRiskSummary {
2163            score: 5,
2164            level: "Medium",
2165            emoji: "🟡",
2166            concerns: vec!["Peg deviation".to_string()],
2167            positives: vec![],
2168        };
2169        let meta = meta_analysis_token(&risk, true, Some(false), Some(40.0), 100_000.0);
2170        assert!(meta.synthesis.contains("peg deviation"));
2171        assert!(meta.synthesis.contains("Concentration risk"));
2172    }
2173
2174    #[test]
2175    fn test_meta_analysis_token_high_risk_empty_concerns_uses_multiple_factors() {
2176        let risk = report::TokenRiskSummary {
2177            score: 8,
2178            level: "High",
2179            emoji: "🔴",
2180            concerns: vec![],
2181            positives: vec![],
2182        };
2183        let meta = meta_analysis_token(&risk, false, None, None, 50_000.0);
2184        assert!(
2185            meta.key_takeaway.contains("multiple factors")
2186                || meta.key_takeaway.contains("High risk")
2187        );
2188    }
2189
2190    #[test]
2191    fn test_infer_target_chain_override_with_address() {
2192        let t = infer_target(
2193            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
2194            Some("arbitrum"),
2195        );
2196        assert!(matches!(t, InferredTarget::Address { chain } if chain == "arbitrum"));
2197    }
2198
2199    #[test]
2200    fn test_meta_analysis_tx_approval_contains_approval_in_match() {
2201        let meta = meta_analysis_tx("ERC-20 Approval", true, false, "0xfrom", Some("0xto"));
2202        assert!(
2203            meta.recommendations
2204                .iter()
2205                .any(|r| r.contains("spender") || r.contains("allowance"))
2206        );
2207    }
2208}