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