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 = fetch_analytics_for_input(
304 &args.target,
305 chain,
306 Period::Hour24,
307 10,
308 clients,
309 Some(&sp),
310 )
311 .await?;
312
313 let risk_summary = report::token_risk_summary(&analytics);
315 output.push_str(&format!(
316 "- **Risk:** {} {}/10 ({})\n",
317 risk_summary.emoji, risk_summary.score, risk_summary.level
318 ));
319 if !risk_summary.concerns.is_empty() {
320 for c in &risk_summary.concerns {
321 output.push_str(&format!("- ⚠️ {}\n", c));
322 }
323 }
324 if !risk_summary.positives.is_empty() {
325 for p in &risk_summary.positives {
326 output.push_str(&format!("- ✅ {}\n", p));
327 }
328 }
329
330 output.push_str(&format!(
331 "- **Token:** {} ({})\n",
332 analytics.token.symbol, analytics.token.name
333 ));
334 output.push_str(&format!(
335 "- **Address:** `{}`\n",
336 analytics.token.contract_address
337 ));
338 output.push_str(&format!("- **Price:** ${:.6}\n", analytics.price_usd));
339 output.push_str(&format!(
340 "- **Liquidity (24h):** ${}\n",
341 crate::display::format_usd(analytics.liquidity_usd)
342 ));
343 output.push_str(&format!(
344 "- **Volume (24h):** ${}\n",
345 crate::display::format_usd(analytics.volume_24h)
346 ));
347
348 if let Some(top) = analytics.holders.first() {
350 output.push_str(&format!(
351 "- **Top holder:** `{}` ({:.1}%)\n",
352 top.address, top.percentage
353 ));
354 if top.percentage > 30.0 {
355 output.push_str(" - ⚠️ High concentration risk\n");
356 }
357 }
358 output.push_str(&format!(
359 "- **Holders displayed:** {}\n",
360 analytics.holders.len()
361 ));
362
363 let mut peg_healthy: Option<bool> = None;
365 if is_stablecoin(&analytics.token.symbol)
366 && let Ok(registry) = VenueRegistry::load()
367 {
368 let venue_id = if registry.contains("binance") {
370 "binance"
371 } else {
372 registry.list().first().copied().unwrap_or("binance")
373 };
374 if let Ok(exchange) = registry.create_exchange_client(venue_id) {
375 let pair = exchange.format_pair(&analytics.token.symbol);
376 if let Ok(book) = exchange.fetch_order_book(&pair).await {
377 let thresholds = HealthThresholds {
378 peg_target: 1.0,
379 peg_range: 0.001,
380 min_levels: 6,
381 min_depth: 3000.0,
382 min_bid_ask_ratio: 0.2,
383 max_bid_ask_ratio: 5.0,
384 };
385 let volume_24h = if exchange.has_ticker() {
386 exchange
387 .fetch_ticker(&pair)
388 .await
389 .ok()
390 .and_then(|t| t.quote_volume_24h.or(t.volume_24h))
391 } else {
392 None
393 };
394 let summary =
395 MarketSummary::from_order_book(&book, 1.0, &thresholds, volume_24h);
396 let deviation_bps = summary
397 .mid_price
398 .map(|m| (m - 1.0) * 10_000.0)
399 .unwrap_or(0.0);
400 peg_healthy = Some(deviation_bps.abs() < 10.0);
401 let peg_status = if peg_healthy.unwrap_or(false) {
402 "Peg healthy"
403 } else if deviation_bps.abs() < 50.0 {
404 "Slight peg deviation"
405 } else {
406 "Peg deviation"
407 };
408 output.push_str(&format!(
409 "- **Market ({} {}):** {} (deviation: {:.1} bps)\n",
410 exchange.venue_name(),
411 pair,
412 peg_status,
413 deviation_bps
414 ));
415 }
416 }
417 }
418
419 let top_holder_pct = analytics.holders.first().map(|h| h.percentage);
421 let meta = meta_analysis_token(
422 &risk_summary,
423 is_stablecoin(&analytics.token.symbol),
424 peg_healthy,
425 top_holder_pct,
426 analytics.liquidity_usd,
427 );
428 output.push_str("\n### Synthesis\n\n");
429 output.push_str(&format!("{}\n\n", meta.synthesis));
430 output.push_str(&format!("**Key takeaway:** {}\n\n", meta.key_takeaway));
431 if !meta.recommendations.is_empty() {
432 output.push_str("**Consider:**\n");
433 for rec in &meta.recommendations {
434 output.push_str(&format!("- {}\n", rec));
435 }
436 }
437 output.push_str("\n---\n\n");
438 output.push_str(&report::generate_report(&analytics));
439 }
440 }
441
442 sp.finish("Insights complete.");
443 println!("{}", output);
444 Ok(())
445}
446
447fn target_type_label(target: &InferredTarget) -> &'static str {
448 match target {
449 InferredTarget::Address { .. } => "Address",
450 InferredTarget::Transaction { .. } => "Transaction",
451 InferredTarget::Token { .. } => "Token",
452 }
453}
454
455fn chain_label(target: &InferredTarget) -> &str {
456 match target {
457 InferredTarget::Address { chain } => chain,
458 InferredTarget::Transaction { chain } => chain,
459 InferredTarget::Token { chain } => chain,
460 }
461}
462
463fn classify_tx_type(input: &str, to: Option<&str>) -> &'static str {
465 if to.is_none() {
466 return "Contract Creation";
467 }
468 let selector = input
469 .trim_start_matches("0x")
470 .chars()
471 .take(8)
472 .collect::<String>();
473 let sel = selector.to_lowercase();
474 match sel.as_str() {
475 "a9059cbb" => "ERC-20 Transfer",
476 "095ea7b3" => "ERC-20 Approve",
477 "23b872dd" => "ERC-20 Transfer From",
478 "38ed1739" | "5c11d795" | "4a25d94a" | "8803dbee" | "7ff36ab5" | "18cbafe5"
479 | "fb3bdb41" | "b6f9de95" => "DEX Swap",
480 "ac9650d8" | "5ae401dc" => "Multicall",
481 _ if input.is_empty() || input == "0x" => "Native Transfer",
482 _ => "Contract Call",
483 }
484}
485
486fn format_tx_value(value_str: &str, chain: &str) -> (String, bool) {
488 let wei: u128 = if value_str.starts_with("0x") {
489 let hex_part = value_str.trim_start_matches("0x");
490 if hex_part.is_empty() {
491 0
492 } else {
493 u128::from_str_radix(hex_part, 16).unwrap_or(0)
494 }
495 } else {
496 value_str.parse().unwrap_or(0)
497 };
498 let decimals = match chain.to_lowercase().as_str() {
499 "ethereum" | "polygon" | "arbitrum" | "optimism" | "base" | "bsc" | "aegis" => 18,
500 "solana" => 9,
501 "tron" => 6,
502 _ => 18,
503 };
504 let divisor = 10_f64.powi(decimals);
505 let human = wei as f64 / divisor;
506 let symbol = native_symbol(chain);
507 let formatted = format!("≈ {:.6} {}", human, symbol);
508 let high_value = human > 10.0;
510 (formatted, high_value)
511}
512
513fn is_stablecoin(symbol: &str) -> bool {
515 matches!(
516 symbol.to_uppercase().as_str(),
517 "USDC" | "USDT" | "DAI" | "BUSD" | "TUSD" | "USDP" | "FRAX" | "LUSD" | "PUSD" | "GUSD"
518 )
519}
520
521struct MetaAnalysis {
523 synthesis: String,
524 key_takeaway: String,
525 recommendations: Vec<String>,
526}
527
528fn meta_analysis_address(
529 is_contract: bool,
530 usd_value: Option<f64>,
531 token_count: usize,
532 risk_score: Option<f32>,
533 risk_level: Option<&crate::compliance::risk::RiskLevel>,
534) -> MetaAnalysis {
535 let mut synthesis_parts = Vec::new();
536 let profile = if is_contract {
537 "contract"
538 } else {
539 "wallet (EOA)"
540 };
541 synthesis_parts.push(format!("A {} on chain.", profile));
542
543 if let Some(usd) = usd_value {
544 if usd > 1_000_000.0 {
545 synthesis_parts.push("Significant value held.".to_string());
546 } else if usd > 10_000.0 {
547 synthesis_parts.push("Moderate value.".to_string());
548 } else if usd < 1.0 {
549 synthesis_parts.push("Minimal value.".to_string());
550 }
551 }
552
553 if token_count > 5 {
554 synthesis_parts.push("Diversified token exposure.".to_string());
555 } else if token_count == 1 && token_count > 0 {
556 synthesis_parts.push("Concentrated in a single token.".to_string());
557 }
558
559 if let (Some(score), Some(level)) = (risk_score, risk_level) {
560 if score >= 7.0 {
561 synthesis_parts.push(format!("Elevated risk ({:?}).", level));
562 } else if score <= 3.0 {
563 synthesis_parts.push("Low risk profile.".to_string());
564 }
565 }
566
567 let synthesis = if synthesis_parts.is_empty() {
568 "Address analyzed with available on-chain data.".to_string()
569 } else {
570 synthesis_parts.join(" ")
571 };
572
573 let key_takeaway = if let (Some(score), Some(level)) = (risk_score, risk_level) {
574 if score >= 7.0 {
575 format!(
576 "Risk assessment warrants closer scrutiny ({:.1}/10).",
577 score
578 )
579 } else {
580 format!("Overall risk: {:?} ({:.1}/10).", level, score)
581 }
582 } else if is_contract {
583 "Contract address — verify intended interaction before use.".to_string()
584 } else if usd_value.map(|u| u > 100_000.0).unwrap_or(false) {
585 "High-value wallet — standard due diligence applies.".to_string()
586 } else {
587 "Review full report for transaction and token details.".to_string()
588 };
589
590 let mut recommendations = Vec::new();
591 if risk_score.map(|s| s >= 6.0).unwrap_or(false) {
592 recommendations.push("Monitor for unusual transaction patterns.".to_string());
593 }
594 if token_count > 0 {
595 recommendations.push("Verify token contracts before large interactions.".to_string());
596 }
597 if is_contract {
598 recommendations.push("Confirm contract source and audit status.".to_string());
599 }
600
601 MetaAnalysis {
602 synthesis,
603 key_takeaway,
604 recommendations,
605 }
606}
607
608fn meta_analysis_tx(
609 tx_type: &str,
610 status: bool,
611 high_value: bool,
612 _from: &str,
613 _to: Option<&str>,
614) -> MetaAnalysis {
615 let mut synthesis_parts = Vec::new();
616
617 if !status {
618 synthesis_parts.push("Transaction failed.".to_string());
619 }
620
621 synthesis_parts.push(format!("{} between parties.", tx_type));
622
623 if high_value {
624 synthesis_parts.push("High-value transfer.".to_string());
625 }
626
627 let synthesis = synthesis_parts.join(" ");
628
629 let key_takeaway = if !status {
630 "Failed transaction — check revert reason and contract state.".to_string()
631 } else if high_value && tx_type == "Native Transfer" {
632 "Large native transfer — verify recipient and intent.".to_string()
633 } else if high_value {
634 "High-value operation — standard verification recommended.".to_string()
635 } else {
636 format!("Routine {} — review full details if needed.", tx_type)
637 };
638
639 let mut recommendations = Vec::new();
640 if !status {
641 recommendations.push("Inspect contract logs for revert reason.".to_string());
642 }
643 if high_value {
644 recommendations.push("Confirm recipient address and amount.".to_string());
645 }
646 if tx_type.contains("Approval") {
647 recommendations.push("Verify approved spender and allowance amount.".to_string());
648 }
649
650 MetaAnalysis {
651 synthesis,
652 key_takeaway,
653 recommendations,
654 }
655}
656
657fn meta_analysis_token(
658 risk_summary: &report::TokenRiskSummary,
659 is_stablecoin: bool,
660 peg_healthy: Option<bool>,
661 top_holder_pct: Option<f64>,
662 liquidity_usd: f64,
663) -> MetaAnalysis {
664 let mut synthesis_parts = Vec::new();
665
666 if risk_summary.score <= 3 {
667 synthesis_parts.push("Low-risk token with healthy metrics.".to_string());
668 } else if risk_summary.score >= 7 {
669 synthesis_parts.push("Elevated risk — multiple concerns identified.".to_string());
670 } else {
671 synthesis_parts.push("Moderate risk — mixed signals.".to_string());
672 }
673
674 if is_stablecoin && let Some(healthy) = peg_healthy {
675 if healthy {
676 synthesis_parts.push("Stablecoin peg is healthy on observed venue.".to_string());
677 } else {
678 synthesis_parts
679 .push("Stablecoin peg deviation detected — verify on multiple venues.".to_string());
680 }
681 }
682
683 if top_holder_pct.map(|p| p > 30.0).unwrap_or(false) {
684 synthesis_parts.push("Concentration risk: top holder holds significant share.".to_string());
685 }
686
687 if liquidity_usd > 1_000_000.0 {
688 synthesis_parts.push("Strong liquidity depth.".to_string());
689 } else if liquidity_usd < 50_000.0 {
690 synthesis_parts.push("Limited liquidity — slippage risk for larger trades.".to_string());
691 }
692
693 let synthesis = synthesis_parts.join(" ");
694
695 let key_takeaway = if risk_summary.score >= 7 {
696 format!(
697 "High risk ({}): {} — exercise caution.",
698 risk_summary.score,
699 risk_summary
700 .concerns
701 .first()
702 .cloned()
703 .unwrap_or_else(|| "multiple factors".to_string())
704 )
705 } else if is_stablecoin && peg_healthy == Some(false) {
706 "Stablecoin deviating from peg — check additional venues before trading.".to_string()
707 } else if !risk_summary.positives.is_empty() && risk_summary.concerns.is_empty() {
708 "Favorable risk profile — standard diligence applies.".to_string()
709 } else {
710 format!(
711 "Risk {}/10 ({}) — weigh concerns against use case.",
712 risk_summary.score, risk_summary.level
713 )
714 };
715
716 let mut recommendations = Vec::new();
717 if risk_summary.score >= 6 {
718 recommendations
719 .push("Consider smaller position sizes or avoid until risk clears.".to_string());
720 }
721 if top_holder_pct.map(|p| p > 25.0).unwrap_or(false) {
722 recommendations.push("Monitor top holder movements for distribution changes.".to_string());
723 }
724 if is_stablecoin && peg_healthy != Some(true) {
725 recommendations.push("Verify peg across multiple DEX/CEX venues.".to_string());
726 }
727 if liquidity_usd < 100_000.0 && risk_summary.score <= 5 {
728 recommendations.push("Use limit orders or split trades to manage slippage.".to_string());
729 }
730
731 MetaAnalysis {
732 synthesis,
733 key_takeaway,
734 recommendations,
735 }
736}
737
738#[cfg(test)]
739mod tests {
740 use super::*;
741 use crate::chains::{
742 Balance as ChainBalance, ChainClient, ChainClientFactory, DexDataSource,
743 Token as ChainToken, TokenBalance as ChainTokenBalance, Transaction as ChainTransaction,
744 };
745 use async_trait::async_trait;
746
747 struct MockChainClient;
752
753 #[async_trait]
754 impl ChainClient for MockChainClient {
755 fn chain_name(&self) -> &str {
756 "ethereum"
757 }
758 fn native_token_symbol(&self) -> &str {
759 "ETH"
760 }
761 async fn get_balance(&self, _address: &str) -> crate::error::Result<ChainBalance> {
762 Ok(ChainBalance {
763 raw: "1000000000000000000".to_string(),
764 formatted: "1.0 ETH".to_string(),
765 decimals: 18,
766 symbol: "ETH".to_string(),
767 usd_value: Some(2500.0),
768 })
769 }
770 async fn enrich_balance_usd(&self, balance: &mut ChainBalance) {
771 balance.usd_value = Some(2500.0);
772 }
773 async fn get_transaction(&self, _hash: &str) -> crate::error::Result<ChainTransaction> {
774 Ok(ChainTransaction {
775 hash: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
776 .to_string(),
777 block_number: Some(12345678),
778 timestamp: Some(1700000000),
779 from: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
780 to: Some("0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string()),
781 value: "1000000000000000000".to_string(),
782 gas_limit: 21000,
783 gas_used: Some(21000),
784 gas_price: "20000000000".to_string(),
785 nonce: 42,
786 input: "0xa9059cbb0000000000000000000000001234".to_string(),
787 status: Some(true),
788 })
789 }
790 async fn get_transactions(
791 &self,
792 _address: &str,
793 _limit: u32,
794 ) -> crate::error::Result<Vec<ChainTransaction>> {
795 Ok(vec![])
796 }
797 async fn get_block_number(&self) -> crate::error::Result<u64> {
798 Ok(12345678)
799 }
800 async fn get_token_balances(
801 &self,
802 _address: &str,
803 ) -> crate::error::Result<Vec<ChainTokenBalance>> {
804 Ok(vec![
805 ChainTokenBalance {
806 token: ChainToken {
807 contract_address: "0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string(),
808 symbol: "USDT".to_string(),
809 name: "Tether USD".to_string(),
810 decimals: 6,
811 },
812 balance: "1000000".to_string(),
813 formatted_balance: "1.0".to_string(),
814 usd_value: Some(1.0),
815 },
816 ChainTokenBalance {
817 token: ChainToken {
818 contract_address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string(),
819 symbol: "USDC".to_string(),
820 name: "USD Coin".to_string(),
821 decimals: 6,
822 },
823 balance: "5000000".to_string(),
824 formatted_balance: "5.0".to_string(),
825 usd_value: Some(5.0),
826 },
827 ])
828 }
829 async fn get_code(&self, _address: &str) -> crate::error::Result<String> {
830 Ok("0x".to_string()) }
832 }
833
834 struct MockFactory;
835
836 impl ChainClientFactory for MockFactory {
837 fn create_chain_client(&self, _chain: &str) -> crate::error::Result<Box<dyn ChainClient>> {
838 Ok(Box::new(MockChainClient))
839 }
840 fn create_dex_client(&self) -> Box<dyn DexDataSource> {
841 crate::chains::DefaultClientFactory {
842 chains_config: Default::default(),
843 }
844 .create_dex_client()
845 }
846 }
847
848 struct MockContractClient;
850
851 #[async_trait]
852 impl ChainClient for MockContractClient {
853 fn chain_name(&self) -> &str {
854 "ethereum"
855 }
856 fn native_token_symbol(&self) -> &str {
857 "ETH"
858 }
859 async fn get_balance(&self, _address: &str) -> crate::error::Result<ChainBalance> {
860 Ok(ChainBalance {
861 raw: "0".to_string(),
862 formatted: "0.0 ETH".to_string(),
863 decimals: 18,
864 symbol: "ETH".to_string(),
865 usd_value: Some(0.0),
866 })
867 }
868 async fn enrich_balance_usd(&self, _balance: &mut ChainBalance) {}
869 async fn get_transaction(&self, hash: &str) -> crate::error::Result<ChainTransaction> {
870 Ok(ChainTransaction {
871 hash: hash.to_string(),
872 block_number: Some(100),
873 timestamp: Some(1700000000),
874 from: "0xfrom".to_string(),
875 to: None, value: "0".to_string(),
877 gas_limit: 100000,
878 gas_used: Some(80000),
879 gas_price: "10000000000".to_string(),
880 nonce: 0,
881 input: "0x60806040".to_string(),
882 status: Some(false), })
884 }
885 async fn get_transactions(
886 &self,
887 _address: &str,
888 _limit: u32,
889 ) -> crate::error::Result<Vec<ChainTransaction>> {
890 Ok(vec![])
891 }
892 async fn get_block_number(&self) -> crate::error::Result<u64> {
893 Ok(100)
894 }
895 async fn get_token_balances(
896 &self,
897 _address: &str,
898 ) -> crate::error::Result<Vec<ChainTokenBalance>> {
899 Ok(vec![])
900 }
901 async fn get_code(&self, _address: &str) -> crate::error::Result<String> {
902 Ok("0x6080604052".to_string()) }
904 }
905
906 struct MockContractFactory;
907
908 impl ChainClientFactory for MockContractFactory {
909 fn create_chain_client(&self, _chain: &str) -> crate::error::Result<Box<dyn ChainClient>> {
910 Ok(Box::new(MockContractClient))
911 }
912 fn create_dex_client(&self) -> Box<dyn DexDataSource> {
913 crate::chains::DefaultClientFactory {
914 chains_config: Default::default(),
915 }
916 .create_dex_client()
917 }
918 }
919
920 struct MockDexDataSource;
922
923 #[async_trait]
924 impl DexDataSource for MockDexDataSource {
925 async fn get_token_price(&self, _chain: &str, _address: &str) -> Option<f64> {
926 Some(1.0)
927 }
928
929 async fn get_native_token_price(&self, _chain: &str) -> Option<f64> {
930 Some(2500.0)
931 }
932
933 async fn get_token_data(
934 &self,
935 _chain: &str,
936 address: &str,
937 ) -> crate::error::Result<crate::chains::dex::DexTokenData> {
938 use crate::chains::{DexPair, PricePoint, VolumePoint};
939 Ok(crate::chains::dex::DexTokenData {
940 address: address.to_string(),
941 symbol: "TEST".to_string(),
942 name: "Test Token".to_string(),
943 price_usd: 1.5,
944 price_change_24h: 5.2,
945 price_change_6h: 2.1,
946 price_change_1h: 0.5,
947 price_change_5m: 0.1,
948 volume_24h: 1_000_000.0,
949 volume_6h: 250_000.0,
950 volume_1h: 50_000.0,
951 liquidity_usd: 500_000.0,
952 market_cap: Some(10_000_000.0),
953 fdv: Some(12_000_000.0),
954 pairs: vec![DexPair {
955 dex_name: "Uniswap V3".to_string(),
956 pair_address: "0xpair123".to_string(),
957 base_token: "TEST".to_string(),
958 quote_token: "USDC".to_string(),
959 price_usd: 1.5,
960 liquidity_usd: 500_000.0,
961 volume_24h: 1_000_000.0,
962 price_change_24h: 5.2,
963 buys_24h: 100,
964 sells_24h: 80,
965 buys_6h: 20,
966 sells_6h: 15,
967 buys_1h: 5,
968 sells_1h: 3,
969 pair_created_at: Some(1690000000),
970 url: Some("https://dexscreener.com/ethereum/0xpair123".to_string()),
971 }],
972 price_history: vec![PricePoint {
973 timestamp: 1690000000,
974 price: 1.5,
975 }],
976 volume_history: vec![VolumePoint {
977 timestamp: 1690000000,
978 volume: 1_000_000.0,
979 }],
980 total_buys_24h: 100,
981 total_sells_24h: 80,
982 total_buys_6h: 20,
983 total_sells_6h: 15,
984 total_buys_1h: 5,
985 total_sells_1h: 3,
986 earliest_pair_created_at: Some(1690000000),
987 image_url: None,
988 websites: Vec::new(),
989 socials: Vec::new(),
990 dexscreener_url: Some("https://dexscreener.com/ethereum/test".to_string()),
991 })
992 }
993
994 async fn search_tokens(
995 &self,
996 _query: &str,
997 _chain: Option<&str>,
998 ) -> crate::error::Result<Vec<crate::chains::TokenSearchResult>> {
999 Ok(vec![crate::chains::TokenSearchResult {
1000 address: "0xTEST1234567890123456789012345678901234567".to_string(),
1001 symbol: "TEST".to_string(),
1002 name: "Test Token".to_string(),
1003 chain: "ethereum".to_string(),
1004 price_usd: Some(1.5),
1005 volume_24h: 1_000_000.0,
1006 liquidity_usd: 500_000.0,
1007 market_cap: Some(10_000_000.0),
1008 }])
1009 }
1010 }
1011
1012 struct MockTokenChainClient;
1014
1015 #[async_trait]
1016 impl ChainClient for MockTokenChainClient {
1017 fn chain_name(&self) -> &str {
1018 "ethereum"
1019 }
1020 fn native_token_symbol(&self) -> &str {
1021 "ETH"
1022 }
1023 async fn get_balance(&self, _address: &str) -> crate::error::Result<ChainBalance> {
1024 Ok(ChainBalance {
1025 raw: "1000000000000000000".to_string(),
1026 formatted: "1.0 ETH".to_string(),
1027 decimals: 18,
1028 symbol: "ETH".to_string(),
1029 usd_value: Some(2500.0),
1030 })
1031 }
1032 async fn enrich_balance_usd(&self, balance: &mut ChainBalance) {
1033 balance.usd_value = Some(2500.0);
1034 }
1035 async fn get_transaction(&self, _hash: &str) -> crate::error::Result<ChainTransaction> {
1036 Ok(ChainTransaction {
1037 hash: "0xabc123".to_string(),
1038 block_number: Some(12345678),
1039 timestamp: Some(1700000000),
1040 from: "0xfrom".to_string(),
1041 to: Some("0xto".to_string()),
1042 value: "0".to_string(),
1043 gas_limit: 21000,
1044 gas_used: Some(21000),
1045 gas_price: "20000000000".to_string(),
1046 nonce: 42,
1047 input: "0x".to_string(),
1048 status: Some(true),
1049 })
1050 }
1051 async fn get_transactions(
1052 &self,
1053 _address: &str,
1054 _limit: u32,
1055 ) -> crate::error::Result<Vec<ChainTransaction>> {
1056 Ok(vec![])
1057 }
1058 async fn get_block_number(&self) -> crate::error::Result<u64> {
1059 Ok(12345678)
1060 }
1061 async fn get_token_balances(
1062 &self,
1063 _address: &str,
1064 ) -> crate::error::Result<Vec<ChainTokenBalance>> {
1065 Ok(vec![])
1066 }
1067 async fn get_code(&self, _address: &str) -> crate::error::Result<String> {
1068 Ok("0x".to_string())
1069 }
1070 async fn get_token_holders(
1071 &self,
1072 _address: &str,
1073 _limit: u32,
1074 ) -> crate::error::Result<Vec<crate::chains::TokenHolder>> {
1075 Ok(vec![
1077 crate::chains::TokenHolder {
1078 address: "0x1111111111111111111111111111111111111111".to_string(),
1079 balance: "3500000000000000000000000".to_string(),
1080 formatted_balance: "3500000.0".to_string(),
1081 percentage: 35.0, rank: 1,
1083 },
1084 crate::chains::TokenHolder {
1085 address: "0x2222222222222222222222222222222222222222".to_string(),
1086 balance: "1500000000000000000000000".to_string(),
1087 formatted_balance: "1500000.0".to_string(),
1088 percentage: 15.0,
1089 rank: 2,
1090 },
1091 crate::chains::TokenHolder {
1092 address: "0x3333333333333333333333333333333333333333".to_string(),
1093 balance: "1000000000000000000000000".to_string(),
1094 formatted_balance: "1000000.0".to_string(),
1095 percentage: 10.0,
1096 rank: 3,
1097 },
1098 ])
1099 }
1100 }
1101
1102 struct MockTokenFactory;
1104
1105 impl ChainClientFactory for MockTokenFactory {
1106 fn create_chain_client(&self, _chain: &str) -> crate::error::Result<Box<dyn ChainClient>> {
1107 Ok(Box::new(MockTokenChainClient))
1108 }
1109 fn create_dex_client(&self) -> Box<dyn DexDataSource> {
1110 Box::new(MockDexDataSource)
1111 }
1112 }
1113
1114 #[tokio::test]
1119 async fn test_run_address_eoa() {
1120 let config = Config::default();
1121 let factory = MockFactory;
1122 let args = InsightsArgs {
1123 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
1124 chain: None,
1125 decode: false,
1126 trace: false,
1127 };
1128 let result = run(args, &config, &factory).await;
1129 assert!(result.is_ok());
1130 }
1131
1132 #[tokio::test]
1133 async fn test_run_address_contract() {
1134 let config = Config::default();
1135 let factory = MockContractFactory;
1136 let args = InsightsArgs {
1137 target: "0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string(),
1138 chain: None,
1139 decode: false,
1140 trace: false,
1141 };
1142 let result = run(args, &config, &factory).await;
1143 assert!(result.is_ok());
1144 }
1145
1146 #[tokio::test]
1147 async fn test_run_transaction() {
1148 let config = Config::default();
1149 let factory = MockFactory;
1150 let args = InsightsArgs {
1151 target: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
1152 .to_string(),
1153 chain: None,
1154 decode: false,
1155 trace: false,
1156 };
1157 let result = run(args, &config, &factory).await;
1158 assert!(result.is_ok());
1159 }
1160
1161 #[tokio::test]
1162 async fn test_run_transaction_failed() {
1163 let config = Config::default();
1164 let factory = MockContractFactory;
1165 let args = InsightsArgs {
1166 target: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
1167 .to_string(),
1168 chain: Some("ethereum".to_string()),
1169 decode: true,
1170 trace: false,
1171 };
1172 let result = run(args, &config, &factory).await;
1173 assert!(result.is_ok());
1174 }
1175
1176 #[tokio::test]
1177 async fn test_run_address_with_chain_override() {
1178 let config = Config::default();
1179 let factory = MockFactory;
1180 let args = InsightsArgs {
1181 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
1182 chain: Some("polygon".to_string()),
1183 decode: false,
1184 trace: false,
1185 };
1186 let result = run(args, &config, &factory).await;
1187 assert!(result.is_ok());
1188 }
1189
1190 #[tokio::test]
1191 async fn test_insights_run_token() {
1192 let config = Config::default();
1193 let factory = MockTokenFactory;
1194 let args = InsightsArgs {
1195 target: "TEST".to_string(),
1196 chain: Some("ethereum".to_string()),
1197 decode: false,
1198 trace: false,
1199 };
1200 let result = run(args, &config, &factory).await;
1201 assert!(result.is_ok());
1202 }
1203
1204 #[tokio::test]
1205 async fn test_insights_run_token_with_concentration_warning() {
1206 let config = Config::default();
1207 let factory = MockTokenFactory;
1208 let args = InsightsArgs {
1209 target: "0xTEST1234567890123456789012345678901234567".to_string(),
1210 chain: Some("ethereum".to_string()),
1211 decode: false,
1212 trace: false,
1213 };
1214 let result = run(args, &config, &factory).await;
1215 assert!(result.is_ok());
1216 }
1217
1218 #[test]
1223 fn test_infer_target_evm_address() {
1224 let t = infer_target("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", None);
1225 assert!(matches!(t, InferredTarget::Address { chain } if chain == "ethereum"));
1226 }
1227
1228 #[test]
1229 fn test_infer_target_tron_address() {
1230 let t = infer_target("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", None);
1231 assert!(matches!(t, InferredTarget::Address { chain } if chain == "tron"));
1232 }
1233
1234 #[test]
1235 fn test_infer_target_solana_address() {
1236 let t = infer_target("DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy", None);
1237 assert!(matches!(t, InferredTarget::Address { chain } if chain == "solana"));
1238 }
1239
1240 #[test]
1241 fn test_infer_target_evm_tx_hash() {
1242 let t = infer_target(
1243 "0xabc123def456789012345678901234567890123456789012345678901234abcd",
1244 None,
1245 );
1246 assert!(matches!(t, InferredTarget::Transaction { chain } if chain == "ethereum"));
1247 }
1248
1249 #[test]
1250 fn test_infer_target_tron_tx_hash() {
1251 let t = infer_target(
1252 "abc123def456789012345678901234567890123456789012345678901234abcd",
1253 None,
1254 );
1255 assert!(matches!(t, InferredTarget::Transaction { chain } if chain == "tron"));
1256 }
1257
1258 #[test]
1259 fn test_infer_target_token_symbol() {
1260 let t = infer_target("USDC", None);
1261 assert!(matches!(t, InferredTarget::Token { chain } if chain == "ethereum"));
1262 }
1263
1264 #[test]
1265 fn test_infer_target_chain_override() {
1266 let t = infer_target(
1267 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
1268 Some("polygon"),
1269 );
1270 assert!(matches!(t, InferredTarget::Address { chain } if chain == "polygon"));
1271 }
1272
1273 #[test]
1274 fn test_infer_target_token_with_chain_override() {
1275 let t = infer_target("USDC", Some("solana"));
1276 assert!(matches!(t, InferredTarget::Token { chain } if chain == "solana"));
1277 }
1278
1279 #[test]
1280 fn test_classify_tx_type() {
1281 assert_eq!(
1282 classify_tx_type("0xa9059cbb1234...", Some("0xto")),
1283 "ERC-20 Transfer"
1284 );
1285 assert_eq!(
1286 classify_tx_type("0x095ea7b3abcd...", Some("0xto")),
1287 "ERC-20 Approve"
1288 );
1289 assert_eq!(classify_tx_type("0x", Some("0xto")), "Native Transfer");
1290 assert_eq!(classify_tx_type("", None), "Contract Creation");
1291 }
1292
1293 #[test]
1294 fn test_format_tx_value() {
1295 let (fmt, high) = format_tx_value("0xDE0B6B3A7640000", "ethereum"); assert!(fmt.contains("1.0") && fmt.contains("ETH"));
1297 assert!(!high);
1298 let (_, high2) = format_tx_value("0x52B7D2DCC80CD2E4000000", "ethereum"); assert!(high2);
1300 }
1301
1302 #[test]
1303 fn test_is_stablecoin() {
1304 assert!(is_stablecoin("USDC"));
1305 assert!(is_stablecoin("usdt"));
1306 assert!(is_stablecoin("DAI"));
1307 assert!(is_stablecoin("BUSD"));
1308 assert!(is_stablecoin("TUSD"));
1309 assert!(is_stablecoin("USDP"));
1310 assert!(is_stablecoin("FRAX"));
1311 assert!(is_stablecoin("LUSD"));
1312 assert!(is_stablecoin("PUSD"));
1313 assert!(is_stablecoin("GUSD"));
1314 assert!(!is_stablecoin("ETH"));
1315 assert!(!is_stablecoin("PEPE"));
1316 assert!(!is_stablecoin("WBTC"));
1317 }
1318
1319 #[test]
1320 fn test_is_stablecoin_empty_string() {
1321 assert!(!is_stablecoin(""));
1322 }
1323
1324 #[test]
1325 fn test_is_stablecoin_case_insensitive() {
1326 assert!(is_stablecoin("UsDc"));
1328 assert!(is_stablecoin("FraX"));
1329 assert!(!is_stablecoin("SOL")); }
1331
1332 #[test]
1337 fn test_target_type_label_address() {
1338 let t = InferredTarget::Address {
1339 chain: "ethereum".to_string(),
1340 };
1341 assert_eq!(target_type_label(&t), "Address");
1342 }
1343
1344 #[test]
1345 fn test_target_type_label_transaction() {
1346 let t = InferredTarget::Transaction {
1347 chain: "ethereum".to_string(),
1348 };
1349 assert_eq!(target_type_label(&t), "Transaction");
1350 }
1351
1352 #[test]
1353 fn test_target_type_label_token() {
1354 let t = InferredTarget::Token {
1355 chain: "ethereum".to_string(),
1356 };
1357 assert_eq!(target_type_label(&t), "Token");
1358 }
1359
1360 #[test]
1361 fn test_chain_label_address() {
1362 let t = InferredTarget::Address {
1363 chain: "polygon".to_string(),
1364 };
1365 assert_eq!(chain_label(&t), "polygon");
1366 }
1367
1368 #[test]
1369 fn test_chain_label_transaction() {
1370 let t = InferredTarget::Transaction {
1371 chain: "tron".to_string(),
1372 };
1373 assert_eq!(chain_label(&t), "tron");
1374 }
1375
1376 #[test]
1377 fn test_chain_label_token() {
1378 let t = InferredTarget::Token {
1379 chain: "solana".to_string(),
1380 };
1381 assert_eq!(chain_label(&t), "solana");
1382 }
1383
1384 #[test]
1389 fn test_classify_tx_type_dex_swaps() {
1390 assert_eq!(
1391 classify_tx_type("0x38ed173900000...", Some("0xrouter")),
1392 "DEX Swap"
1393 );
1394 assert_eq!(
1395 classify_tx_type("0x5c11d79500000...", Some("0xrouter")),
1396 "DEX Swap"
1397 );
1398 assert_eq!(
1399 classify_tx_type("0x4a25d94a00000...", Some("0xrouter")),
1400 "DEX Swap"
1401 );
1402 assert_eq!(
1403 classify_tx_type("0x8803dbee00000...", Some("0xrouter")),
1404 "DEX Swap"
1405 );
1406 assert_eq!(
1407 classify_tx_type("0x7ff36ab500000...", Some("0xrouter")),
1408 "DEX Swap"
1409 );
1410 assert_eq!(
1411 classify_tx_type("0x18cbafe500000...", Some("0xrouter")),
1412 "DEX Swap"
1413 );
1414 assert_eq!(
1415 classify_tx_type("0xfb3bdb4100000...", Some("0xrouter")),
1416 "DEX Swap"
1417 );
1418 assert_eq!(
1419 classify_tx_type("0xb6f9de9500000...", Some("0xrouter")),
1420 "DEX Swap"
1421 );
1422 }
1423
1424 #[test]
1425 fn test_classify_tx_type_multicall() {
1426 assert_eq!(
1427 classify_tx_type("0xac9650d800000...", Some("0xcontract")),
1428 "Multicall"
1429 );
1430 assert_eq!(
1431 classify_tx_type("0x5ae401dc00000...", Some("0xcontract")),
1432 "Multicall"
1433 );
1434 }
1435
1436 #[test]
1437 fn test_classify_tx_type_transfer_from() {
1438 assert_eq!(
1439 classify_tx_type("0x23b872dd00000...", Some("0xtoken")),
1440 "ERC-20 Transfer From"
1441 );
1442 }
1443
1444 #[test]
1445 fn test_classify_tx_type_contract_call() {
1446 assert_eq!(
1447 classify_tx_type("0xdeadbeef00000...", Some("0xcontract")),
1448 "Contract Call"
1449 );
1450 }
1451
1452 #[test]
1453 fn test_classify_tx_type_native_transfer_empty() {
1454 assert_eq!(classify_tx_type("", Some("0xrecipient")), "Native Transfer");
1455 }
1456
1457 #[test]
1462 fn test_format_tx_value_zero() {
1463 let (fmt, high) = format_tx_value("0x0", "ethereum");
1464 assert!(fmt.contains("0.000000"));
1465 assert!(fmt.contains("ETH"));
1466 assert!(!high);
1467 }
1468
1469 #[test]
1470 fn test_format_tx_value_empty_hex() {
1471 let (fmt, high) = format_tx_value("0x", "ethereum");
1472 assert!(fmt.contains("0.000000"));
1473 assert!(!high);
1474 }
1475
1476 #[test]
1477 fn test_format_tx_value_decimal_string() {
1478 let (fmt, high) = format_tx_value("1000000000000000000", "ethereum"); assert!(fmt.contains("1.0"));
1480 assert!(fmt.contains("ETH"));
1481 assert!(!high);
1482 }
1483
1484 #[test]
1485 fn test_format_tx_value_solana() {
1486 let (fmt, high) = format_tx_value("1000000000", "solana"); assert!(fmt.contains("1.0"));
1488 assert!(fmt.contains("SOL"));
1489 assert!(!high);
1490 }
1491
1492 #[test]
1493 fn test_format_tx_value_tron() {
1494 let (fmt, high) = format_tx_value("1000000", "tron"); assert!(fmt.contains("1.0"));
1496 assert!(fmt.contains("TRX"));
1497 assert!(!high);
1498 }
1499
1500 #[test]
1501 fn test_format_tx_value_polygon() {
1502 let (fmt, _) = format_tx_value("1000000000000000000", "polygon");
1503 assert!(fmt.contains("MATIC") || fmt.contains("POL"));
1504 }
1505
1506 #[test]
1507 fn test_format_tx_value_bsc() {
1508 let (fmt, _) = format_tx_value("1000000000000000000", "bsc");
1509 assert!(fmt.contains("BNB"));
1510 }
1511
1512 #[test]
1513 fn test_format_tx_value_high_value_threshold() {
1514 let (_, high) = format_tx_value("11000000000000000000", "ethereum"); assert!(high);
1517 let (_, high2) = format_tx_value("10000000000000000000", "ethereum"); assert!(!high2); }
1520
1521 #[test]
1526 fn test_meta_analysis_address_contract_high_value() {
1527 let meta = meta_analysis_address(true, Some(2_000_000.0), 10, None, None);
1528 assert!(meta.synthesis.contains("contract"));
1529 assert!(meta.synthesis.contains("Significant value"));
1530 assert!(meta.synthesis.contains("Diversified"));
1531 assert!(meta.recommendations.iter().any(|r| r.contains("contract")));
1532 }
1533
1534 #[test]
1535 fn test_meta_analysis_address_eoa_moderate_value() {
1536 let meta = meta_analysis_address(false, Some(50_000.0), 3, None, None);
1537 assert!(meta.synthesis.contains("wallet (EOA)"));
1538 assert!(meta.synthesis.contains("Moderate value"));
1539 }
1540
1541 #[test]
1542 fn test_meta_analysis_address_minimal_value() {
1543 let meta = meta_analysis_address(false, Some(0.5), 0, None, None);
1544 assert!(meta.synthesis.contains("Minimal value"));
1545 }
1546
1547 #[test]
1548 fn test_meta_analysis_address_single_token() {
1549 let meta = meta_analysis_address(false, None, 1, None, None);
1550 assert!(meta.synthesis.contains("Concentrated in a single token"));
1551 }
1552
1553 #[test]
1554 fn test_meta_analysis_address_high_risk() {
1555 use crate::compliance::risk::RiskLevel;
1556 let level = RiskLevel::High;
1557 let meta = meta_analysis_address(false, None, 0, Some(8.5), Some(&level));
1558 assert!(meta.synthesis.contains("Elevated risk"));
1559 assert!(meta.key_takeaway.contains("scrutiny"));
1560 assert!(
1561 meta.recommendations
1562 .iter()
1563 .any(|r| r.contains("unusual transaction"))
1564 );
1565 }
1566
1567 #[test]
1568 fn test_meta_analysis_address_low_risk() {
1569 use crate::compliance::risk::RiskLevel;
1570 let level = RiskLevel::Low;
1571 let meta = meta_analysis_address(false, None, 0, Some(2.0), Some(&level));
1572 assert!(meta.synthesis.contains("Low risk"));
1573 }
1574
1575 #[test]
1576 fn test_meta_analysis_address_contract_no_value() {
1577 let meta = meta_analysis_address(true, None, 0, None, None);
1578 assert!(meta.key_takeaway.contains("Contract address"));
1579 assert!(
1580 meta.recommendations
1581 .iter()
1582 .any(|r| r.contains("Confirm contract"))
1583 );
1584 }
1585
1586 #[test]
1587 fn test_meta_analysis_address_high_value_wallet() {
1588 let meta = meta_analysis_address(false, Some(150_000.0), 0, None, None);
1589 assert!(meta.key_takeaway.contains("High-value wallet"));
1590 }
1591
1592 #[test]
1593 fn test_meta_analysis_address_default_takeaway() {
1594 let meta = meta_analysis_address(false, Some(5_000.0), 0, None, None);
1595 assert!(meta.key_takeaway.contains("Review full report"));
1596 }
1597
1598 #[test]
1599 fn test_meta_analysis_address_with_tokens_recommendation() {
1600 let meta = meta_analysis_address(false, None, 3, None, None);
1601 assert!(
1602 meta.recommendations
1603 .iter()
1604 .any(|r| r.contains("Verify token contracts"))
1605 );
1606 }
1607
1608 #[test]
1613 fn test_meta_analysis_tx_successful_native_transfer() {
1614 let meta = meta_analysis_tx("Native Transfer", true, false, "0xfrom", Some("0xto"));
1615 assert!(meta.synthesis.contains("Native Transfer"));
1616 assert!(meta.key_takeaway.contains("Routine"));
1617 assert!(meta.recommendations.is_empty());
1618 }
1619
1620 #[test]
1621 fn test_meta_analysis_tx_failed() {
1622 let meta = meta_analysis_tx("Contract Call", false, false, "0xfrom", Some("0xto"));
1623 assert!(meta.synthesis.contains("failed"));
1624 assert!(meta.key_takeaway.contains("Failed transaction"));
1625 assert!(meta.recommendations.iter().any(|r| r.contains("revert")));
1626 }
1627
1628 #[test]
1629 fn test_meta_analysis_tx_high_value_native() {
1630 let meta = meta_analysis_tx("Native Transfer", true, true, "0xfrom", Some("0xto"));
1631 assert!(meta.synthesis.contains("High-value"));
1632 assert!(meta.key_takeaway.contains("Large native transfer"));
1633 assert!(meta.recommendations.iter().any(|r| r.contains("recipient")));
1634 }
1635
1636 #[test]
1637 fn test_meta_analysis_tx_high_value_contract_call() {
1638 let meta = meta_analysis_tx("DEX Swap", true, true, "0xfrom", Some("0xto"));
1639 assert!(meta.key_takeaway.contains("High-value operation"));
1640 }
1641
1642 #[test]
1643 fn test_meta_analysis_tx_erc20_approve() {
1644 let meta = meta_analysis_tx("ERC-20 Approval", true, false, "0xfrom", Some("0xto"));
1645 assert!(meta.recommendations.iter().any(|r| r.contains("spender")));
1646 }
1647
1648 #[test]
1649 fn test_meta_analysis_tx_failed_high_value() {
1650 let meta = meta_analysis_tx("Contract Call", false, true, "0xfrom", Some("0xto"));
1651 assert!(meta.synthesis.contains("failed"));
1652 assert!(meta.synthesis.contains("High-value"));
1653 assert!(meta.recommendations.len() >= 2);
1654 }
1655
1656 #[test]
1661 fn test_meta_analysis_token_low_risk() {
1662 let summary = report::TokenRiskSummary {
1663 score: 2,
1664 level: "Low",
1665 emoji: "🟢",
1666 concerns: vec![],
1667 positives: vec!["Good liquidity".to_string()],
1668 };
1669 let meta = meta_analysis_token(&summary, false, None, None, 2_000_000.0);
1670 assert!(meta.synthesis.contains("Low-risk"));
1671 assert!(meta.synthesis.contains("Strong liquidity"));
1672 assert!(meta.key_takeaway.contains("Favorable"));
1673 }
1674
1675 #[test]
1676 fn test_meta_analysis_token_high_risk() {
1677 let summary = report::TokenRiskSummary {
1678 score: 8,
1679 level: "High",
1680 emoji: "🔴",
1681 concerns: vec!["Low liquidity".to_string()],
1682 positives: vec![],
1683 };
1684 let meta = meta_analysis_token(&summary, false, None, None, 10_000.0);
1685 assert!(meta.synthesis.contains("Elevated risk"));
1686 assert!(meta.synthesis.contains("Limited liquidity"));
1687 assert!(meta.key_takeaway.contains("High risk"));
1688 assert!(
1689 meta.recommendations
1690 .iter()
1691 .any(|r| r.contains("smaller position"))
1692 );
1693 }
1694
1695 #[test]
1696 fn test_meta_analysis_token_moderate_risk() {
1697 let summary = report::TokenRiskSummary {
1698 score: 5,
1699 level: "Medium",
1700 emoji: "🟡",
1701 concerns: vec!["Some concern".to_string()],
1702 positives: vec!["Some positive".to_string()],
1703 };
1704 let meta = meta_analysis_token(&summary, false, None, None, 500_000.0);
1705 assert!(meta.synthesis.contains("Moderate risk"));
1706 assert!(meta.key_takeaway.contains("Risk 5/10"));
1707 }
1708
1709 #[test]
1710 fn test_meta_analysis_token_stablecoin_healthy_peg() {
1711 let summary = report::TokenRiskSummary {
1712 score: 2,
1713 level: "Low",
1714 emoji: "🟢",
1715 concerns: vec![],
1716 positives: vec!["Stable peg".to_string()],
1717 };
1718 let meta = meta_analysis_token(&summary, true, Some(true), None, 5_000_000.0);
1719 assert!(meta.synthesis.contains("Stablecoin peg is healthy"));
1720 }
1721
1722 #[test]
1723 fn test_meta_analysis_token_stablecoin_unhealthy_peg() {
1724 let summary = report::TokenRiskSummary {
1725 score: 4,
1726 level: "Medium",
1727 emoji: "🟡",
1728 concerns: vec![],
1729 positives: vec![],
1730 };
1731 let meta = meta_analysis_token(&summary, true, Some(false), None, 500_000.0);
1732 assert!(meta.synthesis.contains("peg deviation"));
1733 assert!(meta.key_takeaway.contains("deviating from peg"));
1734 assert!(meta.recommendations.iter().any(|r| r.contains("peg")));
1735 }
1736
1737 #[test]
1738 fn test_meta_analysis_token_concentration_risk() {
1739 let summary = report::TokenRiskSummary {
1740 score: 5,
1741 level: "Medium",
1742 emoji: "🟡",
1743 concerns: vec![],
1744 positives: vec![],
1745 };
1746 let meta = meta_analysis_token(&summary, false, None, Some(45.0), 500_000.0);
1747 assert!(meta.synthesis.contains("Concentration risk"));
1748 assert!(
1749 meta.recommendations
1750 .iter()
1751 .any(|r| r.contains("top holder"))
1752 );
1753 }
1754
1755 #[test]
1756 fn test_meta_analysis_token_low_liquidity_low_risk() {
1757 let summary = report::TokenRiskSummary {
1758 score: 3,
1759 level: "Low",
1760 emoji: "🟢",
1761 concerns: vec![],
1762 positives: vec![],
1763 };
1764 let meta = meta_analysis_token(&summary, false, None, None, 50_000.0);
1765 assert!(
1766 meta.recommendations
1767 .iter()
1768 .any(|r| r.contains("limit orders") || r.contains("slippage"))
1769 );
1770 }
1771
1772 #[test]
1773 fn test_meta_analysis_token_stablecoin_no_peg_data() {
1774 let summary = report::TokenRiskSummary {
1775 score: 3,
1776 level: "Low",
1777 emoji: "🟢",
1778 concerns: vec![],
1779 positives: vec![],
1780 };
1781 let meta = meta_analysis_token(&summary, true, None, None, 1_000_000.0);
1782 assert!(meta.recommendations.iter().any(|r| r.contains("peg")));
1784 }
1785
1786 #[test]
1791 fn test_infer_target_tx_hash_with_chain_override() {
1792 let t = infer_target(
1793 "0xabc123def456789012345678901234567890123456789012345678901234abcd",
1794 Some("polygon"),
1795 );
1796 assert!(matches!(t, InferredTarget::Transaction { chain } if chain == "polygon"));
1797 }
1798
1799 #[test]
1800 fn test_infer_target_whitespace_trimming() {
1801 let t = infer_target(" USDC ", None);
1802 assert!(matches!(t, InferredTarget::Token { .. }));
1803 }
1804
1805 #[test]
1806 fn test_infer_target_long_token_name() {
1807 let t = infer_target("some-random-token-name", None);
1808 assert!(matches!(t, InferredTarget::Token { chain } if chain == "ethereum"));
1809 }
1810
1811 #[test]
1816 fn test_insights_args_debug() {
1817 let args = InsightsArgs {
1818 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
1819 chain: Some("ethereum".to_string()),
1820 decode: true,
1821 trace: false,
1822 };
1823 let debug_str = format!("{:?}", args);
1824 assert!(debug_str.contains("InsightsArgs"));
1825 assert!(debug_str.contains("0x742d"));
1826 }
1827
1828 #[test]
1833 fn test_classify_tx_type_contract_creation() {
1834 assert_eq!(classify_tx_type("0xa9059cbb...", None), "Contract Creation");
1835 }
1836
1837 #[test]
1838 fn test_classify_tx_type_erc20_transfer() {
1839 assert_eq!(
1840 classify_tx_type("0xa9059cbb00000000", Some("0x1234")),
1841 "ERC-20 Transfer"
1842 );
1843 }
1844
1845 #[test]
1846 fn test_classify_tx_type_erc20_approve() {
1847 assert_eq!(
1848 classify_tx_type("0x095ea7b3...", Some("0x1234")),
1849 "ERC-20 Approve"
1850 );
1851 }
1852
1853 #[test]
1854 fn test_classify_tx_type_erc20_transfer_from() {
1855 assert_eq!(
1856 classify_tx_type("0x23b872dd...", Some("0x1234")),
1857 "ERC-20 Transfer From"
1858 );
1859 }
1860
1861 #[test]
1862 fn test_classify_tx_type_dex_swap() {
1863 assert_eq!(
1864 classify_tx_type("0x38ed1739...", Some("0x1234")),
1865 "DEX Swap"
1866 );
1867 assert_eq!(
1868 classify_tx_type("0x7ff36ab5...", Some("0x1234")),
1869 "DEX Swap"
1870 );
1871 }
1872
1873 #[test]
1874 fn test_classify_tx_type_native_transfer() {
1875 assert_eq!(classify_tx_type("0x", Some("0x1234")), "Native Transfer");
1876 assert_eq!(classify_tx_type("", Some("0x1234")), "Native Transfer");
1877 }
1878
1879 #[test]
1880 fn test_classify_tx_type_unknown_contract_call() {
1881 assert_eq!(
1882 classify_tx_type("0xdeadbeef12345678", Some("0x1234")),
1883 "Contract Call"
1884 );
1885 }
1886
1887 #[test]
1892 fn test_format_tx_value_ethereum_wei() {
1893 let (fmt, high) = format_tx_value("1000000000000000000", "ethereum");
1894 assert!(fmt.contains("1.000000"));
1895 assert!(fmt.contains("ETH"));
1896 assert!(!high); }
1898
1899 #[test]
1900 fn test_format_tx_value_hex() {
1901 let (fmt, _) = format_tx_value("0xde0b6b3a7640000", "ethereum");
1902 assert!(fmt.contains("ETH"));
1904 }
1905
1906 #[test]
1907 fn test_format_tx_value_high_value() {
1908 let (_, high) = format_tx_value("100000000000000000000", "ethereum");
1910 assert!(high); }
1912
1913 #[test]
1914 fn test_format_tx_value_zero_decimal() {
1915 let (fmt, high) = format_tx_value("0", "ethereum");
1916 assert!(fmt.contains("0.000000"));
1917 assert!(!high);
1918 }
1919
1920 #[test]
1921 fn test_format_tx_value_solana_additional() {
1922 let (fmt, _) = format_tx_value("1000000000", "solana"); assert!(fmt.contains("SOL"));
1924 }
1925
1926 #[test]
1927 fn test_format_tx_value_tron_additional() {
1928 let (fmt, _) = format_tx_value("1000000", "tron"); assert!(fmt.contains("TRX"));
1930 }
1931
1932 #[test]
1933 fn test_format_tx_value_empty_hex_additional() {
1934 let (fmt, _) = format_tx_value("0x", "ethereum");
1935 assert!(fmt.contains("0.000000"));
1936 }
1937
1938 #[test]
1943 fn test_target_type_label_combined() {
1944 assert_eq!(
1945 target_type_label(&InferredTarget::Address {
1946 chain: "eth".to_string()
1947 }),
1948 "Address"
1949 );
1950 assert_eq!(
1951 target_type_label(&InferredTarget::Transaction {
1952 chain: "eth".to_string()
1953 }),
1954 "Transaction"
1955 );
1956 assert_eq!(
1957 target_type_label(&InferredTarget::Token {
1958 chain: "eth".to_string()
1959 }),
1960 "Token"
1961 );
1962 }
1963
1964 #[test]
1965 fn test_chain_label_combined() {
1966 assert_eq!(
1967 chain_label(&InferredTarget::Address {
1968 chain: "ethereum".to_string()
1969 }),
1970 "ethereum"
1971 );
1972 assert_eq!(
1973 chain_label(&InferredTarget::Transaction {
1974 chain: "polygon".to_string()
1975 }),
1976 "polygon"
1977 );
1978 assert_eq!(
1979 chain_label(&InferredTarget::Token {
1980 chain: "solana".to_string()
1981 }),
1982 "solana"
1983 );
1984 }
1985
1986 #[test]
1991 fn test_meta_analysis_address_contract_high_risk() {
1992 use crate::compliance::risk::RiskLevel;
1993 let meta = meta_analysis_address(
1994 true,
1995 Some(2_000_000.0),
1996 10,
1997 Some(8.0),
1998 Some(&RiskLevel::High),
1999 );
2000 assert!(meta.synthesis.contains("contract"));
2001 assert!(meta.synthesis.contains("Significant value"));
2002 assert!(meta.key_takeaway.contains("scrutiny"));
2003 assert!(!meta.recommendations.is_empty());
2004 }
2005
2006 #[test]
2007 fn test_meta_analysis_address_wallet_low_risk() {
2008 use crate::compliance::risk::RiskLevel;
2009 let meta = meta_analysis_address(false, Some(0.5), 0, Some(2.0), Some(&RiskLevel::Low));
2010 assert!(meta.synthesis.contains("wallet"));
2011 assert!(meta.synthesis.contains("Minimal value"));
2012 }
2013
2014 #[test]
2015 fn test_meta_analysis_address_no_risk_data() {
2016 let meta = meta_analysis_address(false, None, 0, None, None);
2017 assert!(!meta.synthesis.is_empty());
2018 assert!(meta.key_takeaway.contains("Review full report"));
2019 }
2020
2021 #[test]
2026 fn test_meta_analysis_tx_failed_additional() {
2027 let meta = meta_analysis_tx("Contract Call", false, false, "0x...", Some("0x..."));
2028 assert!(meta.synthesis.contains("failed"));
2029 assert!(meta.key_takeaway.contains("Failed"));
2030 }
2031
2032 #[test]
2033 fn test_meta_analysis_tx_high_value_native_additional() {
2034 let meta = meta_analysis_tx("Native Transfer", true, true, "0x...", Some("0x..."));
2035 assert!(meta.synthesis.contains("High-value"));
2036 assert!(meta.key_takeaway.contains("Large native transfer"));
2037 }
2038
2039 #[test]
2040 fn test_meta_analysis_tx_routine() {
2041 let meta = meta_analysis_tx("ERC-20 Transfer", true, false, "0x...", Some("0x..."));
2042 assert!(meta.key_takeaway.contains("Routine"));
2043 }
2044
2045 #[test]
2050 fn test_meta_analysis_token_high_risk_additional() {
2051 let risk = report::TokenRiskSummary {
2052 score: 8,
2053 level: "High",
2054 emoji: "🔴",
2055 concerns: vec!["Low liquidity".to_string()],
2056 positives: vec![],
2057 };
2058 let meta = meta_analysis_token(&risk, false, None, None, 10_000.0);
2059 assert!(meta.synthesis.contains("Elevated risk"));
2060 assert!(meta.key_takeaway.contains("High risk"));
2061 }
2062
2063 #[test]
2064 fn test_meta_analysis_token_stablecoin_peg_healthy() {
2065 let risk = report::TokenRiskSummary {
2066 score: 2,
2067 level: "Low",
2068 emoji: "🟢",
2069 concerns: vec![],
2070 positives: vec!["Strong liquidity".to_string()],
2071 };
2072 let meta = meta_analysis_token(&risk, true, Some(true), Some(5.0), 5_000_000.0);
2073 assert!(meta.synthesis.contains("peg is healthy"));
2074 assert!(meta.synthesis.contains("Strong liquidity"));
2075 }
2076
2077 #[test]
2078 fn test_meta_analysis_token_stablecoin_peg_unhealthy() {
2079 let risk = report::TokenRiskSummary {
2080 score: 5,
2081 level: "Medium",
2082 emoji: "🟡",
2083 concerns: vec!["Peg deviation".to_string()],
2084 positives: vec![],
2085 };
2086 let meta = meta_analysis_token(&risk, true, Some(false), Some(40.0), 100_000.0);
2087 assert!(meta.synthesis.contains("peg deviation"));
2088 assert!(meta.synthesis.contains("Concentration risk"));
2089 }
2090}