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 let http: std::sync::Arc<dyn crate::http::HttpClient> =
849 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
850 crate::chains::DefaultClientFactory {
851 chains_config: Default::default(),
852 http,
853 }
854 .create_dex_client()
855 }
856 }
857
858 struct MockContractClient;
860
861 #[async_trait]
862 impl ChainClient for MockContractClient {
863 fn chain_name(&self) -> &str {
864 "ethereum"
865 }
866 fn native_token_symbol(&self) -> &str {
867 "ETH"
868 }
869 async fn get_balance(&self, _address: &str) -> crate::error::Result<ChainBalance> {
870 Ok(ChainBalance {
871 raw: "0".to_string(),
872 formatted: "0.0 ETH".to_string(),
873 decimals: 18,
874 symbol: "ETH".to_string(),
875 usd_value: Some(0.0),
876 })
877 }
878 async fn enrich_balance_usd(&self, _balance: &mut ChainBalance) {}
879 async fn get_transaction(&self, hash: &str) -> crate::error::Result<ChainTransaction> {
880 Ok(ChainTransaction {
881 hash: hash.to_string(),
882 block_number: Some(100),
883 timestamp: Some(1700000000),
884 from: "0xfrom".to_string(),
885 to: None, value: "0".to_string(),
887 gas_limit: 100000,
888 gas_used: Some(80000),
889 gas_price: "10000000000".to_string(),
890 nonce: 0,
891 input: "0x60806040".to_string(),
892 status: Some(false), })
894 }
895 async fn get_transactions(
896 &self,
897 _address: &str,
898 _limit: u32,
899 ) -> crate::error::Result<Vec<ChainTransaction>> {
900 Ok(vec![])
901 }
902 async fn get_block_number(&self) -> crate::error::Result<u64> {
903 Ok(100)
904 }
905 async fn get_token_balances(
906 &self,
907 _address: &str,
908 ) -> crate::error::Result<Vec<ChainTokenBalance>> {
909 Ok(vec![])
910 }
911 async fn get_code(&self, _address: &str) -> crate::error::Result<String> {
912 Ok("0x6080604052".to_string()) }
914 }
915
916 struct MockContractFactory;
917
918 impl ChainClientFactory for MockContractFactory {
919 fn create_chain_client(&self, _chain: &str) -> crate::error::Result<Box<dyn ChainClient>> {
920 Ok(Box::new(MockContractClient))
921 }
922 fn create_dex_client(&self) -> Box<dyn DexDataSource> {
923 let http: std::sync::Arc<dyn crate::http::HttpClient> =
924 std::sync::Arc::new(crate::http::NativeHttpClient::new().unwrap());
925 crate::chains::DefaultClientFactory {
926 chains_config: Default::default(),
927 http,
928 }
929 .create_dex_client()
930 }
931 }
932
933 struct MockDexDataSource;
935
936 #[async_trait]
937 impl DexDataSource for MockDexDataSource {
938 async fn get_token_price(&self, _chain: &str, _address: &str) -> Option<f64> {
939 Some(1.0)
940 }
941
942 async fn get_native_token_price(&self, _chain: &str) -> Option<f64> {
943 Some(2500.0)
944 }
945
946 async fn get_token_data(
947 &self,
948 _chain: &str,
949 address: &str,
950 ) -> crate::error::Result<crate::chains::dex::DexTokenData> {
951 use crate::chains::{DexPair, PricePoint, VolumePoint};
952 Ok(crate::chains::dex::DexTokenData {
953 address: address.to_string(),
954 symbol: "TEST".to_string(),
955 name: "Test Token".to_string(),
956 price_usd: 1.5,
957 price_change_24h: 5.2,
958 price_change_6h: 2.1,
959 price_change_1h: 0.5,
960 price_change_5m: 0.1,
961 volume_24h: 1_000_000.0,
962 volume_6h: 250_000.0,
963 volume_1h: 50_000.0,
964 liquidity_usd: 500_000.0,
965 market_cap: Some(10_000_000.0),
966 fdv: Some(12_000_000.0),
967 pairs: vec![DexPair {
968 dex_name: "Uniswap V3".to_string(),
969 pair_address: "0xpair123".to_string(),
970 base_token: "TEST".to_string(),
971 quote_token: "USDC".to_string(),
972 price_usd: 1.5,
973 liquidity_usd: 500_000.0,
974 volume_24h: 1_000_000.0,
975 price_change_24h: 5.2,
976 buys_24h: 100,
977 sells_24h: 80,
978 buys_6h: 20,
979 sells_6h: 15,
980 buys_1h: 5,
981 sells_1h: 3,
982 pair_created_at: Some(1690000000),
983 url: Some("https://dexscreener.com/ethereum/0xpair123".to_string()),
984 }],
985 price_history: vec![PricePoint {
986 timestamp: 1690000000,
987 price: 1.5,
988 }],
989 volume_history: vec![VolumePoint {
990 timestamp: 1690000000,
991 volume: 1_000_000.0,
992 }],
993 total_buys_24h: 100,
994 total_sells_24h: 80,
995 total_buys_6h: 20,
996 total_sells_6h: 15,
997 total_buys_1h: 5,
998 total_sells_1h: 3,
999 earliest_pair_created_at: Some(1690000000),
1000 image_url: None,
1001 websites: Vec::new(),
1002 socials: Vec::new(),
1003 dexscreener_url: Some("https://dexscreener.com/ethereum/test".to_string()),
1004 })
1005 }
1006
1007 async fn search_tokens(
1008 &self,
1009 _query: &str,
1010 _chain: Option<&str>,
1011 ) -> crate::error::Result<Vec<crate::chains::TokenSearchResult>> {
1012 Ok(vec![crate::chains::TokenSearchResult {
1013 address: "0xTEST1234567890123456789012345678901234567".to_string(),
1014 symbol: "TEST".to_string(),
1015 name: "Test Token".to_string(),
1016 chain: "ethereum".to_string(),
1017 price_usd: Some(1.5),
1018 volume_24h: 1_000_000.0,
1019 liquidity_usd: 500_000.0,
1020 market_cap: Some(10_000_000.0),
1021 }])
1022 }
1023 }
1024
1025 struct MockTokenChainClient;
1027
1028 #[async_trait]
1029 impl ChainClient for MockTokenChainClient {
1030 fn chain_name(&self) -> &str {
1031 "ethereum"
1032 }
1033 fn native_token_symbol(&self) -> &str {
1034 "ETH"
1035 }
1036 async fn get_balance(&self, _address: &str) -> crate::error::Result<ChainBalance> {
1037 Ok(ChainBalance {
1038 raw: "1000000000000000000".to_string(),
1039 formatted: "1.0 ETH".to_string(),
1040 decimals: 18,
1041 symbol: "ETH".to_string(),
1042 usd_value: Some(2500.0),
1043 })
1044 }
1045 async fn enrich_balance_usd(&self, balance: &mut ChainBalance) {
1046 balance.usd_value = Some(2500.0);
1047 }
1048 async fn get_transaction(&self, _hash: &str) -> crate::error::Result<ChainTransaction> {
1049 Ok(ChainTransaction {
1050 hash: "0xabc123".to_string(),
1051 block_number: Some(12345678),
1052 timestamp: Some(1700000000),
1053 from: "0xfrom".to_string(),
1054 to: Some("0xto".to_string()),
1055 value: "0".to_string(),
1056 gas_limit: 21000,
1057 gas_used: Some(21000),
1058 gas_price: "20000000000".to_string(),
1059 nonce: 42,
1060 input: "0x".to_string(),
1061 status: Some(true),
1062 })
1063 }
1064 async fn get_transactions(
1065 &self,
1066 _address: &str,
1067 _limit: u32,
1068 ) -> crate::error::Result<Vec<ChainTransaction>> {
1069 Ok(vec![])
1070 }
1071 async fn get_block_number(&self) -> crate::error::Result<u64> {
1072 Ok(12345678)
1073 }
1074 async fn get_token_balances(
1075 &self,
1076 _address: &str,
1077 ) -> crate::error::Result<Vec<ChainTokenBalance>> {
1078 Ok(vec![])
1079 }
1080 async fn get_code(&self, _address: &str) -> crate::error::Result<String> {
1081 Ok("0x".to_string())
1082 }
1083 async fn get_token_holders(
1084 &self,
1085 _address: &str,
1086 _limit: u32,
1087 ) -> crate::error::Result<Vec<crate::chains::TokenHolder>> {
1088 Ok(vec![
1090 crate::chains::TokenHolder {
1091 address: "0x1111111111111111111111111111111111111111".to_string(),
1092 balance: "3500000000000000000000000".to_string(),
1093 formatted_balance: "3500000.0".to_string(),
1094 percentage: 35.0, rank: 1,
1096 },
1097 crate::chains::TokenHolder {
1098 address: "0x2222222222222222222222222222222222222222".to_string(),
1099 balance: "1500000000000000000000000".to_string(),
1100 formatted_balance: "1500000.0".to_string(),
1101 percentage: 15.0,
1102 rank: 2,
1103 },
1104 crate::chains::TokenHolder {
1105 address: "0x3333333333333333333333333333333333333333".to_string(),
1106 balance: "1000000000000000000000000".to_string(),
1107 formatted_balance: "1000000.0".to_string(),
1108 percentage: 10.0,
1109 rank: 3,
1110 },
1111 ])
1112 }
1113 }
1114
1115 struct MockTokenFactory;
1117
1118 impl ChainClientFactory for MockTokenFactory {
1119 fn create_chain_client(&self, _chain: &str) -> crate::error::Result<Box<dyn ChainClient>> {
1120 Ok(Box::new(MockTokenChainClient))
1121 }
1122 fn create_dex_client(&self) -> Box<dyn DexDataSource> {
1123 Box::new(MockDexDataSource)
1124 }
1125 }
1126
1127 #[tokio::test]
1132 async fn test_run_address_eoa() {
1133 let config = Config::default();
1134 let factory = MockFactory;
1135 let args = InsightsArgs {
1136 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
1137 chain: None,
1138 decode: false,
1139 trace: false,
1140 };
1141 let result = run(args, &config, &factory).await;
1142 assert!(result.is_ok());
1143 }
1144
1145 #[tokio::test]
1146 async fn test_run_address_contract() {
1147 let config = Config::default();
1148 let factory = MockContractFactory;
1149 let args = InsightsArgs {
1150 target: "0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string(),
1151 chain: None,
1152 decode: false,
1153 trace: false,
1154 };
1155 let result = run(args, &config, &factory).await;
1156 assert!(result.is_ok());
1157 }
1158
1159 #[tokio::test]
1160 async fn test_run_transaction() {
1161 let config = Config::default();
1162 let factory = MockFactory;
1163 let args = InsightsArgs {
1164 target: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
1165 .to_string(),
1166 chain: None,
1167 decode: false,
1168 trace: false,
1169 };
1170 let result = run(args, &config, &factory).await;
1171 assert!(result.is_ok());
1172 }
1173
1174 #[tokio::test]
1175 async fn test_run_transaction_failed() {
1176 let config = Config::default();
1177 let factory = MockContractFactory;
1178 let args = InsightsArgs {
1179 target: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
1180 .to_string(),
1181 chain: Some("ethereum".to_string()),
1182 decode: true,
1183 trace: false,
1184 };
1185 let result = run(args, &config, &factory).await;
1186 assert!(result.is_ok());
1187 }
1188
1189 #[tokio::test]
1190 async fn test_run_address_with_chain_override() {
1191 let config = Config::default();
1192 let factory = MockFactory;
1193 let args = InsightsArgs {
1194 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
1195 chain: Some("polygon".to_string()),
1196 decode: false,
1197 trace: false,
1198 };
1199 let result = run(args, &config, &factory).await;
1200 assert!(result.is_ok());
1201 }
1202
1203 #[tokio::test]
1204 async fn test_insights_run_token() {
1205 let config = Config::default();
1206 let factory = MockTokenFactory;
1207 let args = InsightsArgs {
1208 target: "TEST".to_string(),
1209 chain: Some("ethereum".to_string()),
1210 decode: false,
1211 trace: false,
1212 };
1213 let result = run(args, &config, &factory).await;
1214 assert!(result.is_ok());
1215 }
1216
1217 #[tokio::test]
1218 async fn test_insights_run_token_with_concentration_warning() {
1219 let config = Config::default();
1220 let factory = MockTokenFactory;
1221 let args = InsightsArgs {
1222 target: "0xTEST1234567890123456789012345678901234567".to_string(),
1223 chain: Some("ethereum".to_string()),
1224 decode: false,
1225 trace: false,
1226 };
1227 let result = run(args, &config, &factory).await;
1228 assert!(result.is_ok());
1229 }
1230
1231 #[test]
1236 fn test_infer_target_evm_address() {
1237 let t = infer_target("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", None);
1238 assert!(matches!(t, InferredTarget::Address { chain } if chain == "ethereum"));
1239 }
1240
1241 #[test]
1242 fn test_infer_target_tron_address() {
1243 let t = infer_target("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", None);
1244 assert!(matches!(t, InferredTarget::Address { chain } if chain == "tron"));
1245 }
1246
1247 #[test]
1248 fn test_infer_target_solana_address() {
1249 let t = infer_target("DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy", None);
1250 assert!(matches!(t, InferredTarget::Address { chain } if chain == "solana"));
1251 }
1252
1253 #[test]
1254 fn test_infer_target_evm_tx_hash() {
1255 let t = infer_target(
1256 "0xabc123def456789012345678901234567890123456789012345678901234abcd",
1257 None,
1258 );
1259 assert!(matches!(t, InferredTarget::Transaction { chain } if chain == "ethereum"));
1260 }
1261
1262 #[test]
1263 fn test_infer_target_tron_tx_hash() {
1264 let t = infer_target(
1265 "abc123def456789012345678901234567890123456789012345678901234abcd",
1266 None,
1267 );
1268 assert!(matches!(t, InferredTarget::Transaction { chain } if chain == "tron"));
1269 }
1270
1271 #[test]
1272 fn test_infer_target_token_symbol() {
1273 let t = infer_target("USDC", None);
1274 assert!(matches!(t, InferredTarget::Token { chain } if chain == "ethereum"));
1275 }
1276
1277 #[test]
1278 fn test_infer_target_chain_override() {
1279 let t = infer_target(
1280 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
1281 Some("polygon"),
1282 );
1283 assert!(matches!(t, InferredTarget::Address { chain } if chain == "polygon"));
1284 }
1285
1286 #[test]
1287 fn test_infer_target_token_with_chain_override() {
1288 let t = infer_target("USDC", Some("solana"));
1289 assert!(matches!(t, InferredTarget::Token { chain } if chain == "solana"));
1290 }
1291
1292 #[test]
1293 fn test_classify_tx_type() {
1294 assert_eq!(
1295 classify_tx_type("0xa9059cbb1234...", Some("0xto")),
1296 "ERC-20 Transfer"
1297 );
1298 assert_eq!(
1299 classify_tx_type("0x095ea7b3abcd...", Some("0xto")),
1300 "ERC-20 Approve"
1301 );
1302 assert_eq!(classify_tx_type("0x", Some("0xto")), "Native Transfer");
1303 assert_eq!(classify_tx_type("", None), "Contract Creation");
1304 }
1305
1306 #[test]
1307 fn test_format_tx_value() {
1308 let (fmt, high) = format_tx_value("0xDE0B6B3A7640000", "ethereum"); assert!(fmt.contains("1.0") && fmt.contains("ETH"));
1310 assert!(!high);
1311 let (_, high2) = format_tx_value("0x52B7D2DCC80CD2E4000000", "ethereum"); assert!(high2);
1313 }
1314
1315 #[test]
1316 fn test_is_stablecoin() {
1317 assert!(is_stablecoin("USDC"));
1318 assert!(is_stablecoin("usdt"));
1319 assert!(is_stablecoin("DAI"));
1320 assert!(is_stablecoin("BUSD"));
1321 assert!(is_stablecoin("TUSD"));
1322 assert!(is_stablecoin("USDP"));
1323 assert!(is_stablecoin("FRAX"));
1324 assert!(is_stablecoin("LUSD"));
1325 assert!(is_stablecoin("GUSD"));
1326 assert!(!is_stablecoin("ETH"));
1327 assert!(!is_stablecoin("PEPE"));
1328 assert!(!is_stablecoin("WBTC"));
1329 }
1330
1331 #[test]
1332 fn test_is_stablecoin_empty_string() {
1333 assert!(!is_stablecoin(""));
1334 }
1335
1336 #[test]
1337 fn test_is_stablecoin_case_insensitive() {
1338 assert!(is_stablecoin("UsDc"));
1340 assert!(is_stablecoin("FraX"));
1341 assert!(!is_stablecoin("SOL")); }
1343
1344 #[test]
1349 fn test_target_type_label_address() {
1350 let t = InferredTarget::Address {
1351 chain: "ethereum".to_string(),
1352 };
1353 assert_eq!(target_type_label(&t), "Address");
1354 }
1355
1356 #[test]
1357 fn test_target_type_label_transaction() {
1358 let t = InferredTarget::Transaction {
1359 chain: "ethereum".to_string(),
1360 };
1361 assert_eq!(target_type_label(&t), "Transaction");
1362 }
1363
1364 #[test]
1365 fn test_target_type_label_token() {
1366 let t = InferredTarget::Token {
1367 chain: "ethereum".to_string(),
1368 };
1369 assert_eq!(target_type_label(&t), "Token");
1370 }
1371
1372 #[test]
1373 fn test_chain_label_address() {
1374 let t = InferredTarget::Address {
1375 chain: "polygon".to_string(),
1376 };
1377 assert_eq!(chain_label(&t), "polygon");
1378 }
1379
1380 #[test]
1381 fn test_chain_label_transaction() {
1382 let t = InferredTarget::Transaction {
1383 chain: "tron".to_string(),
1384 };
1385 assert_eq!(chain_label(&t), "tron");
1386 }
1387
1388 #[test]
1389 fn test_chain_label_token() {
1390 let t = InferredTarget::Token {
1391 chain: "solana".to_string(),
1392 };
1393 assert_eq!(chain_label(&t), "solana");
1394 }
1395
1396 #[test]
1401 fn test_classify_tx_type_dex_swaps() {
1402 assert_eq!(
1403 classify_tx_type("0x38ed173900000...", Some("0xrouter")),
1404 "DEX Swap"
1405 );
1406 assert_eq!(
1407 classify_tx_type("0x5c11d79500000...", Some("0xrouter")),
1408 "DEX Swap"
1409 );
1410 assert_eq!(
1411 classify_tx_type("0x4a25d94a00000...", Some("0xrouter")),
1412 "DEX Swap"
1413 );
1414 assert_eq!(
1415 classify_tx_type("0x8803dbee00000...", Some("0xrouter")),
1416 "DEX Swap"
1417 );
1418 assert_eq!(
1419 classify_tx_type("0x7ff36ab500000...", Some("0xrouter")),
1420 "DEX Swap"
1421 );
1422 assert_eq!(
1423 classify_tx_type("0x18cbafe500000...", Some("0xrouter")),
1424 "DEX Swap"
1425 );
1426 assert_eq!(
1427 classify_tx_type("0xfb3bdb4100000...", Some("0xrouter")),
1428 "DEX Swap"
1429 );
1430 assert_eq!(
1431 classify_tx_type("0xb6f9de9500000...", Some("0xrouter")),
1432 "DEX Swap"
1433 );
1434 }
1435
1436 #[test]
1437 fn test_classify_tx_type_multicall() {
1438 assert_eq!(
1439 classify_tx_type("0xac9650d800000...", Some("0xcontract")),
1440 "Multicall"
1441 );
1442 assert_eq!(
1443 classify_tx_type("0x5ae401dc00000...", Some("0xcontract")),
1444 "Multicall"
1445 );
1446 }
1447
1448 #[test]
1449 fn test_classify_tx_type_transfer_from() {
1450 assert_eq!(
1451 classify_tx_type("0x23b872dd00000...", Some("0xtoken")),
1452 "ERC-20 Transfer From"
1453 );
1454 }
1455
1456 #[test]
1457 fn test_classify_tx_type_contract_call() {
1458 assert_eq!(
1459 classify_tx_type("0xdeadbeef00000...", Some("0xcontract")),
1460 "Contract Call"
1461 );
1462 }
1463
1464 #[test]
1465 fn test_classify_tx_type_native_transfer_empty() {
1466 assert_eq!(classify_tx_type("", Some("0xrecipient")), "Native Transfer");
1467 }
1468
1469 #[test]
1474 fn test_format_tx_value_zero() {
1475 let (fmt, high) = format_tx_value("0x0", "ethereum");
1476 assert!(fmt.contains("0.000000"));
1477 assert!(fmt.contains("ETH"));
1478 assert!(!high);
1479 }
1480
1481 #[test]
1482 fn test_format_tx_value_empty_hex() {
1483 let (fmt, high) = format_tx_value("0x", "ethereum");
1484 assert!(fmt.contains("0.000000"));
1485 assert!(!high);
1486 }
1487
1488 #[test]
1489 fn test_format_tx_value_decimal_string() {
1490 let (fmt, high) = format_tx_value("1000000000000000000", "ethereum"); assert!(fmt.contains("1.0"));
1492 assert!(fmt.contains("ETH"));
1493 assert!(!high);
1494 }
1495
1496 #[test]
1497 fn test_format_tx_value_solana() {
1498 let (fmt, high) = format_tx_value("1000000000", "solana"); assert!(fmt.contains("1.0"));
1500 assert!(fmt.contains("SOL"));
1501 assert!(!high);
1502 }
1503
1504 #[test]
1505 fn test_format_tx_value_tron() {
1506 let (fmt, high) = format_tx_value("1000000", "tron"); assert!(fmt.contains("1.0"));
1508 assert!(fmt.contains("TRX"));
1509 assert!(!high);
1510 }
1511
1512 #[test]
1513 fn test_format_tx_value_polygon() {
1514 let (fmt, _) = format_tx_value("1000000000000000000", "polygon");
1515 assert!(fmt.contains("MATIC") || fmt.contains("POL"));
1516 }
1517
1518 #[test]
1519 fn test_format_tx_value_bsc() {
1520 let (fmt, _) = format_tx_value("1000000000000000000", "bsc");
1521 assert!(fmt.contains("BNB"));
1522 }
1523
1524 #[test]
1525 fn test_format_tx_value_arbitrum() {
1526 let (fmt, _) = format_tx_value("1000000000000000000", "arbitrum");
1527 assert!(fmt.contains("ETH"));
1528 }
1529
1530 #[test]
1531 fn test_format_tx_value_optimism() {
1532 let (fmt, _) = format_tx_value("1000000000000000000", "optimism");
1533 assert!(fmt.contains("ETH"));
1534 }
1535
1536 #[test]
1537 fn test_format_tx_value_base() {
1538 let (fmt, _) = format_tx_value("1000000000000000000", "base");
1539 assert!(fmt.contains("ETH"));
1540 }
1541
1542 #[test]
1543 fn test_format_tx_value_aegis() {
1544 let (fmt, _) = format_tx_value("1000000000000000000", "aegis");
1546 assert!(fmt.contains("1.0") || fmt.contains("1.000000"));
1547 }
1548
1549 #[test]
1550 fn test_format_tx_value_unknown_chain_defaults_18_decimals() {
1551 let (fmt, _) = format_tx_value("1000000000000000000", "unknown_chain");
1552 assert!(fmt.contains("1") || fmt.contains("ETH"));
1553 }
1554
1555 #[test]
1556 fn test_format_tx_value_invalid_hex_parse() {
1557 let (fmt, high) = format_tx_value("0xZZZZ", "ethereum");
1558 assert!(fmt.contains("0.000000"));
1559 assert!(!high);
1560 }
1561
1562 #[test]
1563 fn test_format_tx_value_high_value_threshold() {
1564 let (_, high) = format_tx_value("11000000000000000000", "ethereum"); assert!(high);
1567 let (_, high2) = format_tx_value("10000000000000000000", "ethereum"); assert!(!high2); }
1570
1571 #[test]
1576 fn test_meta_analysis_address_contract_high_value() {
1577 let meta = meta_analysis_address(true, Some(2_000_000.0), 10, None, None);
1578 assert!(meta.synthesis.contains("contract"));
1579 assert!(meta.synthesis.contains("Significant value"));
1580 assert!(meta.synthesis.contains("Diversified"));
1581 assert!(meta.recommendations.iter().any(|r| r.contains("contract")));
1582 }
1583
1584 #[test]
1585 fn test_meta_analysis_address_eoa_moderate_value() {
1586 let meta = meta_analysis_address(false, Some(50_000.0), 3, None, None);
1587 assert!(meta.synthesis.contains("wallet (EOA)"));
1588 assert!(meta.synthesis.contains("Moderate value"));
1589 }
1590
1591 #[test]
1592 fn test_meta_analysis_address_minimal_value() {
1593 let meta = meta_analysis_address(false, Some(0.5), 0, None, None);
1594 assert!(meta.synthesis.contains("Minimal value"));
1595 }
1596
1597 #[test]
1598 fn test_meta_analysis_address_single_token() {
1599 let meta = meta_analysis_address(false, None, 1, None, None);
1600 assert!(meta.synthesis.contains("Concentrated in a single token"));
1601 }
1602
1603 #[test]
1604 fn test_meta_analysis_address_high_risk() {
1605 use crate::compliance::risk::RiskLevel;
1606 let level = RiskLevel::High;
1607 let meta = meta_analysis_address(false, None, 0, Some(8.5), Some(&level));
1608 assert!(meta.synthesis.contains("Elevated risk"));
1609 assert!(meta.key_takeaway.contains("scrutiny"));
1610 assert!(
1611 meta.recommendations
1612 .iter()
1613 .any(|r| r.contains("unusual transaction"))
1614 );
1615 }
1616
1617 #[test]
1618 fn test_meta_analysis_address_low_risk() {
1619 use crate::compliance::risk::RiskLevel;
1620 let level = RiskLevel::Low;
1621 let meta = meta_analysis_address(false, None, 0, Some(2.0), Some(&level));
1622 assert!(meta.synthesis.contains("Low risk"));
1623 }
1624
1625 #[test]
1626 fn test_meta_analysis_address_contract_no_value() {
1627 let meta = meta_analysis_address(true, None, 0, None, None);
1628 assert!(meta.key_takeaway.contains("Contract address"));
1629 assert!(
1630 meta.recommendations
1631 .iter()
1632 .any(|r| r.contains("Confirm contract"))
1633 );
1634 }
1635
1636 #[test]
1637 fn test_meta_analysis_address_high_value_wallet() {
1638 let meta = meta_analysis_address(false, Some(150_000.0), 0, None, None);
1639 assert!(meta.key_takeaway.contains("High-value wallet"));
1640 }
1641
1642 #[test]
1643 fn test_meta_analysis_address_default_takeaway() {
1644 let meta = meta_analysis_address(false, Some(5_000.0), 0, None, None);
1645 assert!(meta.key_takeaway.contains("Review full report"));
1646 }
1647
1648 #[test]
1649 fn test_meta_analysis_address_empty_synthesis() {
1650 let meta = meta_analysis_address(
1651 false,
1652 Some(5_000.0), 2, None,
1655 None,
1656 );
1657 assert!(meta.synthesis.contains("wallet (EOA)"));
1658 }
1659
1660 #[test]
1661 fn test_meta_analysis_address_synthesis_parts_joined() {
1662 let meta = meta_analysis_address(false, None, 0, None, None);
1663 assert!(!meta.synthesis.is_empty());
1664 assert!(meta.synthesis.contains("Address analyzed") || meta.synthesis.contains("wallet"));
1665 }
1666
1667 #[test]
1668 fn test_meta_analysis_address_with_tokens_recommendation() {
1669 let meta = meta_analysis_address(false, None, 3, None, None);
1670 assert!(
1671 meta.recommendations
1672 .iter()
1673 .any(|r| r.contains("Verify token contracts"))
1674 );
1675 }
1676
1677 #[test]
1682 fn test_meta_analysis_tx_successful_native_transfer() {
1683 let meta = meta_analysis_tx("Native Transfer", true, false, "0xfrom", Some("0xto"));
1684 assert!(meta.synthesis.contains("Native Transfer"));
1685 assert!(meta.key_takeaway.contains("Routine"));
1686 assert!(meta.recommendations.is_empty());
1687 }
1688
1689 #[test]
1690 fn test_meta_analysis_tx_failed() {
1691 let meta = meta_analysis_tx("Contract Call", false, false, "0xfrom", Some("0xto"));
1692 assert!(meta.synthesis.contains("failed"));
1693 assert!(meta.key_takeaway.contains("Failed transaction"));
1694 assert!(meta.recommendations.iter().any(|r| r.contains("revert")));
1695 }
1696
1697 #[test]
1698 fn test_meta_analysis_tx_high_value_native() {
1699 let meta = meta_analysis_tx("Native Transfer", true, true, "0xfrom", Some("0xto"));
1700 assert!(meta.synthesis.contains("High-value"));
1701 assert!(meta.key_takeaway.contains("Large native transfer"));
1702 assert!(meta.recommendations.iter().any(|r| r.contains("recipient")));
1703 }
1704
1705 #[test]
1706 fn test_meta_analysis_tx_high_value_contract_call() {
1707 let meta = meta_analysis_tx("DEX Swap", true, true, "0xfrom", Some("0xto"));
1708 assert!(meta.key_takeaway.contains("High-value operation"));
1709 }
1710
1711 #[test]
1712 fn test_meta_analysis_tx_erc20_approve() {
1713 let meta = meta_analysis_tx("ERC-20 Approval", true, false, "0xfrom", Some("0xto"));
1714 assert!(meta.recommendations.iter().any(|r| r.contains("spender")));
1715 }
1716
1717 #[test]
1718 fn test_meta_analysis_tx_failed_high_value() {
1719 let meta = meta_analysis_tx("Contract Call", false, true, "0xfrom", Some("0xto"));
1720 assert!(meta.synthesis.contains("failed"));
1721 assert!(meta.synthesis.contains("High-value"));
1722 assert!(meta.recommendations.len() >= 2);
1723 }
1724
1725 #[test]
1730 fn test_meta_analysis_token_low_risk() {
1731 let summary = report::TokenRiskSummary {
1732 score: 2,
1733 level: "Low",
1734 emoji: "🟢",
1735 concerns: vec![],
1736 positives: vec!["Good liquidity".to_string()],
1737 };
1738 let meta = meta_analysis_token(&summary, false, None, None, 2_000_000.0);
1739 assert!(meta.synthesis.contains("Low-risk"));
1740 assert!(meta.synthesis.contains("Strong liquidity"));
1741 assert!(meta.key_takeaway.contains("Favorable"));
1742 }
1743
1744 #[test]
1745 fn test_meta_analysis_token_high_risk() {
1746 let summary = report::TokenRiskSummary {
1747 score: 8,
1748 level: "High",
1749 emoji: "🔴",
1750 concerns: vec!["Low liquidity".to_string()],
1751 positives: vec![],
1752 };
1753 let meta = meta_analysis_token(&summary, false, None, None, 10_000.0);
1754 assert!(meta.synthesis.contains("Elevated risk"));
1755 assert!(meta.synthesis.contains("Limited liquidity"));
1756 assert!(meta.key_takeaway.contains("High risk"));
1757 assert!(
1758 meta.recommendations
1759 .iter()
1760 .any(|r| r.contains("smaller position"))
1761 );
1762 }
1763
1764 #[test]
1765 fn test_meta_analysis_token_moderate_risk() {
1766 let summary = report::TokenRiskSummary {
1767 score: 5,
1768 level: "Medium",
1769 emoji: "🟡",
1770 concerns: vec!["Some concern".to_string()],
1771 positives: vec!["Some positive".to_string()],
1772 };
1773 let meta = meta_analysis_token(&summary, false, None, None, 500_000.0);
1774 assert!(meta.synthesis.contains("Moderate risk"));
1775 assert!(meta.key_takeaway.contains("Risk 5/10"));
1776 }
1777
1778 #[test]
1779 fn test_meta_analysis_token_stablecoin_healthy_peg() {
1780 let summary = report::TokenRiskSummary {
1781 score: 2,
1782 level: "Low",
1783 emoji: "🟢",
1784 concerns: vec![],
1785 positives: vec!["Stable peg".to_string()],
1786 };
1787 let meta = meta_analysis_token(&summary, true, Some(true), None, 5_000_000.0);
1788 assert!(meta.synthesis.contains("Stablecoin peg is healthy"));
1789 }
1790
1791 #[test]
1792 fn test_meta_analysis_token_stablecoin_unhealthy_peg() {
1793 let summary = report::TokenRiskSummary {
1794 score: 4,
1795 level: "Medium",
1796 emoji: "🟡",
1797 concerns: vec![],
1798 positives: vec![],
1799 };
1800 let meta = meta_analysis_token(&summary, true, Some(false), None, 500_000.0);
1801 assert!(meta.synthesis.contains("peg deviation"));
1802 assert!(meta.key_takeaway.contains("deviating from peg"));
1803 assert!(meta.recommendations.iter().any(|r| r.contains("peg")));
1804 }
1805
1806 #[test]
1807 fn test_meta_analysis_token_concentration_risk() {
1808 let summary = report::TokenRiskSummary {
1809 score: 5,
1810 level: "Medium",
1811 emoji: "🟡",
1812 concerns: vec![],
1813 positives: vec![],
1814 };
1815 let meta = meta_analysis_token(&summary, false, None, Some(45.0), 500_000.0);
1816 assert!(meta.synthesis.contains("Concentration risk"));
1817 assert!(
1818 meta.recommendations
1819 .iter()
1820 .any(|r| r.contains("top holder"))
1821 );
1822 }
1823
1824 #[test]
1825 fn test_meta_analysis_token_low_liquidity_low_risk() {
1826 let summary = report::TokenRiskSummary {
1827 score: 3,
1828 level: "Low",
1829 emoji: "🟢",
1830 concerns: vec![],
1831 positives: vec![],
1832 };
1833 let meta = meta_analysis_token(&summary, false, None, None, 50_000.0);
1834 assert!(
1835 meta.recommendations
1836 .iter()
1837 .any(|r| r.contains("limit orders") || r.contains("slippage"))
1838 );
1839 }
1840
1841 #[test]
1842 fn test_meta_analysis_token_stablecoin_no_peg_data() {
1843 let summary = report::TokenRiskSummary {
1844 score: 3,
1845 level: "Low",
1846 emoji: "🟢",
1847 concerns: vec![],
1848 positives: vec![],
1849 };
1850 let meta = meta_analysis_token(&summary, true, None, None, 1_000_000.0);
1851 assert!(meta.recommendations.iter().any(|r| r.contains("peg")));
1853 }
1854
1855 #[test]
1856 fn test_meta_analysis_token_mixed_signals_key_takeaway() {
1857 let summary = report::TokenRiskSummary {
1858 score: 5,
1859 level: "Medium",
1860 emoji: "🟡",
1861 concerns: vec!["Some concern".to_string()],
1862 positives: vec!["Some positive".to_string()],
1863 };
1864 let meta = meta_analysis_token(&summary, false, None, None, 500_000.0);
1865 assert!(meta.key_takeaway.contains("Risk 5/10"));
1866 assert!(meta.key_takeaway.contains("Medium"));
1867 }
1868
1869 #[test]
1874 fn test_infer_target_tx_hash_with_chain_override() {
1875 let t = infer_target(
1876 "0xabc123def456789012345678901234567890123456789012345678901234abcd",
1877 Some("polygon"),
1878 );
1879 assert!(matches!(t, InferredTarget::Transaction { chain } if chain == "polygon"));
1880 }
1881
1882 #[test]
1883 fn test_infer_target_whitespace_trimming() {
1884 let t = infer_target(" USDC ", None);
1885 assert!(matches!(t, InferredTarget::Token { .. }));
1886 }
1887
1888 #[test]
1889 fn test_infer_target_long_token_name() {
1890 let t = infer_target("some-random-token-name", None);
1891 assert!(matches!(t, InferredTarget::Token { chain } if chain == "ethereum"));
1892 }
1893
1894 #[test]
1899 fn test_insights_args_debug() {
1900 let args = InsightsArgs {
1901 target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
1902 chain: Some("ethereum".to_string()),
1903 decode: true,
1904 trace: false,
1905 };
1906 let debug_str = format!("{:?}", args);
1907 assert!(debug_str.contains("InsightsArgs"));
1908 assert!(debug_str.contains("0x742d"));
1909 }
1910
1911 #[test]
1916 fn test_classify_tx_type_contract_creation() {
1917 assert_eq!(classify_tx_type("0xa9059cbb...", None), "Contract Creation");
1918 }
1919
1920 #[test]
1921 fn test_classify_tx_type_erc20_transfer() {
1922 assert_eq!(
1923 classify_tx_type("0xa9059cbb00000000", Some("0x1234")),
1924 "ERC-20 Transfer"
1925 );
1926 }
1927
1928 #[test]
1929 fn test_classify_tx_type_erc20_approve() {
1930 assert_eq!(
1931 classify_tx_type("0x095ea7b3...", Some("0x1234")),
1932 "ERC-20 Approve"
1933 );
1934 }
1935
1936 #[test]
1937 fn test_classify_tx_type_erc20_transfer_from() {
1938 assert_eq!(
1939 classify_tx_type("0x23b872dd...", Some("0x1234")),
1940 "ERC-20 Transfer From"
1941 );
1942 }
1943
1944 #[test]
1945 fn test_classify_tx_type_dex_swap() {
1946 assert_eq!(
1947 classify_tx_type("0x38ed1739...", Some("0x1234")),
1948 "DEX Swap"
1949 );
1950 assert_eq!(
1951 classify_tx_type("0x7ff36ab5...", Some("0x1234")),
1952 "DEX Swap"
1953 );
1954 }
1955
1956 #[test]
1957 fn test_classify_tx_type_native_transfer() {
1958 assert_eq!(classify_tx_type("0x", Some("0x1234")), "Native Transfer");
1959 assert_eq!(classify_tx_type("", Some("0x1234")), "Native Transfer");
1960 }
1961
1962 #[test]
1963 fn test_classify_tx_type_unknown_contract_call() {
1964 assert_eq!(
1965 classify_tx_type("0xdeadbeef12345678", Some("0x1234")),
1966 "Contract Call"
1967 );
1968 }
1969
1970 #[test]
1975 fn test_format_tx_value_ethereum_wei() {
1976 let (fmt, high) = format_tx_value("1000000000000000000", "ethereum");
1977 assert!(fmt.contains("1.000000"));
1978 assert!(fmt.contains("ETH"));
1979 assert!(!high); }
1981
1982 #[test]
1983 fn test_format_tx_value_hex() {
1984 let (fmt, _) = format_tx_value("0xde0b6b3a7640000", "ethereum");
1985 assert!(fmt.contains("ETH"));
1987 }
1988
1989 #[test]
1990 fn test_format_tx_value_high_value() {
1991 let (_, high) = format_tx_value("100000000000000000000", "ethereum");
1993 assert!(high); }
1995
1996 #[test]
1997 fn test_format_tx_value_zero_decimal() {
1998 let (fmt, high) = format_tx_value("0", "ethereum");
1999 assert!(fmt.contains("0.000000"));
2000 assert!(!high);
2001 }
2002
2003 #[test]
2004 fn test_format_tx_value_solana_additional() {
2005 let (fmt, _) = format_tx_value("1000000000", "solana"); assert!(fmt.contains("SOL"));
2007 }
2008
2009 #[test]
2010 fn test_format_tx_value_tron_additional() {
2011 let (fmt, _) = format_tx_value("1000000", "tron"); assert!(fmt.contains("TRX"));
2013 }
2014
2015 #[test]
2016 fn test_format_tx_value_empty_hex_additional() {
2017 let (fmt, _) = format_tx_value("0x", "ethereum");
2018 assert!(fmt.contains("0.000000"));
2019 }
2020
2021 #[test]
2026 fn test_target_type_label_combined() {
2027 assert_eq!(
2028 target_type_label(&InferredTarget::Address {
2029 chain: "eth".to_string()
2030 }),
2031 "Address"
2032 );
2033 assert_eq!(
2034 target_type_label(&InferredTarget::Transaction {
2035 chain: "eth".to_string()
2036 }),
2037 "Transaction"
2038 );
2039 assert_eq!(
2040 target_type_label(&InferredTarget::Token {
2041 chain: "eth".to_string()
2042 }),
2043 "Token"
2044 );
2045 }
2046
2047 #[test]
2048 fn test_chain_label_combined() {
2049 assert_eq!(
2050 chain_label(&InferredTarget::Address {
2051 chain: "ethereum".to_string()
2052 }),
2053 "ethereum"
2054 );
2055 assert_eq!(
2056 chain_label(&InferredTarget::Transaction {
2057 chain: "polygon".to_string()
2058 }),
2059 "polygon"
2060 );
2061 assert_eq!(
2062 chain_label(&InferredTarget::Token {
2063 chain: "solana".to_string()
2064 }),
2065 "solana"
2066 );
2067 }
2068
2069 #[test]
2074 fn test_meta_analysis_address_contract_high_risk() {
2075 use crate::compliance::risk::RiskLevel;
2076 let meta = meta_analysis_address(
2077 true,
2078 Some(2_000_000.0),
2079 10,
2080 Some(8.0),
2081 Some(&RiskLevel::High),
2082 );
2083 assert!(meta.synthesis.contains("contract"));
2084 assert!(meta.synthesis.contains("Significant value"));
2085 assert!(meta.key_takeaway.contains("scrutiny"));
2086 assert!(!meta.recommendations.is_empty());
2087 }
2088
2089 #[test]
2090 fn test_meta_analysis_address_wallet_low_risk() {
2091 use crate::compliance::risk::RiskLevel;
2092 let meta = meta_analysis_address(false, Some(0.5), 0, Some(2.0), Some(&RiskLevel::Low));
2093 assert!(meta.synthesis.contains("wallet"));
2094 assert!(meta.synthesis.contains("Minimal value"));
2095 }
2096
2097 #[test]
2098 fn test_meta_analysis_address_no_risk_data() {
2099 let meta = meta_analysis_address(false, None, 0, None, None);
2100 assert!(!meta.synthesis.is_empty());
2101 assert!(meta.key_takeaway.contains("Review full report"));
2102 }
2103
2104 #[test]
2109 fn test_meta_analysis_tx_failed_additional() {
2110 let meta = meta_analysis_tx("Contract Call", false, false, "0x...", Some("0x..."));
2111 assert!(meta.synthesis.contains("failed"));
2112 assert!(meta.key_takeaway.contains("Failed"));
2113 }
2114
2115 #[test]
2116 fn test_meta_analysis_tx_high_value_native_additional() {
2117 let meta = meta_analysis_tx("Native Transfer", true, true, "0x...", Some("0x..."));
2118 assert!(meta.synthesis.contains("High-value"));
2119 assert!(meta.key_takeaway.contains("Large native transfer"));
2120 }
2121
2122 #[test]
2123 fn test_meta_analysis_tx_routine() {
2124 let meta = meta_analysis_tx("ERC-20 Transfer", true, false, "0x...", Some("0x..."));
2125 assert!(meta.key_takeaway.contains("Routine"));
2126 }
2127
2128 #[test]
2133 fn test_meta_analysis_token_high_risk_additional() {
2134 let risk = report::TokenRiskSummary {
2135 score: 8,
2136 level: "High",
2137 emoji: "🔴",
2138 concerns: vec!["Low liquidity".to_string()],
2139 positives: vec![],
2140 };
2141 let meta = meta_analysis_token(&risk, false, None, None, 10_000.0);
2142 assert!(meta.synthesis.contains("Elevated risk"));
2143 assert!(meta.key_takeaway.contains("High risk"));
2144 }
2145
2146 #[test]
2147 fn test_meta_analysis_token_stablecoin_peg_healthy() {
2148 let risk = report::TokenRiskSummary {
2149 score: 2,
2150 level: "Low",
2151 emoji: "🟢",
2152 concerns: vec![],
2153 positives: vec!["Strong liquidity".to_string()],
2154 };
2155 let meta = meta_analysis_token(&risk, true, Some(true), Some(5.0), 5_000_000.0);
2156 assert!(meta.synthesis.contains("peg is healthy"));
2157 assert!(meta.synthesis.contains("Strong liquidity"));
2158 }
2159
2160 #[test]
2161 fn test_meta_analysis_token_stablecoin_peg_unhealthy() {
2162 let risk = report::TokenRiskSummary {
2163 score: 5,
2164 level: "Medium",
2165 emoji: "🟡",
2166 concerns: vec!["Peg deviation".to_string()],
2167 positives: vec![],
2168 };
2169 let meta = meta_analysis_token(&risk, true, Some(false), Some(40.0), 100_000.0);
2170 assert!(meta.synthesis.contains("peg deviation"));
2171 assert!(meta.synthesis.contains("Concentration risk"));
2172 }
2173
2174 #[test]
2175 fn test_meta_analysis_token_high_risk_empty_concerns_uses_multiple_factors() {
2176 let risk = report::TokenRiskSummary {
2177 score: 8,
2178 level: "High",
2179 emoji: "🔴",
2180 concerns: vec![],
2181 positives: vec![],
2182 };
2183 let meta = meta_analysis_token(&risk, false, None, None, 50_000.0);
2184 assert!(
2185 meta.key_takeaway.contains("multiple factors")
2186 || meta.key_takeaway.contains("High risk")
2187 );
2188 }
2189
2190 #[test]
2191 fn test_infer_target_chain_override_with_address() {
2192 let t = infer_target(
2193 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
2194 Some("arbitrum"),
2195 );
2196 assert!(matches!(t, InferredTarget::Address { chain } if chain == "arbitrum"));
2197 }
2198
2199 #[test]
2200 fn test_meta_analysis_tx_approval_contains_approval_in_match() {
2201 let meta = meta_analysis_tx("ERC-20 Approval", true, false, "0xfrom", Some("0xto"));
2202 assert!(
2203 meta.recommendations
2204 .iter()
2205 .any(|r| r.contains("spender") || r.contains("allowance"))
2206 );
2207 }
2208}