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)]
32#[command(after_help = "\x1b[1mExamples:\x1b[0m
33 scope insights 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2
34 scope insights @main-wallet \x1b[2m# address book shortcut\x1b[0m
35 scope insights 0xabc123def456... --decode --trace
36 scope insights USDC
37 scope insights DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy --chain solana")]
38pub struct InsightsArgs {
39 pub target: String,
51
52 #[arg(short, long)]
54 pub chain: Option<String>,
55
56 #[arg(long)]
58 pub decode: bool,
59
60 #[arg(long)]
62 pub trace: bool,
63}
64
65pub fn infer_target(input: &str, chain_override: Option<&str>) -> InferredTarget {
67 let trimmed = input.trim();
68
69 if let Some(chain) = chain_override {
70 let chain = chain.to_lowercase();
71 if infer_chain_from_hash(trimmed).is_some() {
73 return InferredTarget::Transaction { chain };
74 }
75 if TokenAliases::is_address(trimmed) {
76 return InferredTarget::Address { chain };
77 }
78 return InferredTarget::Token { chain };
79 }
80
81 if let Some(chain) = infer_chain_from_hash(trimmed) {
83 return InferredTarget::Transaction {
84 chain: chain.to_string(),
85 };
86 }
87
88 if TokenAliases::is_address(trimmed) {
90 let chain = infer_chain_from_address(trimmed).unwrap_or("ethereum");
91 return InferredTarget::Address {
92 chain: chain.to_string(),
93 };
94 }
95
96 InferredTarget::Token {
98 chain: "ethereum".to_string(),
99 }
100}
101
102pub async fn run(
104 mut args: InsightsArgs,
105 config: &Config,
106 clients: &dyn ChainClientFactory,
107) -> Result<()> {
108 if let Some((address, chain)) =
110 crate::cli::address_book::resolve_address_book_input(&args.target, config)?
111 {
112 args.target = address;
113 if args.chain.is_none() {
114 args.chain = Some(chain);
115 }
116 }
117
118 let chain_override = args.chain.as_deref();
119 let target = infer_target(&args.target, chain_override);
120
121 let sp = crate::cli::progress::Spinner::new(&format!(
122 "Analyzing {} on {}...",
123 target_type_label(&target),
124 chain_label(&target)
125 ));
126
127 let mut output = String::new();
128 output.push_str("# Scope Insights\n\n");
129 output.push_str(&format!("**Target:** `{}`\n\n", args.target));
130 output.push_str(&format!(
131 "**Detected:** {} on {}\n\n",
132 target_type_label(&target),
133 chain_label(&target)
134 ));
135 output.push_str("---\n\n");
136
137 match &target {
138 InferredTarget::Address { chain } => {
139 output.push_str("## Observations\n\n");
140 let addr_args = AddressArgs {
141 address: args.target.clone(),
142 chain: chain.clone(),
143 format: Some(crate::config::OutputFormat::Markdown),
144 include_txs: false,
145 include_tokens: true,
146 limit: 10,
147 report: None,
148 dossier: false,
149 };
150 let client = clients.create_chain_client(chain)?;
151 let report = address::analyze_address(&addr_args, client.as_ref()).await?;
152
153 let code_result = client.get_code(&args.target).await;
155 let is_contract = code_result
156 .as_ref()
157 .is_ok_and(|c| !c.is_empty() && c != "0x");
158 if code_result.is_ok() {
159 output.push_str(&format!(
160 "- **Type:** {}\n",
161 if is_contract {
162 "Contract"
163 } else {
164 "Externally Owned Account (EOA)"
165 }
166 ));
167 }
168
169 output.push_str(&format!(
170 "- **Native balance:** {} ({})\n",
171 report.balance.formatted,
172 crate::chains::native_symbol(chain)
173 ));
174 if let Some(ref usd) = report.balance.usd {
175 output.push_str(&format!("- **USD value:** ${:.2}\n", usd));
176 }
177 output.push_str(&format!(
178 "- **Transaction count:** {}\n",
179 report.transaction_count
180 ));
181 if let Some(ref tokens) = report.tokens
182 && !tokens.is_empty()
183 {
184 output.push_str(&format!(
185 "- **Token holdings:** {} different tokens\n",
186 tokens.len()
187 ));
188 output.push_str("\n### Token Balances\n\n");
189 for tb in tokens.iter().take(10) {
190 output.push_str(&format!(
191 "- {}: {} ({})\n",
192 tb.symbol, tb.formatted_balance, tb.contract_address
193 ));
194 }
195 if tokens.len() > 10 {
196 output.push_str(&format!("\n*...and {} more*\n", tokens.len() - 10));
197 }
198 }
199
200 let risk_assessment =
202 match crate::compliance::datasource::BlockchainDataClient::from_env_opt() {
203 Some(data_client) => {
204 crate::compliance::risk::RiskEngine::with_data_client(data_client)
205 .assess_address(&args.target, chain)
206 .await
207 .ok()
208 }
209 None => crate::compliance::risk::RiskEngine::new()
210 .assess_address(&args.target, chain)
211 .await
212 .ok(),
213 };
214
215 if let Some(ref risk) = risk_assessment {
216 output.push_str(&format!(
217 "\n- **Risk:** {} {:.1}/10 ({:?})\n",
218 risk.risk_level.emoji(),
219 risk.overall_score,
220 risk.risk_level
221 ));
222 }
223
224 let meta = meta_analysis_address(
226 is_contract,
227 report.balance.usd,
228 report.tokens.as_ref().map(|t| t.len()).unwrap_or(0),
229 risk_assessment.as_ref().map(|r| r.overall_score),
230 risk_assessment.as_ref().map(|r| &r.risk_level),
231 );
232 output.push_str("\n### Synthesis\n\n");
233 output.push_str(&format!("{}\n\n", meta.synthesis));
234 output.push_str(&format!("**Key takeaway:** {}\n\n", meta.key_takeaway));
235 if !meta.recommendations.is_empty() {
236 output.push_str("**Consider:**\n");
237 for rec in &meta.recommendations {
238 output.push_str(&format!("- {}\n", rec));
239 }
240 }
241 output.push_str("\n---\n\n");
242 let full_report = if let Some(ref risk) = risk_assessment {
243 crate::cli::address_report::generate_dossier_report(&report, risk)
244 } else {
245 crate::cli::address_report::generate_address_report(&report)
246 };
247 output.push_str(&full_report);
248 }
249 InferredTarget::Transaction { chain } => {
250 output.push_str("## Observations\n\n");
251 let tx_report =
252 fetch_transaction_report(&args.target, chain, args.decode, args.trace, clients)
253 .await?;
254
255 let tx_type = classify_tx_type(
256 &tx_report.transaction.input,
257 tx_report.transaction.to.as_deref(),
258 );
259 output.push_str(&format!("- **Type:** {}\n", tx_type));
260
261 output.push_str(&format!(
262 "- **Status:** {}\n",
263 if tx_report.transaction.status {
264 "Success"
265 } else {
266 "Failed"
267 }
268 ));
269 output.push_str(&format!("- **From:** `{}`\n", tx_report.transaction.from));
270 output.push_str(&format!(
271 "- **To:** `{}`\n",
272 tx_report
273 .transaction
274 .to
275 .as_deref()
276 .unwrap_or("Contract Creation")
277 ));
278
279 let (formatted_value, high_value) =
280 format_tx_value(&tx_report.transaction.value, chain);
281 output.push_str(&format!("- **Value:** {}\n", formatted_value));
282 if high_value {
283 output.push_str("- ⚠️ **High-value transfer**\n");
284 }
285
286 output.push_str(&format!("- **Fee:** {}\n", tx_report.gas.transaction_fee));
287
288 let meta = meta_analysis_tx(
290 tx_type,
291 tx_report.transaction.status,
292 high_value,
293 &tx_report.transaction.from,
294 tx_report.transaction.to.as_deref(),
295 );
296 output.push_str("\n### Synthesis\n\n");
297 output.push_str(&format!("{}\n\n", meta.synthesis));
298 output.push_str(&format!("**Key takeaway:** {}\n\n", meta.key_takeaway));
299 if !meta.recommendations.is_empty() {
300 output.push_str("**Consider:**\n");
301 for rec in &meta.recommendations {
302 output.push_str(&format!("- {}\n", rec));
303 }
304 }
305 output.push_str("\n---\n\n");
306 output.push_str(&format_tx_markdown(&tx_report));
307 }
308 InferredTarget::Token { chain } => {
309 output.push_str("## Observations\n\n");
310 let analytics = fetch_analytics_for_input(
311 &args.target,
312 chain,
313 Period::Hour24,
314 10,
315 clients,
316 Some(&sp),
317 )
318 .await?;
319
320 let risk_summary = report::token_risk_summary(&analytics);
322 output.push_str(&format!(
323 "- **Risk:** {} {}/10 ({})\n",
324 risk_summary.emoji, risk_summary.score, risk_summary.level
325 ));
326 if !risk_summary.concerns.is_empty() {
327 for c in &risk_summary.concerns {
328 output.push_str(&format!("- ⚠️ {}\n", c));
329 }
330 }
331 if !risk_summary.positives.is_empty() {
332 for p in &risk_summary.positives {
333 output.push_str(&format!("- ✅ {}\n", p));
334 }
335 }
336
337 output.push_str(&format!(
338 "- **Token:** {} ({})\n",
339 analytics.token.symbol, analytics.token.name
340 ));
341 output.push_str(&format!(
342 "- **Address:** `{}`\n",
343 analytics.token.contract_address
344 ));
345 output.push_str(&format!("- **Price:** ${:.6}\n", analytics.price_usd));
346 output.push_str(&format!(
347 "- **Liquidity (24h):** ${}\n",
348 crate::display::format_usd(analytics.liquidity_usd)
349 ));
350 output.push_str(&format!(
351 "- **Volume (24h):** ${}\n",
352 crate::display::format_usd(analytics.volume_24h)
353 ));
354
355 if let Some(top) = analytics.holders.first() {
357 output.push_str(&format!(
358 "- **Top holder:** `{}` ({:.1}%)\n",
359 top.address, top.percentage
360 ));
361 if top.percentage > 30.0 {
362 output.push_str(" - ⚠️ High concentration risk\n");
363 }
364 }
365 output.push_str(&format!(
366 "- **Holders displayed:** {}\n",
367 analytics.holders.len()
368 ));
369
370 let mut peg_healthy: Option<bool> = None;
372 if is_stablecoin(&analytics.token.symbol)
373 && let Ok(registry) = VenueRegistry::load()
374 {
375 let venue_id = if registry.contains("binance") {
377 "binance"
378 } else {
379 registry.list().first().copied().unwrap_or("binance")
380 };
381 if let Ok(exchange) = registry.create_exchange_client(venue_id) {
382 let pair = exchange.format_pair(&analytics.token.symbol);
383 if let Ok(book) = exchange.fetch_order_book(&pair).await {
384 let thresholds = HealthThresholds {
385 peg_target: 1.0,
386 peg_range: 0.001,
387 min_levels: 6,
388 min_depth: 3000.0,
389 min_bid_ask_ratio: 0.2,
390 max_bid_ask_ratio: 5.0,
391 };
392 let volume_24h = if exchange.has_ticker() {
393 exchange
394 .fetch_ticker(&pair)
395 .await
396 .ok()
397 .and_then(|t| t.quote_volume_24h.or(t.volume_24h))
398 } else {
399 None
400 };
401 let summary =
402 MarketSummary::from_order_book(&book, 1.0, &thresholds, volume_24h);
403 let deviation_bps = summary
404 .mid_price
405 .map(|m| (m - 1.0) * 10_000.0)
406 .unwrap_or(0.0);
407 peg_healthy = Some(deviation_bps.abs() < 10.0);
408 let peg_status = if peg_healthy.unwrap_or(false) {
409 "Peg healthy"
410 } else if deviation_bps.abs() < 50.0 {
411 "Slight peg deviation"
412 } else {
413 "Peg deviation"
414 };
415 output.push_str(&format!(
416 "- **Market ({} {}):** {} (deviation: {:.1} bps)\n",
417 exchange.venue_name(),
418 pair,
419 peg_status,
420 deviation_bps
421 ));
422 }
423 }
424 }
425
426 let top_holder_pct = analytics.holders.first().map(|h| h.percentage);
428 let meta = meta_analysis_token(
429 &risk_summary,
430 is_stablecoin(&analytics.token.symbol),
431 peg_healthy,
432 top_holder_pct,
433 analytics.liquidity_usd,
434 );
435 output.push_str("\n### Synthesis\n\n");
436 output.push_str(&format!("{}\n\n", meta.synthesis));
437 output.push_str(&format!("**Key takeaway:** {}\n\n", meta.key_takeaway));
438 if !meta.recommendations.is_empty() {
439 output.push_str("**Consider:**\n");
440 for rec in &meta.recommendations {
441 output.push_str(&format!("- {}\n", rec));
442 }
443 }
444 output.push_str("\n---\n\n");
445 output.push_str(&report::generate_report(&analytics));
446 }
447 }
448
449 sp.finish("Insights complete.");
450 println!("{}", output);
451 Ok(())
452}
453
454fn target_type_label(target: &InferredTarget) -> &'static str {
455 match target {
456 InferredTarget::Address { .. } => "Address",
457 InferredTarget::Transaction { .. } => "Transaction",
458 InferredTarget::Token { .. } => "Token",
459 }
460}
461
462fn chain_label(target: &InferredTarget) -> &str {
463 match target {
464 InferredTarget::Address { chain } => chain,
465 InferredTarget::Transaction { chain } => chain,
466 InferredTarget::Token { chain } => chain,
467 }
468}
469
470fn classify_tx_type(input: &str, to: Option<&str>) -> &'static str {
472 if to.is_none() {
473 return "Contract Creation";
474 }
475 let selector = input
476 .trim_start_matches("0x")
477 .chars()
478 .take(8)
479 .collect::<String>();
480 let sel = selector.to_lowercase();
481 match sel.as_str() {
482 "a9059cbb" => "ERC-20 Transfer",
483 "095ea7b3" => "ERC-20 Approve",
484 "23b872dd" => "ERC-20 Transfer From",
485 "38ed1739" | "5c11d795" | "4a25d94a" | "8803dbee" | "7ff36ab5" | "18cbafe5"
486 | "fb3bdb41" | "b6f9de95" => "DEX Swap",
487 "ac9650d8" | "5ae401dc" => "Multicall",
488 _ if input.is_empty() || input == "0x" => "Native Transfer",
489 _ => "Contract Call",
490 }
491}
492
493fn format_tx_value(value_str: &str, chain: &str) -> (String, bool) {
495 let wei: u128 = if value_str.starts_with("0x") {
496 let hex_part = value_str.trim_start_matches("0x");
497 if hex_part.is_empty() {
498 0
499 } else {
500 u128::from_str_radix(hex_part, 16).unwrap_or(0)
501 }
502 } else {
503 value_str.parse().unwrap_or(0)
504 };
505 let decimals = match chain.to_lowercase().as_str() {
506 "ethereum" | "polygon" | "arbitrum" | "optimism" | "base" | "bsc" | "aegis" => 18,
507 "solana" => 9,
508 "tron" => 6,
509 _ => 18,
510 };
511 let divisor = 10_f64.powi(decimals);
512 let human = wei as f64 / divisor;
513 let symbol = native_symbol(chain);
514 let formatted = format!("≈ {:.6} {}", human, symbol);
515 let high_value = human > 10.0;
517 (formatted, high_value)
518}
519
520fn is_stablecoin(symbol: &str) -> bool {
522 matches!(
523 symbol.to_uppercase().as_str(),
524 "USDC" | "USDT" | "DAI" | "BUSD" | "TUSD" | "USDP" | "FRAX" | "LUSD" | "GUSD"
525 )
526}
527
528struct MetaAnalysis {
530 synthesis: String,
531 key_takeaway: String,
532 recommendations: Vec<String>,
533}
534
535fn meta_analysis_address(
536 is_contract: bool,
537 usd_value: Option<f64>,
538 token_count: usize,
539 risk_score: Option<f32>,
540 risk_level: Option<&crate::compliance::risk::RiskLevel>,
541) -> MetaAnalysis {
542 let mut synthesis_parts = Vec::new();
543 let profile = if is_contract {
544 "contract"
545 } else {
546 "wallet (EOA)"
547 };
548 synthesis_parts.push(format!("A {} on chain.", profile));
549
550 if let Some(usd) = usd_value {
551 if usd > 1_000_000.0 {
552 synthesis_parts.push("Significant value held.".to_string());
553 } else if usd > 10_000.0 {
554 synthesis_parts.push("Moderate value.".to_string());
555 } else if usd < 1.0 {
556 synthesis_parts.push("Minimal value.".to_string());
557 }
558 }
559
560 if token_count > 5 {
561 synthesis_parts.push("Diversified token exposure.".to_string());
562 } else if token_count == 1 && token_count > 0 {
563 synthesis_parts.push("Concentrated in a single token.".to_string());
564 }
565
566 if let (Some(score), Some(level)) = (risk_score, risk_level) {
567 if score >= 7.0 {
568 synthesis_parts.push(format!("Elevated risk ({:?}).", level));
569 } else if score <= 3.0 {
570 synthesis_parts.push("Low risk profile.".to_string());
571 }
572 }
573
574 let synthesis = if synthesis_parts.is_empty() {
575 "Address analyzed with available on-chain data.".to_string()
576 } else {
577 synthesis_parts.join(" ")
578 };
579
580 let key_takeaway = if let (Some(score), Some(level)) = (risk_score, risk_level) {
581 if score >= 7.0 {
582 format!(
583 "Risk assessment warrants closer scrutiny ({:.1}/10).",
584 score
585 )
586 } else {
587 format!("Overall risk: {:?} ({:.1}/10).", level, score)
588 }
589 } else if is_contract {
590 "Contract address — verify intended interaction before use.".to_string()
591 } else if usd_value.map(|u| u > 100_000.0).unwrap_or(false) {
592 "High-value wallet — standard due diligence applies.".to_string()
593 } else {
594 "Review full report for transaction and token details.".to_string()
595 };
596
597 let mut recommendations = Vec::new();
598 if risk_score.map(|s| s >= 6.0).unwrap_or(false) {
599 recommendations.push("Monitor for unusual transaction patterns.".to_string());
600 }
601 if token_count > 0 {
602 recommendations.push("Verify token contracts before large interactions.".to_string());
603 }
604 if is_contract {
605 recommendations.push("Confirm contract source and audit status.".to_string());
606 }
607
608 MetaAnalysis {
609 synthesis,
610 key_takeaway,
611 recommendations,
612 }
613}
614
615fn meta_analysis_tx(
616 tx_type: &str,
617 status: bool,
618 high_value: bool,
619 _from: &str,
620 _to: Option<&str>,
621) -> MetaAnalysis {
622 let mut synthesis_parts = Vec::new();
623
624 if !status {
625 synthesis_parts.push("Transaction failed.".to_string());
626 }
627
628 synthesis_parts.push(format!("{} between parties.", tx_type));
629
630 if high_value {
631 synthesis_parts.push("High-value transfer.".to_string());
632 }
633
634 let synthesis = synthesis_parts.join(" ");
635
636 let key_takeaway = if !status {
637 "Failed transaction — check revert reason and contract state.".to_string()
638 } else if high_value && tx_type == "Native Transfer" {
639 "Large native transfer — verify recipient and intent.".to_string()
640 } else if high_value {
641 "High-value operation — standard verification recommended.".to_string()
642 } else {
643 format!("Routine {} — review full details if needed.", tx_type)
644 };
645
646 let mut recommendations = Vec::new();
647 if !status {
648 recommendations.push("Inspect contract logs for revert reason.".to_string());
649 }
650 if high_value {
651 recommendations.push("Confirm recipient address and amount.".to_string());
652 }
653 if tx_type.contains("Approval") {
654 recommendations.push("Verify approved spender and allowance amount.".to_string());
655 }
656
657 MetaAnalysis {
658 synthesis,
659 key_takeaway,
660 recommendations,
661 }
662}
663
664fn meta_analysis_token(
665 risk_summary: &report::TokenRiskSummary,
666 is_stablecoin: bool,
667 peg_healthy: Option<bool>,
668 top_holder_pct: Option<f64>,
669 liquidity_usd: f64,
670) -> MetaAnalysis {
671 let mut synthesis_parts = Vec::new();
672
673 if risk_summary.score <= 3 {
674 synthesis_parts.push("Low-risk token with healthy metrics.".to_string());
675 } else if risk_summary.score >= 7 {
676 synthesis_parts.push("Elevated risk — multiple concerns identified.".to_string());
677 } else {
678 synthesis_parts.push("Moderate risk — mixed signals.".to_string());
679 }
680
681 if is_stablecoin && let Some(healthy) = peg_healthy {
682 if healthy {
683 synthesis_parts.push("Stablecoin peg is healthy on observed venue.".to_string());
684 } else {
685 synthesis_parts
686 .push("Stablecoin peg deviation detected — verify on multiple venues.".to_string());
687 }
688 }
689
690 if top_holder_pct.map(|p| p > 30.0).unwrap_or(false) {
691 synthesis_parts.push("Concentration risk: top holder holds significant share.".to_string());
692 }
693
694 if liquidity_usd > 1_000_000.0 {
695 synthesis_parts.push("Strong liquidity depth.".to_string());
696 } else if liquidity_usd < 50_000.0 {
697 synthesis_parts.push("Limited liquidity — slippage risk for larger trades.".to_string());
698 }
699
700 let synthesis = synthesis_parts.join(" ");
701
702 let key_takeaway = if risk_summary.score >= 7 {
703 format!(
704 "High risk ({}): {} — exercise caution.",
705 risk_summary.score,
706 risk_summary
707 .concerns
708 .first()
709 .cloned()
710 .unwrap_or_else(|| "multiple factors".to_string())
711 )
712 } else if is_stablecoin && peg_healthy == Some(false) {
713 "Stablecoin deviating from peg — check additional venues before trading.".to_string()
714 } else if !risk_summary.positives.is_empty() && risk_summary.concerns.is_empty() {
715 "Favorable risk profile — standard diligence applies.".to_string()
716 } else {
717 format!(
718 "Risk {}/10 ({}) — weigh concerns against use case.",
719 risk_summary.score, risk_summary.level
720 )
721 };
722
723 let mut recommendations = Vec::new();
724 if risk_summary.score >= 6 {
725 recommendations
726 .push("Consider smaller position sizes or avoid until risk clears.".to_string());
727 }
728 if top_holder_pct.map(|p| p > 25.0).unwrap_or(false) {
729 recommendations.push("Monitor top holder movements for distribution changes.".to_string());
730 }
731 if is_stablecoin && peg_healthy != Some(true) {
732 recommendations.push("Verify peg across multiple DEX/CEX venues.".to_string());
733 }
734 if liquidity_usd < 100_000.0 && risk_summary.score <= 5 {
735 recommendations.push("Use limit orders or split trades to manage slippage.".to_string());
736 }
737
738 MetaAnalysis {
739 synthesis,
740 key_takeaway,
741 recommendations,
742 }
743}
744
745#[cfg(test)]
746mod tests {
747 use super::*;
748 use crate::chains::{
749 Balance as ChainBalance, ChainClient, ChainClientFactory, DexDataSource,
750 Token as ChainToken, TokenBalance as ChainTokenBalance, Transaction as ChainTransaction,
751 };
752 use async_trait::async_trait;
753
754 struct MockChainClient;
759
760 #[async_trait]
761 impl ChainClient for MockChainClient {
762 fn chain_name(&self) -> &str {
763 "ethereum"
764 }
765 fn native_token_symbol(&self) -> &str {
766 "ETH"
767 }
768 async fn get_balance(&self, _address: &str) -> crate::error::Result<ChainBalance> {
769 Ok(ChainBalance {
770 raw: "1000000000000000000".to_string(),
771 formatted: "1.0 ETH".to_string(),
772 decimals: 18,
773 symbol: "ETH".to_string(),
774 usd_value: Some(2500.0),
775 })
776 }
777 async fn enrich_balance_usd(&self, balance: &mut ChainBalance) {
778 balance.usd_value = Some(2500.0);
779 }
780 async fn get_transaction(&self, _hash: &str) -> crate::error::Result<ChainTransaction> {
781 Ok(ChainTransaction {
782 hash: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
783 .to_string(),
784 block_number: Some(12345678),
785 timestamp: Some(1700000000),
786 from: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
787 to: Some("0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string()),
788 value: "1000000000000000000".to_string(),
789 gas_limit: 21000,
790 gas_used: Some(21000),
791 gas_price: "20000000000".to_string(),
792 nonce: 42,
793 input: "0xa9059cbb0000000000000000000000001234".to_string(),
794 status: Some(true),
795 })
796 }
797 async fn get_transactions(
798 &self,
799 _address: &str,
800 _limit: u32,
801 ) -> crate::error::Result<Vec<ChainTransaction>> {
802 Ok(vec![])
803 }
804 async fn get_block_number(&self) -> crate::error::Result<u64> {
805 Ok(12345678)
806 }
807 async fn get_token_balances(
808 &self,
809 _address: &str,
810 ) -> crate::error::Result<Vec<ChainTokenBalance>> {
811 Ok(vec![
812 ChainTokenBalance {
813 token: ChainToken {
814 contract_address: "0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string(),
815 symbol: "USDT".to_string(),
816 name: "Tether USD".to_string(),
817 decimals: 6,
818 },
819 balance: "1000000".to_string(),
820 formatted_balance: "1.0".to_string(),
821 usd_value: Some(1.0),
822 },
823 ChainTokenBalance {
824 token: ChainToken {
825 contract_address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string(),
826 symbol: "USDC".to_string(),
827 name: "USD Coin".to_string(),
828 decimals: 6,
829 },
830 balance: "5000000".to_string(),
831 formatted_balance: "5.0".to_string(),
832 usd_value: Some(5.0),
833 },
834 ])
835 }
836 async fn get_code(&self, _address: &str) -> crate::error::Result<String> {
837 Ok("0x".to_string()) }
839 }
840
841 struct MockFactory;
842
843 impl ChainClientFactory for MockFactory {
844 fn create_chain_client(&self, _chain: &str) -> crate::error::Result<Box<dyn ChainClient>> {
845 Ok(Box::new(MockChainClient))
846 }
847 fn create_dex_client(&self) -> Box<dyn DexDataSource> {
848 crate::chains::DefaultClientFactory {
849 chains_config: Default::default(),
850 }
851 .create_dex_client()
852 }
853 }
854
855 struct MockContractClient;
857
858 #[async_trait]
859 impl ChainClient for MockContractClient {
860 fn chain_name(&self) -> &str {
861 "ethereum"
862 }
863 fn native_token_symbol(&self) -> &str {
864 "ETH"
865 }
866 async fn get_balance(&self, _address: &str) -> crate::error::Result<ChainBalance> {
867 Ok(ChainBalance {
868 raw: "0".to_string(),
869 formatted: "0.0 ETH".to_string(),
870 decimals: 18,
871 symbol: "ETH".to_string(),
872 usd_value: Some(0.0),
873 })
874 }
875 async fn enrich_balance_usd(&self, _balance: &mut ChainBalance) {}
876 async fn get_transaction(&self, hash: &str) -> crate::error::Result<ChainTransaction> {
877 Ok(ChainTransaction {
878 hash: hash.to_string(),
879 block_number: Some(100),
880 timestamp: Some(1700000000),
881 from: "0xfrom".to_string(),
882 to: None, value: "0".to_string(),
884 gas_limit: 100000,
885 gas_used: Some(80000),
886 gas_price: "10000000000".to_string(),
887 nonce: 0,
888 input: "0x60806040".to_string(),
889 status: Some(false), })
891 }
892 async fn get_transactions(
893 &self,
894 _address: &str,
895 _limit: u32,
896 ) -> crate::error::Result<Vec<ChainTransaction>> {
897 Ok(vec![])
898 }
899 async fn get_block_number(&self) -> crate::error::Result<u64> {
900 Ok(100)
901 }
902 async fn get_token_balances(
903 &self,
904 _address: &str,
905 ) -> crate::error::Result<Vec<ChainTokenBalance>> {
906 Ok(vec![])
907 }
908 async fn get_code(&self, _address: &str) -> crate::error::Result<String> {
909 Ok("0x6080604052".to_string()) }
911 }
912
913 struct MockContractFactory;
914
915 impl ChainClientFactory for MockContractFactory {
916 fn create_chain_client(&self, _chain: &str) -> crate::error::Result<Box<dyn ChainClient>> {
917 Ok(Box::new(MockContractClient))
918 }
919 fn create_dex_client(&self) -> Box<dyn DexDataSource> {
920 crate::chains::DefaultClientFactory {
921 chains_config: Default::default(),
922 }
923 .create_dex_client()
924 }
925 }
926
927 struct MockDexDataSource;
929
930 #[async_trait]
931 impl DexDataSource for MockDexDataSource {
932 async fn get_token_price(&self, _chain: &str, _address: &str) -> Option<f64> {
933 Some(1.0)
934 }
935
936 async fn get_native_token_price(&self, _chain: &str) -> Option<f64> {
937 Some(2500.0)
938 }
939
940 async fn get_token_data(
941 &self,
942 _chain: &str,
943 address: &str,
944 ) -> crate::error::Result<crate::chains::dex::DexTokenData> {
945 use crate::chains::{DexPair, PricePoint, VolumePoint};
946 Ok(crate::chains::dex::DexTokenData {
947 address: address.to_string(),
948 symbol: "TEST".to_string(),
949 name: "Test Token".to_string(),
950 price_usd: 1.5,
951 price_change_24h: 5.2,
952 price_change_6h: 2.1,
953 price_change_1h: 0.5,
954 price_change_5m: 0.1,
955 volume_24h: 1_000_000.0,
956 volume_6h: 250_000.0,
957 volume_1h: 50_000.0,
958 liquidity_usd: 500_000.0,
959 market_cap: Some(10_000_000.0),
960 fdv: Some(12_000_000.0),
961 pairs: vec![DexPair {
962 dex_name: "Uniswap V3".to_string(),
963 pair_address: "0xpair123".to_string(),
964 base_token: "TEST".to_string(),
965 quote_token: "USDC".to_string(),
966 price_usd: 1.5,
967 liquidity_usd: 500_000.0,
968 volume_24h: 1_000_000.0,
969 price_change_24h: 5.2,
970 buys_24h: 100,
971 sells_24h: 80,
972 buys_6h: 20,
973 sells_6h: 15,
974 buys_1h: 5,
975 sells_1h: 3,
976 pair_created_at: Some(1690000000),
977 url: Some("https://dexscreener.com/ethereum/0xpair123".to_string()),
978 }],
979 price_history: vec![PricePoint {
980 timestamp: 1690000000,
981 price: 1.5,
982 }],
983 volume_history: vec![VolumePoint {
984 timestamp: 1690000000,
985 volume: 1_000_000.0,
986 }],
987 total_buys_24h: 100,
988 total_sells_24h: 80,
989 total_buys_6h: 20,
990 total_sells_6h: 15,
991 total_buys_1h: 5,
992 total_sells_1h: 3,
993 earliest_pair_created_at: Some(1690000000),
994 image_url: None,
995 websites: Vec::new(),
996 socials: Vec::new(),
997 dexscreener_url: Some("https://dexscreener.com/ethereum/test".to_string()),
998 })
999 }
1000
1001 async fn search_tokens(
1002 &self,
1003 _query: &str,
1004 _chain: Option<&str>,
1005 ) -> crate::error::Result<Vec<crate::chains::TokenSearchResult>> {
1006 Ok(vec![crate::chains::TokenSearchResult {
1007 address: "0xTEST1234567890123456789012345678901234567".to_string(),
1008 symbol: "TEST".to_string(),
1009 name: "Test Token".to_string(),
1010 chain: "ethereum".to_string(),
1011 price_usd: Some(1.5),
1012 volume_24h: 1_000_000.0,
1013 liquidity_usd: 500_000.0,
1014 market_cap: Some(10_000_000.0),
1015 }])
1016 }
1017 }
1018
1019 struct MockTokenChainClient;
1021
1022 #[async_trait]
1023 impl ChainClient for MockTokenChainClient {
1024 fn chain_name(&self) -> &str {
1025 "ethereum"
1026 }
1027 fn native_token_symbol(&self) -> &str {
1028 "ETH"
1029 }
1030 async fn get_balance(&self, _address: &str) -> crate::error::Result<ChainBalance> {
1031 Ok(ChainBalance {
1032 raw: "1000000000000000000".to_string(),
1033 formatted: "1.0 ETH".to_string(),
1034 decimals: 18,
1035 symbol: "ETH".to_string(),
1036 usd_value: Some(2500.0),
1037 })
1038 }
1039 async fn enrich_balance_usd(&self, balance: &mut ChainBalance) {
1040 balance.usd_value = Some(2500.0);
1041 }
1042 async fn get_transaction(&self, _hash: &str) -> crate::error::Result<ChainTransaction> {
1043 Ok(ChainTransaction {
1044 hash: "0xabc123".to_string(),
1045 block_number: Some(12345678),
1046 timestamp: Some(1700000000),
1047 from: "0xfrom".to_string(),
1048 to: Some("0xto".to_string()),
1049 value: "0".to_string(),
1050 gas_limit: 21000,
1051 gas_used: Some(21000),
1052 gas_price: "20000000000".to_string(),
1053 nonce: 42,
1054 input: "0x".to_string(),
1055 status: Some(true),
1056 })
1057 }
1058 async fn get_transactions(
1059 &self,
1060 _address: &str,
1061 _limit: u32,
1062 ) -> crate::error::Result<Vec<ChainTransaction>> {
1063 Ok(vec![])
1064 }
1065 async fn get_block_number(&self) -> crate::error::Result<u64> {
1066 Ok(12345678)
1067 }
1068 async fn get_token_balances(
1069 &self,
1070 _address: &str,
1071 ) -> crate::error::Result<Vec<ChainTokenBalance>> {
1072 Ok(vec![])
1073 }
1074 async fn get_code(&self, _address: &str) -> crate::error::Result<String> {
1075 Ok("0x".to_string())
1076 }
1077 async fn get_token_holders(
1078 &self,
1079 _address: &str,
1080 _limit: u32,
1081 ) -> crate::error::Result<Vec<crate::chains::TokenHolder>> {
1082 Ok(vec![
1084 crate::chains::TokenHolder {
1085 address: "0x1111111111111111111111111111111111111111".to_string(),
1086 balance: "3500000000000000000000000".to_string(),
1087 formatted_balance: "3500000.0".to_string(),
1088 percentage: 35.0, rank: 1,
1090 },
1091 crate::chains::TokenHolder {
1092 address: "0x2222222222222222222222222222222222222222".to_string(),
1093 balance: "1500000000000000000000000".to_string(),
1094 formatted_balance: "1500000.0".to_string(),
1095 percentage: 15.0,
1096 rank: 2,
1097 },
1098 crate::chains::TokenHolder {
1099 address: "0x3333333333333333333333333333333333333333".to_string(),
1100 balance: "1000000000000000000000000".to_string(),
1101 formatted_balance: "1000000.0".to_string(),
1102 percentage: 10.0,
1103 rank: 3,
1104 },
1105 ])
1106 }
1107 }
1108
1109 struct MockTokenFactory;
1111
1112 impl ChainClientFactory for MockTokenFactory {
1113 fn create_chain_client(&self, _chain: &str) -> crate::error::Result<Box<dyn ChainClient>> {
1114 Ok(Box::new(MockTokenChainClient))
1115 }
1116 fn create_dex_client(&self) -> Box<dyn DexDataSource> {
1117 Box::new(MockDexDataSource)
1118 }
1119 }
1120
1121 #[tokio::test]
1126 async fn test_run_address_eoa() {
1127 let config = Config::default();
1128 let factory = MockFactory;
1129 let args = InsightsArgs {
1130 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".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_address_contract() {
1141 let config = Config::default();
1142 let factory = MockContractFactory;
1143 let args = InsightsArgs {
1144 target: "0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string(),
1145 chain: None,
1146 decode: false,
1147 trace: false,
1148 };
1149 let result = run(args, &config, &factory).await;
1150 assert!(result.is_ok());
1151 }
1152
1153 #[tokio::test]
1154 async fn test_run_transaction() {
1155 let config = Config::default();
1156 let factory = MockFactory;
1157 let args = InsightsArgs {
1158 target: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
1159 .to_string(),
1160 chain: None,
1161 decode: false,
1162 trace: false,
1163 };
1164 let result = run(args, &config, &factory).await;
1165 assert!(result.is_ok());
1166 }
1167
1168 #[tokio::test]
1169 async fn test_run_transaction_failed() {
1170 let config = Config::default();
1171 let factory = MockContractFactory;
1172 let args = InsightsArgs {
1173 target: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
1174 .to_string(),
1175 chain: Some("ethereum".to_string()),
1176 decode: true,
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_run_address_with_chain_override() {
1185 let config = Config::default();
1186 let factory = MockFactory;
1187 let args = InsightsArgs {
1188 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
1189 chain: Some("polygon".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() {
1199 let config = Config::default();
1200 let factory = MockTokenFactory;
1201 let args = InsightsArgs {
1202 target: "TEST".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 #[tokio::test]
1212 async fn test_insights_run_token_with_concentration_warning() {
1213 let config = Config::default();
1214 let factory = MockTokenFactory;
1215 let args = InsightsArgs {
1216 target: "0xTEST1234567890123456789012345678901234567".to_string(),
1217 chain: Some("ethereum".to_string()),
1218 decode: false,
1219 trace: false,
1220 };
1221 let result = run(args, &config, &factory).await;
1222 assert!(result.is_ok());
1223 }
1224
1225 #[test]
1230 fn test_infer_target_evm_address() {
1231 let t = infer_target("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", None);
1232 assert!(matches!(t, InferredTarget::Address { chain } if chain == "ethereum"));
1233 }
1234
1235 #[test]
1236 fn test_infer_target_tron_address() {
1237 let t = infer_target("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", None);
1238 assert!(matches!(t, InferredTarget::Address { chain } if chain == "tron"));
1239 }
1240
1241 #[test]
1242 fn test_infer_target_solana_address() {
1243 let t = infer_target("DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy", None);
1244 assert!(matches!(t, InferredTarget::Address { chain } if chain == "solana"));
1245 }
1246
1247 #[test]
1248 fn test_infer_target_evm_tx_hash() {
1249 let t = infer_target(
1250 "0xabc123def456789012345678901234567890123456789012345678901234abcd",
1251 None,
1252 );
1253 assert!(matches!(t, InferredTarget::Transaction { chain } if chain == "ethereum"));
1254 }
1255
1256 #[test]
1257 fn test_infer_target_tron_tx_hash() {
1258 let t = infer_target(
1259 "abc123def456789012345678901234567890123456789012345678901234abcd",
1260 None,
1261 );
1262 assert!(matches!(t, InferredTarget::Transaction { chain } if chain == "tron"));
1263 }
1264
1265 #[test]
1266 fn test_infer_target_token_symbol() {
1267 let t = infer_target("USDC", None);
1268 assert!(matches!(t, InferredTarget::Token { chain } if chain == "ethereum"));
1269 }
1270
1271 #[test]
1272 fn test_infer_target_chain_override() {
1273 let t = infer_target(
1274 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
1275 Some("polygon"),
1276 );
1277 assert!(matches!(t, InferredTarget::Address { chain } if chain == "polygon"));
1278 }
1279
1280 #[test]
1281 fn test_infer_target_token_with_chain_override() {
1282 let t = infer_target("USDC", Some("solana"));
1283 assert!(matches!(t, InferredTarget::Token { chain } if chain == "solana"));
1284 }
1285
1286 #[test]
1287 fn test_classify_tx_type() {
1288 assert_eq!(
1289 classify_tx_type("0xa9059cbb1234...", Some("0xto")),
1290 "ERC-20 Transfer"
1291 );
1292 assert_eq!(
1293 classify_tx_type("0x095ea7b3abcd...", Some("0xto")),
1294 "ERC-20 Approve"
1295 );
1296 assert_eq!(classify_tx_type("0x", Some("0xto")), "Native Transfer");
1297 assert_eq!(classify_tx_type("", None), "Contract Creation");
1298 }
1299
1300 #[test]
1301 fn test_format_tx_value() {
1302 let (fmt, high) = format_tx_value("0xDE0B6B3A7640000", "ethereum"); assert!(fmt.contains("1.0") && fmt.contains("ETH"));
1304 assert!(!high);
1305 let (_, high2) = format_tx_value("0x52B7D2DCC80CD2E4000000", "ethereum"); assert!(high2);
1307 }
1308
1309 #[test]
1310 fn test_is_stablecoin() {
1311 assert!(is_stablecoin("USDC"));
1312 assert!(is_stablecoin("usdt"));
1313 assert!(is_stablecoin("DAI"));
1314 assert!(is_stablecoin("BUSD"));
1315 assert!(is_stablecoin("TUSD"));
1316 assert!(is_stablecoin("USDP"));
1317 assert!(is_stablecoin("FRAX"));
1318 assert!(is_stablecoin("LUSD"));
1319 assert!(is_stablecoin("GUSD"));
1320 assert!(!is_stablecoin("ETH"));
1321 assert!(!is_stablecoin("PEPE"));
1322 assert!(!is_stablecoin("WBTC"));
1323 }
1324
1325 #[test]
1326 fn test_is_stablecoin_empty_string() {
1327 assert!(!is_stablecoin(""));
1328 }
1329
1330 #[test]
1331 fn test_is_stablecoin_case_insensitive() {
1332 assert!(is_stablecoin("UsDc"));
1334 assert!(is_stablecoin("FraX"));
1335 assert!(!is_stablecoin("SOL")); }
1337
1338 #[test]
1343 fn test_target_type_label_address() {
1344 let t = InferredTarget::Address {
1345 chain: "ethereum".to_string(),
1346 };
1347 assert_eq!(target_type_label(&t), "Address");
1348 }
1349
1350 #[test]
1351 fn test_target_type_label_transaction() {
1352 let t = InferredTarget::Transaction {
1353 chain: "ethereum".to_string(),
1354 };
1355 assert_eq!(target_type_label(&t), "Transaction");
1356 }
1357
1358 #[test]
1359 fn test_target_type_label_token() {
1360 let t = InferredTarget::Token {
1361 chain: "ethereum".to_string(),
1362 };
1363 assert_eq!(target_type_label(&t), "Token");
1364 }
1365
1366 #[test]
1367 fn test_chain_label_address() {
1368 let t = InferredTarget::Address {
1369 chain: "polygon".to_string(),
1370 };
1371 assert_eq!(chain_label(&t), "polygon");
1372 }
1373
1374 #[test]
1375 fn test_chain_label_transaction() {
1376 let t = InferredTarget::Transaction {
1377 chain: "tron".to_string(),
1378 };
1379 assert_eq!(chain_label(&t), "tron");
1380 }
1381
1382 #[test]
1383 fn test_chain_label_token() {
1384 let t = InferredTarget::Token {
1385 chain: "solana".to_string(),
1386 };
1387 assert_eq!(chain_label(&t), "solana");
1388 }
1389
1390 #[test]
1395 fn test_classify_tx_type_dex_swaps() {
1396 assert_eq!(
1397 classify_tx_type("0x38ed173900000...", Some("0xrouter")),
1398 "DEX Swap"
1399 );
1400 assert_eq!(
1401 classify_tx_type("0x5c11d79500000...", Some("0xrouter")),
1402 "DEX Swap"
1403 );
1404 assert_eq!(
1405 classify_tx_type("0x4a25d94a00000...", Some("0xrouter")),
1406 "DEX Swap"
1407 );
1408 assert_eq!(
1409 classify_tx_type("0x8803dbee00000...", Some("0xrouter")),
1410 "DEX Swap"
1411 );
1412 assert_eq!(
1413 classify_tx_type("0x7ff36ab500000...", Some("0xrouter")),
1414 "DEX Swap"
1415 );
1416 assert_eq!(
1417 classify_tx_type("0x18cbafe500000...", Some("0xrouter")),
1418 "DEX Swap"
1419 );
1420 assert_eq!(
1421 classify_tx_type("0xfb3bdb4100000...", Some("0xrouter")),
1422 "DEX Swap"
1423 );
1424 assert_eq!(
1425 classify_tx_type("0xb6f9de9500000...", Some("0xrouter")),
1426 "DEX Swap"
1427 );
1428 }
1429
1430 #[test]
1431 fn test_classify_tx_type_multicall() {
1432 assert_eq!(
1433 classify_tx_type("0xac9650d800000...", Some("0xcontract")),
1434 "Multicall"
1435 );
1436 assert_eq!(
1437 classify_tx_type("0x5ae401dc00000...", Some("0xcontract")),
1438 "Multicall"
1439 );
1440 }
1441
1442 #[test]
1443 fn test_classify_tx_type_transfer_from() {
1444 assert_eq!(
1445 classify_tx_type("0x23b872dd00000...", Some("0xtoken")),
1446 "ERC-20 Transfer From"
1447 );
1448 }
1449
1450 #[test]
1451 fn test_classify_tx_type_contract_call() {
1452 assert_eq!(
1453 classify_tx_type("0xdeadbeef00000...", Some("0xcontract")),
1454 "Contract Call"
1455 );
1456 }
1457
1458 #[test]
1459 fn test_classify_tx_type_native_transfer_empty() {
1460 assert_eq!(classify_tx_type("", Some("0xrecipient")), "Native Transfer");
1461 }
1462
1463 #[test]
1468 fn test_format_tx_value_zero() {
1469 let (fmt, high) = format_tx_value("0x0", "ethereum");
1470 assert!(fmt.contains("0.000000"));
1471 assert!(fmt.contains("ETH"));
1472 assert!(!high);
1473 }
1474
1475 #[test]
1476 fn test_format_tx_value_empty_hex() {
1477 let (fmt, high) = format_tx_value("0x", "ethereum");
1478 assert!(fmt.contains("0.000000"));
1479 assert!(!high);
1480 }
1481
1482 #[test]
1483 fn test_format_tx_value_decimal_string() {
1484 let (fmt, high) = format_tx_value("1000000000000000000", "ethereum"); assert!(fmt.contains("1.0"));
1486 assert!(fmt.contains("ETH"));
1487 assert!(!high);
1488 }
1489
1490 #[test]
1491 fn test_format_tx_value_solana() {
1492 let (fmt, high) = format_tx_value("1000000000", "solana"); assert!(fmt.contains("1.0"));
1494 assert!(fmt.contains("SOL"));
1495 assert!(!high);
1496 }
1497
1498 #[test]
1499 fn test_format_tx_value_tron() {
1500 let (fmt, high) = format_tx_value("1000000", "tron"); assert!(fmt.contains("1.0"));
1502 assert!(fmt.contains("TRX"));
1503 assert!(!high);
1504 }
1505
1506 #[test]
1507 fn test_format_tx_value_polygon() {
1508 let (fmt, _) = format_tx_value("1000000000000000000", "polygon");
1509 assert!(fmt.contains("MATIC") || fmt.contains("POL"));
1510 }
1511
1512 #[test]
1513 fn test_format_tx_value_bsc() {
1514 let (fmt, _) = format_tx_value("1000000000000000000", "bsc");
1515 assert!(fmt.contains("BNB"));
1516 }
1517
1518 #[test]
1519 fn test_format_tx_value_arbitrum() {
1520 let (fmt, _) = format_tx_value("1000000000000000000", "arbitrum");
1521 assert!(fmt.contains("ETH"));
1522 }
1523
1524 #[test]
1525 fn test_format_tx_value_optimism() {
1526 let (fmt, _) = format_tx_value("1000000000000000000", "optimism");
1527 assert!(fmt.contains("ETH"));
1528 }
1529
1530 #[test]
1531 fn test_format_tx_value_base() {
1532 let (fmt, _) = format_tx_value("1000000000000000000", "base");
1533 assert!(fmt.contains("ETH"));
1534 }
1535
1536 #[test]
1537 fn test_format_tx_value_aegis() {
1538 let (fmt, _) = format_tx_value("1000000000000000000", "aegis");
1540 assert!(fmt.contains("1.0") || fmt.contains("1.000000"));
1541 }
1542
1543 #[test]
1544 fn test_format_tx_value_unknown_chain_defaults_18_decimals() {
1545 let (fmt, _) = format_tx_value("1000000000000000000", "unknown_chain");
1546 assert!(fmt.contains("1") || fmt.contains("ETH"));
1547 }
1548
1549 #[test]
1550 fn test_format_tx_value_invalid_hex_parse() {
1551 let (fmt, high) = format_tx_value("0xZZZZ", "ethereum");
1552 assert!(fmt.contains("0.000000"));
1553 assert!(!high);
1554 }
1555
1556 #[test]
1557 fn test_format_tx_value_high_value_threshold() {
1558 let (_, high) = format_tx_value("11000000000000000000", "ethereum"); assert!(high);
1561 let (_, high2) = format_tx_value("10000000000000000000", "ethereum"); assert!(!high2); }
1564
1565 #[test]
1570 fn test_meta_analysis_address_contract_high_value() {
1571 let meta = meta_analysis_address(true, Some(2_000_000.0), 10, None, None);
1572 assert!(meta.synthesis.contains("contract"));
1573 assert!(meta.synthesis.contains("Significant value"));
1574 assert!(meta.synthesis.contains("Diversified"));
1575 assert!(meta.recommendations.iter().any(|r| r.contains("contract")));
1576 }
1577
1578 #[test]
1579 fn test_meta_analysis_address_eoa_moderate_value() {
1580 let meta = meta_analysis_address(false, Some(50_000.0), 3, None, None);
1581 assert!(meta.synthesis.contains("wallet (EOA)"));
1582 assert!(meta.synthesis.contains("Moderate value"));
1583 }
1584
1585 #[test]
1586 fn test_meta_analysis_address_minimal_value() {
1587 let meta = meta_analysis_address(false, Some(0.5), 0, None, None);
1588 assert!(meta.synthesis.contains("Minimal value"));
1589 }
1590
1591 #[test]
1592 fn test_meta_analysis_address_single_token() {
1593 let meta = meta_analysis_address(false, None, 1, None, None);
1594 assert!(meta.synthesis.contains("Concentrated in a single token"));
1595 }
1596
1597 #[test]
1598 fn test_meta_analysis_address_high_risk() {
1599 use crate::compliance::risk::RiskLevel;
1600 let level = RiskLevel::High;
1601 let meta = meta_analysis_address(false, None, 0, Some(8.5), Some(&level));
1602 assert!(meta.synthesis.contains("Elevated risk"));
1603 assert!(meta.key_takeaway.contains("scrutiny"));
1604 assert!(
1605 meta.recommendations
1606 .iter()
1607 .any(|r| r.contains("unusual transaction"))
1608 );
1609 }
1610
1611 #[test]
1612 fn test_meta_analysis_address_low_risk() {
1613 use crate::compliance::risk::RiskLevel;
1614 let level = RiskLevel::Low;
1615 let meta = meta_analysis_address(false, None, 0, Some(2.0), Some(&level));
1616 assert!(meta.synthesis.contains("Low risk"));
1617 }
1618
1619 #[test]
1620 fn test_meta_analysis_address_contract_no_value() {
1621 let meta = meta_analysis_address(true, None, 0, None, None);
1622 assert!(meta.key_takeaway.contains("Contract address"));
1623 assert!(
1624 meta.recommendations
1625 .iter()
1626 .any(|r| r.contains("Confirm contract"))
1627 );
1628 }
1629
1630 #[test]
1631 fn test_meta_analysis_address_high_value_wallet() {
1632 let meta = meta_analysis_address(false, Some(150_000.0), 0, None, None);
1633 assert!(meta.key_takeaway.contains("High-value wallet"));
1634 }
1635
1636 #[test]
1637 fn test_meta_analysis_address_default_takeaway() {
1638 let meta = meta_analysis_address(false, Some(5_000.0), 0, None, None);
1639 assert!(meta.key_takeaway.contains("Review full report"));
1640 }
1641
1642 #[test]
1643 fn test_meta_analysis_address_empty_synthesis() {
1644 let meta = meta_analysis_address(
1645 false,
1646 Some(5_000.0), 2, None,
1649 None,
1650 );
1651 assert!(meta.synthesis.contains("wallet (EOA)"));
1652 }
1653
1654 #[test]
1655 fn test_meta_analysis_address_synthesis_parts_joined() {
1656 let meta = meta_analysis_address(false, None, 0, None, None);
1657 assert!(!meta.synthesis.is_empty());
1658 assert!(meta.synthesis.contains("Address analyzed") || meta.synthesis.contains("wallet"));
1659 }
1660
1661 #[test]
1662 fn test_meta_analysis_address_with_tokens_recommendation() {
1663 let meta = meta_analysis_address(false, None, 3, None, None);
1664 assert!(
1665 meta.recommendations
1666 .iter()
1667 .any(|r| r.contains("Verify token contracts"))
1668 );
1669 }
1670
1671 #[test]
1676 fn test_meta_analysis_tx_successful_native_transfer() {
1677 let meta = meta_analysis_tx("Native Transfer", true, false, "0xfrom", Some("0xto"));
1678 assert!(meta.synthesis.contains("Native Transfer"));
1679 assert!(meta.key_takeaway.contains("Routine"));
1680 assert!(meta.recommendations.is_empty());
1681 }
1682
1683 #[test]
1684 fn test_meta_analysis_tx_failed() {
1685 let meta = meta_analysis_tx("Contract Call", false, false, "0xfrom", Some("0xto"));
1686 assert!(meta.synthesis.contains("failed"));
1687 assert!(meta.key_takeaway.contains("Failed transaction"));
1688 assert!(meta.recommendations.iter().any(|r| r.contains("revert")));
1689 }
1690
1691 #[test]
1692 fn test_meta_analysis_tx_high_value_native() {
1693 let meta = meta_analysis_tx("Native Transfer", true, true, "0xfrom", Some("0xto"));
1694 assert!(meta.synthesis.contains("High-value"));
1695 assert!(meta.key_takeaway.contains("Large native transfer"));
1696 assert!(meta.recommendations.iter().any(|r| r.contains("recipient")));
1697 }
1698
1699 #[test]
1700 fn test_meta_analysis_tx_high_value_contract_call() {
1701 let meta = meta_analysis_tx("DEX Swap", true, true, "0xfrom", Some("0xto"));
1702 assert!(meta.key_takeaway.contains("High-value operation"));
1703 }
1704
1705 #[test]
1706 fn test_meta_analysis_tx_erc20_approve() {
1707 let meta = meta_analysis_tx("ERC-20 Approval", true, false, "0xfrom", Some("0xto"));
1708 assert!(meta.recommendations.iter().any(|r| r.contains("spender")));
1709 }
1710
1711 #[test]
1712 fn test_meta_analysis_tx_failed_high_value() {
1713 let meta = meta_analysis_tx("Contract Call", false, true, "0xfrom", Some("0xto"));
1714 assert!(meta.synthesis.contains("failed"));
1715 assert!(meta.synthesis.contains("High-value"));
1716 assert!(meta.recommendations.len() >= 2);
1717 }
1718
1719 #[test]
1724 fn test_meta_analysis_token_low_risk() {
1725 let summary = report::TokenRiskSummary {
1726 score: 2,
1727 level: "Low",
1728 emoji: "🟢",
1729 concerns: vec![],
1730 positives: vec!["Good liquidity".to_string()],
1731 };
1732 let meta = meta_analysis_token(&summary, false, None, None, 2_000_000.0);
1733 assert!(meta.synthesis.contains("Low-risk"));
1734 assert!(meta.synthesis.contains("Strong liquidity"));
1735 assert!(meta.key_takeaway.contains("Favorable"));
1736 }
1737
1738 #[test]
1739 fn test_meta_analysis_token_high_risk() {
1740 let summary = report::TokenRiskSummary {
1741 score: 8,
1742 level: "High",
1743 emoji: "🔴",
1744 concerns: vec!["Low liquidity".to_string()],
1745 positives: vec![],
1746 };
1747 let meta = meta_analysis_token(&summary, false, None, None, 10_000.0);
1748 assert!(meta.synthesis.contains("Elevated risk"));
1749 assert!(meta.synthesis.contains("Limited liquidity"));
1750 assert!(meta.key_takeaway.contains("High risk"));
1751 assert!(
1752 meta.recommendations
1753 .iter()
1754 .any(|r| r.contains("smaller position"))
1755 );
1756 }
1757
1758 #[test]
1759 fn test_meta_analysis_token_moderate_risk() {
1760 let summary = report::TokenRiskSummary {
1761 score: 5,
1762 level: "Medium",
1763 emoji: "🟡",
1764 concerns: vec!["Some concern".to_string()],
1765 positives: vec!["Some positive".to_string()],
1766 };
1767 let meta = meta_analysis_token(&summary, false, None, None, 500_000.0);
1768 assert!(meta.synthesis.contains("Moderate risk"));
1769 assert!(meta.key_takeaway.contains("Risk 5/10"));
1770 }
1771
1772 #[test]
1773 fn test_meta_analysis_token_stablecoin_healthy_peg() {
1774 let summary = report::TokenRiskSummary {
1775 score: 2,
1776 level: "Low",
1777 emoji: "🟢",
1778 concerns: vec![],
1779 positives: vec!["Stable peg".to_string()],
1780 };
1781 let meta = meta_analysis_token(&summary, true, Some(true), None, 5_000_000.0);
1782 assert!(meta.synthesis.contains("Stablecoin peg is healthy"));
1783 }
1784
1785 #[test]
1786 fn test_meta_analysis_token_stablecoin_unhealthy_peg() {
1787 let summary = report::TokenRiskSummary {
1788 score: 4,
1789 level: "Medium",
1790 emoji: "🟡",
1791 concerns: vec![],
1792 positives: vec![],
1793 };
1794 let meta = meta_analysis_token(&summary, true, Some(false), None, 500_000.0);
1795 assert!(meta.synthesis.contains("peg deviation"));
1796 assert!(meta.key_takeaway.contains("deviating from peg"));
1797 assert!(meta.recommendations.iter().any(|r| r.contains("peg")));
1798 }
1799
1800 #[test]
1801 fn test_meta_analysis_token_concentration_risk() {
1802 let summary = report::TokenRiskSummary {
1803 score: 5,
1804 level: "Medium",
1805 emoji: "🟡",
1806 concerns: vec![],
1807 positives: vec![],
1808 };
1809 let meta = meta_analysis_token(&summary, false, None, Some(45.0), 500_000.0);
1810 assert!(meta.synthesis.contains("Concentration risk"));
1811 assert!(
1812 meta.recommendations
1813 .iter()
1814 .any(|r| r.contains("top holder"))
1815 );
1816 }
1817
1818 #[test]
1819 fn test_meta_analysis_token_low_liquidity_low_risk() {
1820 let summary = report::TokenRiskSummary {
1821 score: 3,
1822 level: "Low",
1823 emoji: "🟢",
1824 concerns: vec![],
1825 positives: vec![],
1826 };
1827 let meta = meta_analysis_token(&summary, false, None, None, 50_000.0);
1828 assert!(
1829 meta.recommendations
1830 .iter()
1831 .any(|r| r.contains("limit orders") || r.contains("slippage"))
1832 );
1833 }
1834
1835 #[test]
1836 fn test_meta_analysis_token_stablecoin_no_peg_data() {
1837 let summary = report::TokenRiskSummary {
1838 score: 3,
1839 level: "Low",
1840 emoji: "🟢",
1841 concerns: vec![],
1842 positives: vec![],
1843 };
1844 let meta = meta_analysis_token(&summary, true, None, None, 1_000_000.0);
1845 assert!(meta.recommendations.iter().any(|r| r.contains("peg")));
1847 }
1848
1849 #[test]
1850 fn test_meta_analysis_token_mixed_signals_key_takeaway() {
1851 let summary = report::TokenRiskSummary {
1852 score: 5,
1853 level: "Medium",
1854 emoji: "🟡",
1855 concerns: vec!["Some concern".to_string()],
1856 positives: vec!["Some positive".to_string()],
1857 };
1858 let meta = meta_analysis_token(&summary, false, None, None, 500_000.0);
1859 assert!(meta.key_takeaway.contains("Risk 5/10"));
1860 assert!(meta.key_takeaway.contains("Medium"));
1861 }
1862
1863 #[test]
1868 fn test_infer_target_tx_hash_with_chain_override() {
1869 let t = infer_target(
1870 "0xabc123def456789012345678901234567890123456789012345678901234abcd",
1871 Some("polygon"),
1872 );
1873 assert!(matches!(t, InferredTarget::Transaction { chain } if chain == "polygon"));
1874 }
1875
1876 #[test]
1877 fn test_infer_target_whitespace_trimming() {
1878 let t = infer_target(" USDC ", None);
1879 assert!(matches!(t, InferredTarget::Token { .. }));
1880 }
1881
1882 #[test]
1883 fn test_infer_target_long_token_name() {
1884 let t = infer_target("some-random-token-name", None);
1885 assert!(matches!(t, InferredTarget::Token { chain } if chain == "ethereum"));
1886 }
1887
1888 #[test]
1893 fn test_insights_args_debug() {
1894 let args = InsightsArgs {
1895 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
1896 chain: Some("ethereum".to_string()),
1897 decode: true,
1898 trace: false,
1899 };
1900 let debug_str = format!("{:?}", args);
1901 assert!(debug_str.contains("InsightsArgs"));
1902 assert!(debug_str.contains("0x742d"));
1903 }
1904
1905 #[test]
1910 fn test_classify_tx_type_contract_creation() {
1911 assert_eq!(classify_tx_type("0xa9059cbb...", None), "Contract Creation");
1912 }
1913
1914 #[test]
1915 fn test_classify_tx_type_erc20_transfer() {
1916 assert_eq!(
1917 classify_tx_type("0xa9059cbb00000000", Some("0x1234")),
1918 "ERC-20 Transfer"
1919 );
1920 }
1921
1922 #[test]
1923 fn test_classify_tx_type_erc20_approve() {
1924 assert_eq!(
1925 classify_tx_type("0x095ea7b3...", Some("0x1234")),
1926 "ERC-20 Approve"
1927 );
1928 }
1929
1930 #[test]
1931 fn test_classify_tx_type_erc20_transfer_from() {
1932 assert_eq!(
1933 classify_tx_type("0x23b872dd...", Some("0x1234")),
1934 "ERC-20 Transfer From"
1935 );
1936 }
1937
1938 #[test]
1939 fn test_classify_tx_type_dex_swap() {
1940 assert_eq!(
1941 classify_tx_type("0x38ed1739...", Some("0x1234")),
1942 "DEX Swap"
1943 );
1944 assert_eq!(
1945 classify_tx_type("0x7ff36ab5...", Some("0x1234")),
1946 "DEX Swap"
1947 );
1948 }
1949
1950 #[test]
1951 fn test_classify_tx_type_native_transfer() {
1952 assert_eq!(classify_tx_type("0x", Some("0x1234")), "Native Transfer");
1953 assert_eq!(classify_tx_type("", Some("0x1234")), "Native Transfer");
1954 }
1955
1956 #[test]
1957 fn test_classify_tx_type_unknown_contract_call() {
1958 assert_eq!(
1959 classify_tx_type("0xdeadbeef12345678", Some("0x1234")),
1960 "Contract Call"
1961 );
1962 }
1963
1964 #[test]
1969 fn test_format_tx_value_ethereum_wei() {
1970 let (fmt, high) = format_tx_value("1000000000000000000", "ethereum");
1971 assert!(fmt.contains("1.000000"));
1972 assert!(fmt.contains("ETH"));
1973 assert!(!high); }
1975
1976 #[test]
1977 fn test_format_tx_value_hex() {
1978 let (fmt, _) = format_tx_value("0xde0b6b3a7640000", "ethereum");
1979 assert!(fmt.contains("ETH"));
1981 }
1982
1983 #[test]
1984 fn test_format_tx_value_high_value() {
1985 let (_, high) = format_tx_value("100000000000000000000", "ethereum");
1987 assert!(high); }
1989
1990 #[test]
1991 fn test_format_tx_value_zero_decimal() {
1992 let (fmt, high) = format_tx_value("0", "ethereum");
1993 assert!(fmt.contains("0.000000"));
1994 assert!(!high);
1995 }
1996
1997 #[test]
1998 fn test_format_tx_value_solana_additional() {
1999 let (fmt, _) = format_tx_value("1000000000", "solana"); assert!(fmt.contains("SOL"));
2001 }
2002
2003 #[test]
2004 fn test_format_tx_value_tron_additional() {
2005 let (fmt, _) = format_tx_value("1000000", "tron"); assert!(fmt.contains("TRX"));
2007 }
2008
2009 #[test]
2010 fn test_format_tx_value_empty_hex_additional() {
2011 let (fmt, _) = format_tx_value("0x", "ethereum");
2012 assert!(fmt.contains("0.000000"));
2013 }
2014
2015 #[test]
2020 fn test_target_type_label_combined() {
2021 assert_eq!(
2022 target_type_label(&InferredTarget::Address {
2023 chain: "eth".to_string()
2024 }),
2025 "Address"
2026 );
2027 assert_eq!(
2028 target_type_label(&InferredTarget::Transaction {
2029 chain: "eth".to_string()
2030 }),
2031 "Transaction"
2032 );
2033 assert_eq!(
2034 target_type_label(&InferredTarget::Token {
2035 chain: "eth".to_string()
2036 }),
2037 "Token"
2038 );
2039 }
2040
2041 #[test]
2042 fn test_chain_label_combined() {
2043 assert_eq!(
2044 chain_label(&InferredTarget::Address {
2045 chain: "ethereum".to_string()
2046 }),
2047 "ethereum"
2048 );
2049 assert_eq!(
2050 chain_label(&InferredTarget::Transaction {
2051 chain: "polygon".to_string()
2052 }),
2053 "polygon"
2054 );
2055 assert_eq!(
2056 chain_label(&InferredTarget::Token {
2057 chain: "solana".to_string()
2058 }),
2059 "solana"
2060 );
2061 }
2062
2063 #[test]
2068 fn test_meta_analysis_address_contract_high_risk() {
2069 use crate::compliance::risk::RiskLevel;
2070 let meta = meta_analysis_address(
2071 true,
2072 Some(2_000_000.0),
2073 10,
2074 Some(8.0),
2075 Some(&RiskLevel::High),
2076 );
2077 assert!(meta.synthesis.contains("contract"));
2078 assert!(meta.synthesis.contains("Significant value"));
2079 assert!(meta.key_takeaway.contains("scrutiny"));
2080 assert!(!meta.recommendations.is_empty());
2081 }
2082
2083 #[test]
2084 fn test_meta_analysis_address_wallet_low_risk() {
2085 use crate::compliance::risk::RiskLevel;
2086 let meta = meta_analysis_address(false, Some(0.5), 0, Some(2.0), Some(&RiskLevel::Low));
2087 assert!(meta.synthesis.contains("wallet"));
2088 assert!(meta.synthesis.contains("Minimal value"));
2089 }
2090
2091 #[test]
2092 fn test_meta_analysis_address_no_risk_data() {
2093 let meta = meta_analysis_address(false, None, 0, None, None);
2094 assert!(!meta.synthesis.is_empty());
2095 assert!(meta.key_takeaway.contains("Review full report"));
2096 }
2097
2098 #[test]
2103 fn test_meta_analysis_tx_failed_additional() {
2104 let meta = meta_analysis_tx("Contract Call", false, false, "0x...", Some("0x..."));
2105 assert!(meta.synthesis.contains("failed"));
2106 assert!(meta.key_takeaway.contains("Failed"));
2107 }
2108
2109 #[test]
2110 fn test_meta_analysis_tx_high_value_native_additional() {
2111 let meta = meta_analysis_tx("Native Transfer", true, true, "0x...", Some("0x..."));
2112 assert!(meta.synthesis.contains("High-value"));
2113 assert!(meta.key_takeaway.contains("Large native transfer"));
2114 }
2115
2116 #[test]
2117 fn test_meta_analysis_tx_routine() {
2118 let meta = meta_analysis_tx("ERC-20 Transfer", true, false, "0x...", Some("0x..."));
2119 assert!(meta.key_takeaway.contains("Routine"));
2120 }
2121
2122 #[test]
2127 fn test_meta_analysis_token_high_risk_additional() {
2128 let risk = report::TokenRiskSummary {
2129 score: 8,
2130 level: "High",
2131 emoji: "🔴",
2132 concerns: vec!["Low liquidity".to_string()],
2133 positives: vec![],
2134 };
2135 let meta = meta_analysis_token(&risk, false, None, None, 10_000.0);
2136 assert!(meta.synthesis.contains("Elevated risk"));
2137 assert!(meta.key_takeaway.contains("High risk"));
2138 }
2139
2140 #[test]
2141 fn test_meta_analysis_token_stablecoin_peg_healthy() {
2142 let risk = report::TokenRiskSummary {
2143 score: 2,
2144 level: "Low",
2145 emoji: "🟢",
2146 concerns: vec![],
2147 positives: vec!["Strong liquidity".to_string()],
2148 };
2149 let meta = meta_analysis_token(&risk, true, Some(true), Some(5.0), 5_000_000.0);
2150 assert!(meta.synthesis.contains("peg is healthy"));
2151 assert!(meta.synthesis.contains("Strong liquidity"));
2152 }
2153
2154 #[test]
2155 fn test_meta_analysis_token_stablecoin_peg_unhealthy() {
2156 let risk = report::TokenRiskSummary {
2157 score: 5,
2158 level: "Medium",
2159 emoji: "🟡",
2160 concerns: vec!["Peg deviation".to_string()],
2161 positives: vec![],
2162 };
2163 let meta = meta_analysis_token(&risk, true, Some(false), Some(40.0), 100_000.0);
2164 assert!(meta.synthesis.contains("peg deviation"));
2165 assert!(meta.synthesis.contains("Concentration risk"));
2166 }
2167
2168 #[test]
2169 fn test_meta_analysis_token_high_risk_empty_concerns_uses_multiple_factors() {
2170 let risk = report::TokenRiskSummary {
2171 score: 8,
2172 level: "High",
2173 emoji: "🔴",
2174 concerns: vec![],
2175 positives: vec![],
2176 };
2177 let meta = meta_analysis_token(&risk, false, None, None, 50_000.0);
2178 assert!(
2179 meta.key_takeaway.contains("multiple factors")
2180 || meta.key_takeaway.contains("High risk")
2181 );
2182 }
2183
2184 #[test]
2185 fn test_infer_target_chain_override_with_address() {
2186 let t = infer_target(
2187 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
2188 Some("arbitrum"),
2189 );
2190 assert!(matches!(t, InferredTarget::Address { chain } if chain == "arbitrum"));
2191 }
2192
2193 #[test]
2194 fn test_meta_analysis_tx_approval_contains_approval_in_match() {
2195 let meta = meta_analysis_tx("ERC-20 Approval", true, false, "0xfrom", Some("0xto"));
2196 assert!(
2197 meta.recommendations
2198 .iter()
2199 .any(|r| r.contains("spender") || r.contains("allowance"))
2200 );
2201 }
2202}