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