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