1use crate::chains::{ChainClientFactory, validate_solana_signature, validate_tron_tx_hash};
21use crate::config::{Config, OutputFormat};
22use crate::error::{Result, ScopeError};
23use clap::Args;
24
25#[derive(Debug, Clone, Args)]
27pub struct TxArgs {
28 #[arg(value_name = "HASH")]
33 pub hash: String,
34
35 #[arg(short, long, default_value = "ethereum")]
40 pub chain: String,
41
42 #[arg(short, long, value_name = "FORMAT")]
44 pub format: Option<OutputFormat>,
45
46 #[arg(long)]
48 pub trace: bool,
49
50 #[arg(long)]
52 pub decode: bool,
53}
54
55#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
57pub struct TransactionReport {
58 pub hash: String,
60
61 pub chain: String,
63
64 pub block: BlockInfo,
66
67 pub transaction: TransactionDetails,
69
70 pub gas: GasInfo,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub decoded_input: Option<DecodedInput>,
76
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub internal_transactions: Option<Vec<InternalTransaction>>,
80}
81
82#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
84pub struct BlockInfo {
85 pub number: u64,
87
88 pub timestamp: u64,
90
91 pub hash: String,
93}
94
95#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
97pub struct TransactionDetails {
98 pub from: String,
100
101 pub to: Option<String>,
103
104 pub value: String,
106
107 pub nonce: u64,
109
110 pub transaction_index: u64,
112
113 pub status: bool,
115
116 pub input: String,
118}
119
120#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
122pub struct GasInfo {
123 pub gas_limit: u64,
125
126 pub gas_used: u64,
128
129 pub gas_price: String,
131
132 pub transaction_fee: String,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub effective_gas_price: Option<String>,
138}
139
140#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
142pub struct DecodedInput {
143 pub function_signature: String,
145
146 pub function_name: String,
148
149 pub parameters: Vec<DecodedParameter>,
151}
152
153#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
155pub struct DecodedParameter {
156 pub name: String,
158
159 pub param_type: String,
161
162 pub value: String,
164}
165
166#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
168pub struct InternalTransaction {
169 pub call_type: String,
171
172 pub from: String,
174
175 pub to: String,
177
178 pub value: String,
180
181 pub gas: u64,
183
184 pub input: String,
186
187 pub output: String,
189}
190
191pub async fn run(
207 mut args: TxArgs,
208 config: &Config,
209 clients: &dyn ChainClientFactory,
210) -> Result<()> {
211 if args.chain == "ethereum"
213 && let Some(inferred) = crate::chains::infer_chain_from_hash(&args.hash)
214 && inferred != "ethereum"
215 {
216 tracing::info!("Auto-detected chain: {}", inferred);
217 println!("Auto-detected chain: {}", inferred);
218 args.chain = inferred.to_string();
219 }
220
221 tracing::info!(
222 hash = %args.hash,
223 chain = %args.chain,
224 "Starting transaction analysis"
225 );
226
227 validate_tx_hash(&args.hash, &args.chain)?;
229
230 println!("Analyzing transaction on {}...", args.chain);
231
232 let client = clients.create_chain_client(&args.chain)?;
233 let tx = client.get_transaction(&args.hash).await?;
234
235 let gas_price_val: u128 = tx.gas_price.parse().unwrap_or(0);
237 let gas_used_val = tx.gas_used.unwrap_or(0) as u128;
238 let fee_wei = gas_price_val * gas_used_val;
239 let chain_lower = args.chain.to_lowercase();
240 let fee_str = if chain_lower == "solana" || chain_lower == "sol" {
241 let fee_sol = tx.gas_price.parse::<f64>().unwrap_or(0.0) / 1_000_000_000.0;
243 format!("{:.9}", fee_sol)
244 } else {
245 fee_wei.to_string()
246 };
247
248 let report = TransactionReport {
249 hash: tx.hash.clone(),
250 chain: args.chain.clone(),
251 block: BlockInfo {
252 number: tx.block_number.unwrap_or(0),
253 timestamp: tx.timestamp.unwrap_or(0),
254 hash: String::new(), },
256 transaction: TransactionDetails {
257 from: tx.from.clone(),
258 to: tx.to.clone(),
259 value: tx.value.clone(),
260 nonce: tx.nonce,
261 transaction_index: 0,
262 status: tx.status.unwrap_or(true),
263 input: tx.input.clone(),
264 },
265 gas: GasInfo {
266 gas_limit: tx.gas_limit,
267 gas_used: tx.gas_used.unwrap_or(0),
268 gas_price: tx.gas_price.clone(),
269 transaction_fee: fee_str,
270 effective_gas_price: None,
271 },
272 decoded_input: if args.decode && !tx.input.is_empty() && tx.input != "0x" {
273 let selector = if tx.input.len() >= 10 {
275 &tx.input[..10]
276 } else {
277 &tx.input
278 };
279 Some(DecodedInput {
280 function_signature: format!("{}(...)", selector),
281 function_name: selector.to_string(),
282 parameters: vec![],
283 })
284 } else if args.decode {
285 Some(DecodedInput {
286 function_signature: "transfer()".to_string(),
287 function_name: "Native Transfer".to_string(),
288 parameters: vec![],
289 })
290 } else {
291 None
292 },
293 internal_transactions: if args.trace { Some(vec![]) } else { None },
294 };
295
296 let format = args.format.unwrap_or(config.output.format);
298 output_report(&report, format)?;
299
300 Ok(())
301}
302
303fn validate_tx_hash(hash: &str, chain: &str) -> Result<()> {
305 match chain {
306 "ethereum" | "polygon" | "arbitrum" | "optimism" | "base" | "bsc" | "aegis" => {
308 if !hash.starts_with("0x") {
309 return Err(ScopeError::InvalidHash(format!(
310 "Transaction hash must start with '0x': {}",
311 hash
312 )));
313 }
314 if hash.len() != 66 {
315 return Err(ScopeError::InvalidHash(format!(
316 "Transaction hash must be 66 characters (0x + 64 hex): {}",
317 hash
318 )));
319 }
320 if !hash[2..].chars().all(|c| c.is_ascii_hexdigit()) {
321 return Err(ScopeError::InvalidHash(format!(
322 "Transaction hash contains invalid hex characters: {}",
323 hash
324 )));
325 }
326 }
327 "solana" => {
329 validate_solana_signature(hash)?;
330 }
331 "tron" => {
333 validate_tron_tx_hash(hash)?;
334 }
335 _ => {
336 return Err(ScopeError::Chain(format!(
337 "Unsupported chain: {}. Supported: ethereum, polygon, arbitrum, optimism, base, bsc, solana, tron",
338 chain
339 )));
340 }
341 }
342 Ok(())
343}
344
345fn output_report(report: &TransactionReport, format: OutputFormat) -> Result<()> {
347 match format {
348 OutputFormat::Json => {
349 let json = serde_json::to_string_pretty(report)?;
350 println!("{}", json);
351 }
352 OutputFormat::Csv => {
353 println!("hash,chain,block,from,to,value,status,gas_used,fee");
354 println!(
355 "{},{},{},{},{},{},{},{},{}",
356 report.hash,
357 report.chain,
358 report.block.number,
359 report.transaction.from,
360 report.transaction.to.as_deref().unwrap_or(""),
361 report.transaction.value,
362 report.transaction.status,
363 report.gas.gas_used,
364 report.gas.transaction_fee
365 );
366 }
367 OutputFormat::Table => {
368 println!("Transaction Analysis Report");
369 println!("===========================");
370 println!("Hash: {}", report.hash);
371 println!("Chain: {}", report.chain);
372 println!("Block: {}", report.block.number);
373 println!(
374 "Status: {}",
375 if report.transaction.status {
376 "Success"
377 } else {
378 "Failed"
379 }
380 );
381 println!();
382 println!("From: {}", report.transaction.from);
383 println!(
384 "To: {}",
385 report
386 .transaction
387 .to
388 .as_deref()
389 .unwrap_or("Contract Creation")
390 );
391 println!("Value: {}", report.transaction.value);
392 println!();
393 println!("Gas Limit: {}", report.gas.gas_limit);
394 println!("Gas Used: {}", report.gas.gas_used);
395 println!("Gas Price: {}", report.gas.gas_price);
396 println!("Fee: {}", report.gas.transaction_fee);
397
398 if let Some(ref decoded) = report.decoded_input {
399 println!();
400 println!("Function: {}", decoded.function_name);
401 println!("Signature: {}", decoded.function_signature);
402 if !decoded.parameters.is_empty() {
403 println!("Parameters:");
404 for param in &decoded.parameters {
405 println!(" {} ({}): {}", param.name, param.param_type, param.value);
406 }
407 }
408 }
409
410 if let Some(ref traces) = report.internal_transactions
411 && !traces.is_empty()
412 {
413 println!();
414 println!("Internal Transactions: {}", traces.len());
415 for (i, trace) in traces.iter().enumerate() {
416 println!(
417 " [{}] {} {} -> {}",
418 i, trace.call_type, trace.from, trace.to
419 );
420 }
421 }
422 }
423 OutputFormat::Markdown => {
424 let md = format_tx_markdown(report);
425 println!("{}", md);
426 }
427 }
428 Ok(())
429}
430
431fn format_tx_markdown(report: &TransactionReport) -> String {
433 let mut md = String::new();
434 md.push_str("# Transaction Analysis\n\n");
435 md.push_str("| Field | Value |\n|-------|-------|\n");
436 md.push_str(&format!("| Hash | `{}` |\n", report.hash));
437 md.push_str(&format!("| Chain | {} |\n", report.chain));
438 md.push_str(&format!("| Block | {} |\n", report.block.number));
439 md.push_str(&format!(
440 "| Status | {} |\n",
441 if report.transaction.status {
442 "Success"
443 } else {
444 "Failed"
445 }
446 ));
447 md.push_str(&format!("| From | `{}` |\n", report.transaction.from));
448 md.push_str(&format!(
449 "| To | `{}` |\n",
450 report
451 .transaction
452 .to
453 .as_deref()
454 .unwrap_or("Contract Creation")
455 ));
456 md.push_str(&format!("| Value | {} |\n", report.transaction.value));
457 md.push_str(&format!("| Gas Used | {} |\n", report.gas.gas_used));
458 md.push_str(&format!("| Fee | {} |\n", report.gas.transaction_fee));
459 if let Some(ref decoded) = report.decoded_input {
460 md.push_str("\n## Decoded Input\n\n");
461 md.push_str(&format!("- **Function:** {}\n", decoded.function_name));
462 md.push_str(&format!(
463 "- **Signature:** `{}`\n",
464 decoded.function_signature
465 ));
466 if !decoded.parameters.is_empty() {
467 md.push_str("\n| Parameter | Type | Value |\n|-----------|------|-------|\n");
468 for param in &decoded.parameters {
469 md.push_str(&format!(
470 "| {} | {} | {} |\n",
471 param.name, param.param_type, param.value
472 ));
473 }
474 }
475 }
476 if let Some(ref traces) = report.internal_transactions
477 && !traces.is_empty()
478 {
479 md.push_str("\n## Internal Transactions\n\n");
480 md.push_str("| # | Type | From | To |\n|---|---|---|---|\n");
481 for (i, trace) in traces.iter().enumerate() {
482 md.push_str(&format!(
483 "| {} | {} | `{}` | `{}` |\n",
484 i + 1,
485 trace.call_type,
486 trace.from,
487 trace.to
488 ));
489 }
490 }
491 md
492}
493
494#[cfg(test)]
499mod tests {
500 use super::*;
501
502 const VALID_TX_HASH: &str =
503 "0xabc123def456789012345678901234567890123456789012345678901234abcd";
504
505 #[test]
506 fn test_validate_tx_hash_valid() {
507 let result = validate_tx_hash(VALID_TX_HASH, "ethereum");
508 assert!(result.is_ok());
509 }
510
511 #[test]
512 fn test_validate_tx_hash_valid_lowercase() {
513 let hash = "0xabc123def456789012345678901234567890123456789012345678901234abcd";
514 let result = validate_tx_hash(hash, "ethereum");
515 assert!(result.is_ok());
516 }
517
518 #[test]
519 fn test_validate_tx_hash_valid_polygon() {
520 let result = validate_tx_hash(VALID_TX_HASH, "polygon");
521 assert!(result.is_ok());
522 }
523
524 #[test]
525 fn test_validate_tx_hash_missing_prefix() {
526 let hash = "abc123def456789012345678901234567890123456789012345678901234abcd";
527 let result = validate_tx_hash(hash, "ethereum");
528 assert!(result.is_err());
529 assert!(result.unwrap_err().to_string().contains("0x"));
530 }
531
532 #[test]
533 fn test_validate_tx_hash_too_short() {
534 let hash = "0xabc123";
535 let result = validate_tx_hash(hash, "ethereum");
536 assert!(result.is_err());
537 assert!(result.unwrap_err().to_string().contains("66 characters"));
538 }
539
540 #[test]
541 fn test_validate_tx_hash_too_long() {
542 let hash = "0xabc123def456789012345678901234567890123456789012345678901234abcde";
543 let result = validate_tx_hash(hash, "ethereum");
544 assert!(result.is_err());
545 }
546
547 #[test]
548 fn test_validate_tx_hash_invalid_hex() {
549 let hash = "0xabc123def456789012345678901234567890123456789012345678901234GHIJ";
550 let result = validate_tx_hash(hash, "ethereum");
551 assert!(result.is_err());
552 assert!(result.unwrap_err().to_string().contains("invalid hex"));
553 }
554
555 #[test]
556 fn test_validate_tx_hash_unsupported_chain() {
557 let result = validate_tx_hash(VALID_TX_HASH, "bitcoin");
558 assert!(result.is_err());
559 assert!(
560 result
561 .unwrap_err()
562 .to_string()
563 .contains("Unsupported chain")
564 );
565 }
566
567 #[test]
568 fn test_validate_tx_hash_valid_bsc() {
569 let result = validate_tx_hash(VALID_TX_HASH, "bsc");
570 assert!(result.is_ok());
571 }
572
573 #[test]
574 fn test_validate_tx_hash_valid_aegis() {
575 let result = validate_tx_hash(VALID_TX_HASH, "aegis");
576 assert!(result.is_ok());
577 }
578
579 #[test]
580 fn test_validate_tx_hash_valid_solana() {
581 let sig = "5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW";
583 let result = validate_tx_hash(sig, "solana");
584 assert!(result.is_ok());
585 }
586
587 #[test]
588 fn test_validate_tx_hash_invalid_solana() {
589 let result = validate_tx_hash(VALID_TX_HASH, "solana");
591 assert!(result.is_err());
592 }
593
594 #[test]
595 fn test_validate_tx_hash_valid_tron() {
596 let hash = "abc123def456789012345678901234567890123456789012345678901234abcd";
598 let result = validate_tx_hash(hash, "tron");
599 assert!(result.is_ok());
600 }
601
602 #[test]
603 fn test_validate_tx_hash_invalid_tron() {
604 let result = validate_tx_hash(VALID_TX_HASH, "tron");
606 assert!(result.is_err());
607 }
608
609 #[test]
610 fn test_tx_args_default_values() {
611 use clap::Parser;
612
613 #[derive(Parser)]
614 struct TestCli {
615 #[command(flatten)]
616 args: TxArgs,
617 }
618
619 let cli = TestCli::try_parse_from(["test", VALID_TX_HASH]).unwrap();
620
621 assert_eq!(cli.args.chain, "ethereum");
622 assert!(!cli.args.trace);
623 assert!(!cli.args.decode);
624 assert!(cli.args.format.is_none());
625 }
626
627 #[test]
628 fn test_tx_args_with_options() {
629 use clap::Parser;
630
631 #[derive(Parser)]
632 struct TestCli {
633 #[command(flatten)]
634 args: TxArgs,
635 }
636
637 let cli = TestCli::try_parse_from([
638 "test",
639 VALID_TX_HASH,
640 "--chain",
641 "polygon",
642 "--trace",
643 "--decode",
644 "--format",
645 "json",
646 ])
647 .unwrap();
648
649 assert_eq!(cli.args.chain, "polygon");
650 assert!(cli.args.trace);
651 assert!(cli.args.decode);
652 assert_eq!(cli.args.format, Some(OutputFormat::Json));
653 }
654
655 #[test]
656 fn test_transaction_report_serialization() {
657 let report = TransactionReport {
658 hash: VALID_TX_HASH.to_string(),
659 chain: "ethereum".to_string(),
660 block: BlockInfo {
661 number: 12345678,
662 timestamp: 1700000000,
663 hash: "0xblock".to_string(),
664 },
665 transaction: TransactionDetails {
666 from: "0xfrom".to_string(),
667 to: Some("0xto".to_string()),
668 value: "1.0".to_string(),
669 nonce: 42,
670 transaction_index: 5,
671 status: true,
672 input: "0x".to_string(),
673 },
674 gas: GasInfo {
675 gas_limit: 100000,
676 gas_used: 21000,
677 gas_price: "20000000000".to_string(),
678 transaction_fee: "0.00042".to_string(),
679 effective_gas_price: None,
680 },
681 decoded_input: None,
682 internal_transactions: None,
683 };
684
685 let json = serde_json::to_string(&report).unwrap();
686 assert!(json.contains(VALID_TX_HASH));
687 assert!(json.contains("12345678"));
688 assert!(json.contains("21000"));
689 assert!(!json.contains("decoded_input"));
690 assert!(!json.contains("internal_transactions"));
691 }
692
693 #[test]
694 fn test_block_info_serialization() {
695 let block = BlockInfo {
696 number: 12345678,
697 timestamp: 1700000000,
698 hash: "0xblockhash".to_string(),
699 };
700
701 let json = serde_json::to_string(&block).unwrap();
702 assert!(json.contains("12345678"));
703 assert!(json.contains("1700000000"));
704 assert!(json.contains("0xblockhash"));
705 }
706
707 #[test]
708 fn test_gas_info_serialization() {
709 let gas = GasInfo {
710 gas_limit: 100000,
711 gas_used: 50000,
712 gas_price: "20000000000".to_string(),
713 transaction_fee: "0.001".to_string(),
714 effective_gas_price: Some("25000000000".to_string()),
715 };
716
717 let json = serde_json::to_string(&gas).unwrap();
718 assert!(json.contains("100000"));
719 assert!(json.contains("50000"));
720 assert!(json.contains("effective_gas_price"));
721 }
722
723 #[test]
724 fn test_decoded_input_serialization() {
725 let decoded = DecodedInput {
726 function_signature: "transfer(address,uint256)".to_string(),
727 function_name: "transfer".to_string(),
728 parameters: vec![
729 DecodedParameter {
730 name: "to".to_string(),
731 param_type: "address".to_string(),
732 value: "0xrecipient".to_string(),
733 },
734 DecodedParameter {
735 name: "amount".to_string(),
736 param_type: "uint256".to_string(),
737 value: "1000000".to_string(),
738 },
739 ],
740 };
741
742 let json = serde_json::to_string(&decoded).unwrap();
743 assert!(json.contains("transfer(address,uint256)"));
744 assert!(json.contains("0xrecipient"));
745 assert!(json.contains("1000000"));
746 }
747
748 #[test]
749 fn test_internal_transaction_serialization() {
750 let internal = InternalTransaction {
751 call_type: "call".to_string(),
752 from: "0xfrom".to_string(),
753 to: "0xto".to_string(),
754 value: "1.0".to_string(),
755 gas: 50000,
756 input: "0x".to_string(),
757 output: "0x".to_string(),
758 };
759
760 let json = serde_json::to_string(&internal).unwrap();
761 assert!(json.contains("call"));
762 assert!(json.contains("0xfrom"));
763 assert!(json.contains("50000"));
764 }
765
766 fn make_test_tx_report() -> TransactionReport {
771 TransactionReport {
772 hash: VALID_TX_HASH.to_string(),
773 chain: "ethereum".to_string(),
774 block: BlockInfo {
775 number: 12345678,
776 timestamp: 1700000000,
777 hash: "0xblock".to_string(),
778 },
779 transaction: TransactionDetails {
780 from: "0xfrom".to_string(),
781 to: Some("0xto".to_string()),
782 value: "1.0".to_string(),
783 nonce: 42,
784 transaction_index: 5,
785 status: true,
786 input: "0xa9059cbb0000000000".to_string(),
787 },
788 gas: GasInfo {
789 gas_limit: 100000,
790 gas_used: 21000,
791 gas_price: "20000000000".to_string(),
792 transaction_fee: "0.00042".to_string(),
793 effective_gas_price: None,
794 },
795 decoded_input: Some(DecodedInput {
796 function_signature: "transfer(address,uint256)".to_string(),
797 function_name: "transfer".to_string(),
798 parameters: vec![DecodedParameter {
799 name: "to".to_string(),
800 param_type: "address".to_string(),
801 value: "0xrecipient".to_string(),
802 }],
803 }),
804 internal_transactions: Some(vec![InternalTransaction {
805 call_type: "call".to_string(),
806 from: "0xfrom".to_string(),
807 to: "0xto".to_string(),
808 value: "0.5".to_string(),
809 gas: 30000,
810 input: "0x".to_string(),
811 output: "0x".to_string(),
812 }]),
813 }
814 }
815
816 #[test]
817 fn test_output_report_json() {
818 let report = make_test_tx_report();
819 let result = output_report(&report, OutputFormat::Json);
820 assert!(result.is_ok());
821 }
822
823 #[test]
824 fn test_output_report_csv() {
825 let report = make_test_tx_report();
826 let result = output_report(&report, OutputFormat::Csv);
827 assert!(result.is_ok());
828 }
829
830 #[test]
831 fn test_output_report_table() {
832 let report = make_test_tx_report();
833 let result = output_report(&report, OutputFormat::Table);
834 assert!(result.is_ok());
835 }
836
837 #[test]
838 fn test_output_report_table_no_decoded() {
839 let mut report = make_test_tx_report();
840 report.decoded_input = None;
841 report.internal_transactions = None;
842 let result = output_report(&report, OutputFormat::Table);
843 assert!(result.is_ok());
844 }
845
846 #[test]
847 fn test_output_report_table_failed_tx() {
848 let mut report = make_test_tx_report();
849 report.transaction.status = false;
850 report.transaction.to = None; let result = output_report(&report, OutputFormat::Table);
852 assert!(result.is_ok());
853 }
854
855 #[test]
856 fn test_output_report_table_empty_traces() {
857 let mut report = make_test_tx_report();
858 report.internal_transactions = Some(vec![]);
859 let result = output_report(&report, OutputFormat::Table);
860 assert!(result.is_ok());
861 }
862
863 #[test]
864 fn test_output_report_csv_no_to() {
865 let mut report = make_test_tx_report();
866 report.transaction.to = None;
867 let result = output_report(&report, OutputFormat::Csv);
868 assert!(result.is_ok());
869 }
870
871 use crate::chains::mocks::MockClientFactory;
876
877 fn mock_factory() -> MockClientFactory {
878 MockClientFactory::new()
879 }
880
881 #[tokio::test]
882 async fn test_run_ethereum_tx() {
883 let config = Config::default();
884 let factory = mock_factory();
885 let args = TxArgs {
886 hash: "0xabc123def456789012345678901234567890123456789012345678901234abcd".to_string(),
887 chain: "ethereum".to_string(),
888 format: Some(OutputFormat::Json),
889 trace: false,
890 decode: false,
891 };
892 let result = super::run(args, &config, &factory).await;
893 assert!(result.is_ok());
894 }
895
896 #[tokio::test]
897 async fn test_run_tx_with_decode() {
898 let config = Config::default();
899 let mut factory = mock_factory();
900 factory.mock_client.transaction.input = "0xa9059cbb000000000000000000000000".to_string();
901 let args = TxArgs {
902 hash: "0xabc123def456789012345678901234567890123456789012345678901234abcd".to_string(),
903 chain: "ethereum".to_string(),
904 format: Some(OutputFormat::Table),
905 trace: false,
906 decode: true,
907 };
908 let result = super::run(args, &config, &factory).await;
909 assert!(result.is_ok());
910 }
911
912 #[tokio::test]
913 async fn test_run_tx_with_trace() {
914 let config = Config::default();
915 let factory = mock_factory();
916 let args = TxArgs {
917 hash: "0xabc123def456789012345678901234567890123456789012345678901234abcd".to_string(),
918 chain: "ethereum".to_string(),
919 format: Some(OutputFormat::Csv),
920 trace: true,
921 decode: false,
922 };
923 let result = super::run(args, &config, &factory).await;
924 assert!(result.is_ok());
925 }
926
927 #[tokio::test]
928 async fn test_run_tx_invalid_hash() {
929 let config = Config::default();
930 let factory = mock_factory();
931 let args = TxArgs {
932 hash: "invalid".to_string(),
933 chain: "ethereum".to_string(),
934 format: Some(OutputFormat::Json),
935 trace: false,
936 decode: false,
937 };
938 let result = super::run(args, &config, &factory).await;
939 assert!(result.is_err());
940 }
941
942 #[tokio::test]
943 async fn test_run_tx_auto_detect_tron() {
944 let config = Config::default();
945 let factory = mock_factory();
946 let args = TxArgs {
947 hash: "abc123def456789012345678901234567890123456789012345678901234abcd".to_string(),
949 chain: "ethereum".to_string(), format: Some(OutputFormat::Json),
951 trace: false,
952 decode: false,
953 };
954 let result = super::run(args, &config, &factory).await;
955 assert!(result.is_ok());
956 }
957}