1use 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#[derive(Debug, Clone)]
21pub enum InferredTarget {
22 Address { chain: String },
24 Transaction { chain: String },
26 Token { chain: String },
28}
29
30#[derive(Debug, Args)]
32pub struct InsightsArgs {
33 pub target: String,
44
45 #[arg(short, long)]
47 pub chain: Option<String>,
48
49 #[arg(long)]
51 pub decode: bool,
52
53 #[arg(long)]
55 pub trace: bool,
56}
57
58pub 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 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 if let Some(chain) = infer_chain_from_hash(trimmed) {
76 return InferredTarget::Transaction {
77 chain: chain.to_string(),
78 };
79 }
80
81 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 InferredTarget::Token {
91 chain: "ethereum".to_string(),
92 }
93}
94
95pub 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 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 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 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 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 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 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 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 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
429fn 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
452fn 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 let high_value = human > 10.0;
476 (formatted, high_value)
477}
478
479fn 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
487struct 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 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()) }
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 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, 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), })
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()) }
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 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 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 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, 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 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 #[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 #[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"); assert!(fmt.contains("1.0") && fmt.contains("ETH"));
1263 assert!(!high);
1264 let (_, high2) = format_tx_value("0x52B7D2DCC80CD2E4000000", "ethereum"); 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 #[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 #[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 #[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"); 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"); 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"); 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 let (_, high) = format_tx_value("11000000000000000000", "ethereum"); assert!(high);
1470 let (_, high2) = format_tx_value("10000000000000000000", "ethereum"); assert!(!high2); }
1473
1474 #[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 #[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 #[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 assert!(meta.recommendations.iter().any(|r| r.contains("peg")));
1737 }
1738
1739 #[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 #[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}