1use crate::contract::source::{AbiEntry, ContractSource};
19use serde::Deserialize;
20
21#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
23pub struct DecodedCall {
24 pub selector: String,
26 pub signature: String,
28 pub function_name: String,
30 pub parameters: Vec<DecodedParam>,
32 pub source: DecodeSource,
34}
35
36#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
38pub enum DecodeSource {
39 VerifiedAbi,
41 FourByteDirectory,
43 Unknown,
45}
46
47#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
49pub struct DecodedParam {
50 pub name: String,
52 pub param_type: String,
54 pub value: String,
56}
57
58#[derive(Deserialize)]
60struct FourByteResponse {
61 results: Vec<FourByteResult>,
62}
63
64#[derive(Deserialize)]
65struct FourByteResult {
66 text_signature: String,
67}
68
69#[derive(Deserialize)]
71struct OpenChainResponse {
72 ok: bool,
73 result: Option<OpenChainResult>,
74}
75
76#[derive(Deserialize)]
77struct OpenChainResult {
78 function: Option<std::collections::HashMap<String, Vec<OpenChainEntry>>>,
79}
80
81#[derive(Deserialize)]
82struct OpenChainEntry {
83 name: String,
84}
85
86pub async fn decode_calldata(
94 input: &str,
95 contract_source: Option<&ContractSource>,
96 http_client: &dyn crate::http::HttpClient,
97) -> DecodedCall {
98 if input.is_empty() || input == "0x" {
99 return DecodedCall {
100 selector: "0x".to_string(),
101 signature: "()".to_string(),
102 function_name: "Native Transfer".to_string(),
103 parameters: vec![],
104 source: DecodeSource::Unknown,
105 };
106 }
107
108 let selector = if input.len() >= 10 {
109 &input[..10]
110 } else {
111 input
112 };
113
114 if let Some(source) = contract_source
116 && let Some(entry) = find_abi_by_selector(selector, &source.parsed_abi)
117 {
118 let params = decode_params_from_abi(input, entry);
119 return DecodedCall {
120 selector: selector.to_string(),
121 signature: entry.signature(),
122 function_name: entry.name.clone(),
123 parameters: params,
124 source: DecodeSource::VerifiedAbi,
125 };
126 }
127
128 if let Some(sig) = lookup_openchain(selector, http_client).await {
130 let fn_name = sig.split('(').next().unwrap_or(&sig).to_string();
131 return DecodedCall {
132 selector: selector.to_string(),
133 signature: sig,
134 function_name: fn_name,
135 parameters: vec![],
136 source: DecodeSource::FourByteDirectory,
137 };
138 }
139
140 if let Some(sig) = lookup_4byte(selector, http_client).await {
142 let fn_name = sig.split('(').next().unwrap_or(&sig).to_string();
143 return DecodedCall {
144 selector: selector.to_string(),
145 signature: sig,
146 function_name: fn_name,
147 parameters: vec![],
148 source: DecodeSource::FourByteDirectory,
149 };
150 }
151
152 DecodedCall {
154 selector: selector.to_string(),
155 signature: format!("{}(...)", selector),
156 function_name: selector.to_string(),
157 parameters: vec![],
158 source: DecodeSource::Unknown,
159 }
160}
161
162async fn lookup_openchain(
164 selector: &str,
165 http_client: &dyn crate::http::HttpClient,
166) -> Option<String> {
167 let url = format!(
168 "https://api.openchain.xyz/signature-database/v1/lookup?function={}&filter=true",
169 selector
170 );
171
172 let response = http_client
173 .send(crate::http::Request::get(&url))
174 .await
175 .ok()?;
176 let body: OpenChainResponse = response.json().ok()?;
177
178 if !body.ok {
179 return None;
180 }
181
182 body.result
183 .and_then(|r| r.function)
184 .and_then(|mut f| f.remove(selector))
185 .and_then(|entries| entries.into_iter().next())
186 .map(|e| e.name)
187}
188
189async fn lookup_4byte(selector: &str, http_client: &dyn crate::http::HttpClient) -> Option<String> {
191 let hex = selector.trim_start_matches("0x");
192 let url = format!(
193 "https://www.4byte.directory/api/v1/signatures/?hex_signature=0x{}",
194 hex
195 );
196
197 let response = http_client
198 .send(crate::http::Request::get(&url))
199 .await
200 .ok()?;
201 let body: FourByteResponse = response.json().ok()?;
202 body.results.into_iter().next().map(|r| r.text_signature)
203}
204
205fn find_abi_by_selector<'a>(selector: &str, abi: &'a [AbiEntry]) -> Option<&'a AbiEntry> {
207 let selector_lower = selector.to_lowercase();
208 abi.iter().find(|entry| {
209 entry.entry_type == "function" && entry.selector().to_lowercase() == selector_lower
210 })
211}
212
213fn decode_params_from_abi(input: &str, entry: &AbiEntry) -> Vec<DecodedParam> {
218 let data = if input.len() > 10 { &input[10..] } else { "" };
219
220 entry
221 .inputs
222 .iter()
223 .enumerate()
224 .map(|(i, param)| {
225 let offset = i * 64;
226 let raw_value = if offset + 64 <= data.len() {
227 &data[offset..offset + 64]
228 } else if offset < data.len() {
229 &data[offset..]
230 } else {
231 ""
232 };
233
234 let value = decode_abi_value(raw_value, ¶m.param_type);
235
236 DecodedParam {
237 name: if param.name.is_empty() {
238 format!("arg{}", i)
239 } else {
240 param.name.clone()
241 },
242 param_type: param.param_type.clone(),
243 value,
244 }
245 })
246 .collect()
247}
248
249fn decode_abi_value(hex_value: &str, solidity_type: &str) -> String {
251 if hex_value.is_empty() {
252 return "(empty)".to_string();
253 }
254
255 match solidity_type {
256 "address" => {
257 let addr = if hex_value.len() >= 40 {
259 &hex_value[hex_value.len() - 40..]
260 } else {
261 hex_value
262 };
263 format!("0x{}", addr)
264 }
265 t if t.starts_with("uint") || t.starts_with("int") => {
266 let trimmed = hex_value.trim_start_matches('0');
268 if trimmed.is_empty() {
269 "0".to_string()
270 } else if trimmed.len() <= 16 {
271 u64::from_str_radix(trimmed, 16)
273 .map(|v| v.to_string())
274 .unwrap_or_else(|_| format!("0x{}", hex_value))
275 } else {
276 format!("0x{}", trimmed)
277 }
278 }
279 "bool" => {
280 let last_char = hex_value.chars().last().unwrap_or('0');
281 if last_char == '1' {
282 "true".to_string()
283 } else {
284 "false".to_string()
285 }
286 }
287 t if t.starts_with("bytes") && !t.contains('[') => {
288 let n: usize = t.trim_start_matches("bytes").parse().unwrap_or(32);
290 let hex_chars = n * 2;
291 let bytes_hex = if hex_value.len() >= hex_chars {
292 &hex_value[..hex_chars]
293 } else {
294 hex_value
295 };
296 format!("0x{}", bytes_hex)
297 }
298 _ => {
299 if hex_value.len() > 16 {
301 format!("0x{}...", &hex_value[..16])
302 } else {
303 format!("0x{}", hex_value)
304 }
305 }
306 }
307}
308
309pub fn build_selector_map(abi: &[AbiEntry]) -> std::collections::HashMap<String, String> {
311 abi.iter()
312 .filter(|e| e.entry_type == "function")
313 .map(|e| (e.selector().to_lowercase(), e.signature()))
314 .collect()
315}
316
317pub fn get_state_changing_functions(abi: &[AbiEntry]) -> Vec<&AbiEntry> {
319 abi.iter().filter(|e| e.is_state_changing()).collect()
320}
321
322pub fn get_events(abi: &[AbiEntry]) -> Vec<&AbiEntry> {
324 abi.iter().filter(|e| e.entry_type == "event").collect()
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use crate::contract::source::AbiParam;
331
332 #[test]
333 fn test_decode_address() {
334 let hex = "000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7";
335 let result = decode_abi_value(hex, "address");
336 assert_eq!(result, "0xdac17f958d2ee523a2206206994597c13d831ec7");
337 }
338
339 #[test]
340 fn test_decode_uint256() {
341 let hex = "0000000000000000000000000000000000000000000000000000000005f5e100";
342 let result = decode_abi_value(hex, "uint256");
343 assert_eq!(result, "100000000");
344 }
345
346 #[test]
347 fn test_decode_uint256_zero() {
348 let hex = "0000000000000000000000000000000000000000000000000000000000000000";
349 let result = decode_abi_value(hex, "uint256");
350 assert_eq!(result, "0");
351 }
352
353 #[test]
354 fn test_decode_bool_true() {
355 let hex = "0000000000000000000000000000000000000000000000000000000000000001";
356 let result = decode_abi_value(hex, "bool");
357 assert_eq!(result, "true");
358 }
359
360 #[test]
361 fn test_decode_bool_false() {
362 let hex = "0000000000000000000000000000000000000000000000000000000000000000";
363 let result = decode_abi_value(hex, "bool");
364 assert_eq!(result, "false");
365 }
366
367 #[test]
368 fn test_decode_empty() {
369 let result = decode_abi_value("", "uint256");
370 assert_eq!(result, "(empty)");
371 }
372
373 #[test]
374 fn test_native_transfer_decoding() {
375 let rt = tokio::runtime::Runtime::new().unwrap();
376 let result = rt.block_on(async {
377 let client = crate::http::NativeHttpClient::new().unwrap();
378 decode_calldata("0x", None, &client).await
379 });
380 assert_eq!(result.function_name, "Native Transfer");
381 }
382
383 #[test]
384 fn test_build_selector_map() {
385 let abi = vec![
386 AbiEntry {
387 entry_type: "function".to_string(),
388 name: "transfer".to_string(),
389 inputs: vec![
390 AbiParam {
391 name: "to".to_string(),
392 param_type: "address".to_string(),
393 indexed: false,
394 components: vec![],
395 },
396 AbiParam {
397 name: "amount".to_string(),
398 param_type: "uint256".to_string(),
399 indexed: false,
400 components: vec![],
401 },
402 ],
403 outputs: vec![],
404 state_mutability: "nonpayable".to_string(),
405 },
406 AbiEntry {
407 entry_type: "event".to_string(),
408 name: "Transfer".to_string(),
409 inputs: vec![],
410 outputs: vec![],
411 state_mutability: String::new(),
412 },
413 ];
414 let map = build_selector_map(&abi);
415 assert_eq!(map.len(), 1); assert!(map.values().any(|v| v == "transfer(address,uint256)"));
417 }
418
419 #[test]
420 fn test_get_state_changing_functions() {
421 let abi = vec![
422 AbiEntry {
423 entry_type: "function".to_string(),
424 name: "transfer".to_string(),
425 inputs: vec![],
426 outputs: vec![],
427 state_mutability: "nonpayable".to_string(),
428 },
429 AbiEntry {
430 entry_type: "function".to_string(),
431 name: "balanceOf".to_string(),
432 inputs: vec![],
433 outputs: vec![],
434 state_mutability: "view".to_string(),
435 },
436 ];
437 let sc_fns = get_state_changing_functions(&abi);
438 assert_eq!(sc_fns.len(), 1);
439 assert_eq!(sc_fns[0].name, "transfer");
440 }
441
442 #[test]
443 fn test_get_events() {
444 let abi = vec![
445 AbiEntry {
446 entry_type: "function".to_string(),
447 name: "transfer".to_string(),
448 inputs: vec![],
449 outputs: vec![],
450 state_mutability: "nonpayable".to_string(),
451 },
452 AbiEntry {
453 entry_type: "event".to_string(),
454 name: "Transfer".to_string(),
455 inputs: vec![],
456 outputs: vec![],
457 state_mutability: String::new(),
458 },
459 AbiEntry {
460 entry_type: "event".to_string(),
461 name: "Approval".to_string(),
462 inputs: vec![],
463 outputs: vec![],
464 state_mutability: String::new(),
465 },
466 ];
467 let events = get_events(&abi);
468 assert_eq!(events.len(), 2);
469 assert_eq!(events[0].name, "Transfer");
470 assert_eq!(events[1].name, "Approval");
471 }
472
473 #[test]
474 fn test_find_abi_by_selector_match() {
475 let entry = AbiEntry {
476 entry_type: "function".to_string(),
477 name: "transfer".to_string(),
478 inputs: vec![
479 AbiParam {
480 name: "to".to_string(),
481 param_type: "address".to_string(),
482 indexed: false,
483 components: vec![],
484 },
485 AbiParam {
486 name: "amount".to_string(),
487 param_type: "uint256".to_string(),
488 indexed: false,
489 components: vec![],
490 },
491 ],
492 outputs: vec![],
493 state_mutability: "nonpayable".to_string(),
494 };
495 let selector = entry.selector();
496 let abi = vec![entry];
497 let found = find_abi_by_selector(&selector, &abi);
498 assert!(found.is_some());
499 assert_eq!(found.unwrap().name, "transfer");
500 }
501
502 #[test]
503 fn test_find_abi_by_selector_no_match() {
504 let abi = vec![AbiEntry {
505 entry_type: "function".to_string(),
506 name: "transfer".to_string(),
507 inputs: vec![],
508 outputs: vec![],
509 state_mutability: "nonpayable".to_string(),
510 }];
511 let found = find_abi_by_selector("0xdeadbeef", &abi);
512 assert!(found.is_none());
513 }
514
515 #[test]
516 fn test_find_abi_by_selector_skips_events() {
517 let abi = vec![AbiEntry {
518 entry_type: "event".to_string(),
519 name: "Transfer".to_string(),
520 inputs: vec![],
521 outputs: vec![],
522 state_mutability: String::new(),
523 }];
524 let found = find_abi_by_selector("0xdeadbeef", &abi);
525 assert!(found.is_none());
526 }
527
528 #[test]
529 fn test_decode_params_from_abi_basic() {
530 let entry = AbiEntry {
531 entry_type: "function".to_string(),
532 name: "transfer".to_string(),
533 inputs: vec![
534 AbiParam {
535 name: "to".to_string(),
536 param_type: "address".to_string(),
537 indexed: false,
538 components: vec![],
539 },
540 AbiParam {
541 name: "amount".to_string(),
542 param_type: "uint256".to_string(),
543 indexed: false,
544 components: vec![],
545 },
546 ],
547 outputs: vec![],
548 state_mutability: "nonpayable".to_string(),
549 };
550 let selector = entry.selector();
551 let addr_pad = "000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7";
552 let amount_pad = "0000000000000000000000000000000000000000000000000000000005f5e100";
553 let input = format!("{}{}{}", selector, addr_pad, amount_pad);
554 let params = decode_params_from_abi(&input, &entry);
555 assert_eq!(params.len(), 2);
556 assert_eq!(params[0].name, "to");
557 assert_eq!(params[0].param_type, "address");
558 assert!(
559 params[0]
560 .value
561 .contains("dac17f958d2ee523a2206206994597c13d831ec7")
562 );
563 assert_eq!(params[1].name, "amount");
564 assert_eq!(params[1].value, "100000000");
565 }
566
567 #[test]
568 fn test_decode_params_from_abi_unnamed_params() {
569 let entry = AbiEntry {
570 entry_type: "function".to_string(),
571 name: "test".to_string(),
572 inputs: vec![AbiParam {
573 name: String::new(),
574 param_type: "bool".to_string(),
575 indexed: false,
576 components: vec![],
577 }],
578 outputs: vec![],
579 state_mutability: "nonpayable".to_string(),
580 };
581 let selector = entry.selector();
582 let bool_pad = "0000000000000000000000000000000000000000000000000000000000000001";
583 let input = format!("{}{}", selector, bool_pad);
584 let params = decode_params_from_abi(&input, &entry);
585 assert_eq!(params.len(), 1);
586 assert_eq!(params[0].name, "arg0");
587 assert_eq!(params[0].value, "true");
588 }
589
590 #[test]
591 fn test_decode_params_from_abi_short_data() {
592 let entry = AbiEntry {
593 entry_type: "function".to_string(),
594 name: "test".to_string(),
595 inputs: vec![
596 AbiParam {
597 name: "a".to_string(),
598 param_type: "uint256".to_string(),
599 indexed: false,
600 components: vec![],
601 },
602 AbiParam {
603 name: "b".to_string(),
604 param_type: "uint256".to_string(),
605 indexed: false,
606 components: vec![],
607 },
608 ],
609 outputs: vec![],
610 state_mutability: "nonpayable".to_string(),
611 };
612 let input = format!(
613 "0xa1b2c3d4{}",
614 "00000000000000000000000000000000000000000000000000000000000000ff00"
615 );
616 let params = decode_params_from_abi(&input, &entry);
617 assert_eq!(params.len(), 2);
618 assert_eq!(params[0].name, "a");
619 assert!(!params[1].value.is_empty());
620 }
621
622 #[test]
623 fn test_decode_params_no_data() {
624 let entry = AbiEntry {
625 entry_type: "function".to_string(),
626 name: "test".to_string(),
627 inputs: vec![AbiParam {
628 name: "x".to_string(),
629 param_type: "uint256".to_string(),
630 indexed: false,
631 components: vec![],
632 }],
633 outputs: vec![],
634 state_mutability: "nonpayable".to_string(),
635 };
636 let input = "0xa1b2c3d4";
637 let params = decode_params_from_abi(input, &entry);
638 assert_eq!(params.len(), 1);
639 assert_eq!(params[0].value, "(empty)");
640 }
641
642 #[test]
643 fn test_decode_abi_value_int() {
644 let hex = "0000000000000000000000000000000000000000000000000000000000000042";
645 assert_eq!(decode_abi_value(hex, "int256"), "66");
646 }
647
648 #[test]
649 fn test_decode_abi_value_large_uint() {
650 let hex = "00000000000000000000000000000000ffffffffffffffffffffffffffffffff";
651 let result = decode_abi_value(hex, "uint256");
652 assert!(result.starts_with("0x"));
653 }
654
655 #[test]
656 fn test_decode_abi_value_bytes4() {
657 let hex = "a9059cbb00000000000000000000000000000000000000000000000000000000";
658 let result = decode_abi_value(hex, "bytes4");
659 assert_eq!(result, "0xa9059cbb");
660 }
661
662 #[test]
663 fn test_decode_abi_value_bytes32() {
664 let hex = "a9059cbb000000000000000000000000000000000000000000000000deadbeef";
665 let result = decode_abi_value(hex, "bytes32");
666 assert!(result.starts_with("0x"));
667 assert_eq!(result.len(), 2 + 64);
668 }
669
670 #[test]
671 fn test_decode_abi_value_short_address() {
672 let hex = "deadbeef";
673 let result = decode_abi_value(hex, "address");
674 assert_eq!(result, "0xdeadbeef");
675 }
676
677 #[test]
678 fn test_decode_abi_value_dynamic_type_long() {
679 let hex = "0000000000000000000000000000000000000000000000000000000000000020";
680 let result = decode_abi_value(hex, "string");
681 assert!(result.starts_with("0x"));
682 assert!(result.ends_with("..."));
683 }
684
685 #[test]
686 fn test_decode_abi_value_dynamic_type_short() {
687 let hex = "abcdef01";
688 let result = decode_abi_value(hex, "bytes");
689 assert_eq!(result, "0xabcdef01");
690 }
691
692 #[test]
693 fn test_decode_calldata_empty_string() {
694 let rt = tokio::runtime::Runtime::new().unwrap();
695 let result = rt.block_on(async {
696 let client = crate::http::NativeHttpClient::new().unwrap();
697 decode_calldata("", None, &client).await
698 });
699 assert_eq!(result.function_name, "Native Transfer");
700 assert_eq!(result.selector, "0x");
701 }
702
703 #[test]
704 fn test_decode_calldata_short_selector() {
705 let rt = tokio::runtime::Runtime::new().unwrap();
706 let result = rt.block_on(async {
707 let client = crate::http::NativeHttpClient::new().unwrap();
708 decode_calldata("0xabcd", None, &client).await
709 });
710 assert_eq!(result.selector, "0xabcd");
711 }
712
713 #[test]
714 fn test_decode_calldata_with_abi() {
715 let entry = AbiEntry {
716 entry_type: "function".to_string(),
717 name: "transfer".to_string(),
718 inputs: vec![
719 AbiParam {
720 name: "to".to_string(),
721 param_type: "address".to_string(),
722 indexed: false,
723 components: vec![],
724 },
725 AbiParam {
726 name: "value".to_string(),
727 param_type: "uint256".to_string(),
728 indexed: false,
729 components: vec![],
730 },
731 ],
732 outputs: vec![],
733 state_mutability: "nonpayable".to_string(),
734 };
735 let selector = entry.selector();
736 let source = ContractSource {
737 contract_name: "Token".to_string(),
738 source_code: String::new(),
739 abi: String::new(),
740 compiler_version: String::new(),
741 optimization_used: false,
742 optimization_runs: 0,
743 evm_version: String::new(),
744 license_type: String::new(),
745 is_proxy: false,
746 implementation_address: None,
747 constructor_arguments: String::new(),
748 library: String::new(),
749 swarm_source: String::new(),
750 parsed_abi: vec![entry],
751 };
752 let addr_pad = "000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7";
753 let amt_pad = "0000000000000000000000000000000000000000000000000000000005f5e100";
754 let input = format!("{}{}{}", selector, addr_pad, amt_pad);
755
756 let rt = tokio::runtime::Runtime::new().unwrap();
757 let result = rt.block_on(async {
758 let client = crate::http::NativeHttpClient::new().unwrap();
759 decode_calldata(&input, Some(&source), &client).await
760 });
761 assert_eq!(result.function_name, "transfer");
762 assert_eq!(result.parameters.len(), 2);
763 matches!(result.source, DecodeSource::VerifiedAbi);
764 }
765
766 #[test]
767 fn test_decode_abi_value_uint_parse_error() {
768 let hex = "zzzzzzzzzzzzzzzz";
769 let result = decode_abi_value(hex, "uint256");
770 assert!(result.starts_with("0x"));
771 }
772
773 #[test]
774 fn test_decode_abi_value_bytes_without_size() {
775 let hex = "a9059cbb00000000000000000000000000000000000000000000000000000000";
776 let result = decode_abi_value(hex, "bytes");
777 assert!(result.starts_with("0x"));
778 }
779
780 #[test]
781 fn test_decode_abi_value_bool_non_one_zero() {
782 let hex = "0000000000000000000000000000000000000000000000000000000000000002";
783 let result = decode_abi_value(hex, "bool");
784 assert_eq!(result, "false");
785 }
786
787 #[test]
788 fn test_decode_params_from_abi_offset_exceeds_data() {
789 let entry = AbiEntry {
790 entry_type: "function".to_string(),
791 name: "twoArgs".to_string(),
792 inputs: vec![
793 AbiParam {
794 name: "a".to_string(),
795 param_type: "uint256".to_string(),
796 indexed: false,
797 components: vec![],
798 },
799 AbiParam {
800 name: "b".to_string(),
801 param_type: "uint256".to_string(),
802 indexed: false,
803 components: vec![],
804 },
805 ],
806 outputs: vec![],
807 state_mutability: "nonpayable".to_string(),
808 };
809 let selector = entry.selector();
810 let single_param_data = "0000000000000000000000000000000000000000000000000000000000000042";
811 let input = format!("{}{}", selector, single_param_data);
812 let params = decode_params_from_abi(&input, &entry);
813 assert_eq!(params.len(), 2);
814 assert_eq!(params[0].value, "66");
815 assert_eq!(params[1].value, "(empty)");
816 }
817
818 #[test]
819 fn test_decode_params_from_abi_partial_second_param() {
820 let entry = AbiEntry {
821 entry_type: "function".to_string(),
822 name: "twoArgs".to_string(),
823 inputs: vec![
824 AbiParam {
825 name: "a".to_string(),
826 param_type: "uint256".to_string(),
827 indexed: false,
828 components: vec![],
829 },
830 AbiParam {
831 name: "b".to_string(),
832 param_type: "uint256".to_string(),
833 indexed: false,
834 components: vec![],
835 },
836 ],
837 outputs: vec![],
838 state_mutability: "nonpayable".to_string(),
839 };
840 let selector = entry.selector();
841 let first_full = "0000000000000000000000000000000000000000000000000000000000000042";
842 let second_partial = "000000000000000000000000000000000000000000000000000000000000";
843 let input = format!("{}{}{}", selector, first_full, second_partial);
844 let params = decode_params_from_abi(&input, &entry);
845 assert_eq!(params.len(), 2);
846 assert_eq!(params[0].value, "66");
847 }
848
849 #[test]
850 fn test_decode_calldata_raw_selector_fallback() {
851 let rt = tokio::runtime::Runtime::new().unwrap();
852 let result = rt.block_on(async {
853 let client = crate::http::NativeHttpClient::new().unwrap();
854 decode_calldata("0xdeadbeef", None, &client).await
855 });
856 assert_eq!(result.selector, "0xdeadbeef");
857 matches!(
858 result.source,
859 DecodeSource::Unknown | DecodeSource::FourByteDirectory
860 );
861 }
862
863 #[test]
864 fn test_decoded_call_serialize_deserialize() {
865 let call = DecodedCall {
866 selector: "0xa9059cbb".to_string(),
867 signature: "transfer(address,uint256)".to_string(),
868 function_name: "transfer".to_string(),
869 parameters: vec![DecodedParam {
870 name: "to".to_string(),
871 param_type: "address".to_string(),
872 value: "0xdac17f958d2ee523a2206206994597c13d831ec7".to_string(),
873 }],
874 source: DecodeSource::VerifiedAbi,
875 };
876 let json = serde_json::to_string(&call).unwrap();
877 let restored: DecodedCall = serde_json::from_str(&json).unwrap();
878 assert_eq!(restored.selector, call.selector);
879 assert_eq!(restored.function_name, call.function_name);
880 assert_eq!(restored.parameters.len(), 1);
881 }
882 struct Routes {
886 routes: Vec<(&'static str, &'static str)>,
887 urls: std::sync::Mutex<Vec<String>>,
888 }
889
890 impl Routes {
891 fn new(routes: Vec<(&'static str, &'static str)>) -> Self {
892 Self {
893 routes,
894 urls: Default::default(),
895 }
896 }
897 }
898
899 #[async_trait::async_trait]
900 impl crate::http::HttpClient for Routes {
901 async fn send(
902 &self,
903 req: crate::http::Request,
904 ) -> std::result::Result<crate::http::Response, crate::error::ScopeError> {
905 self.urls.lock().unwrap().push(req.url.clone());
906 let (_, body) = self
907 .routes
908 .iter()
909 .find(|(fragment, _)| req.url.contains(fragment))
910 .ok_or_else(|| crate::error::ScopeError::Network("unreachable".into()))?;
911 Ok(crate::http::Response {
912 status_code: 200,
913 headers: Default::default(),
914 body: body.to_string(),
915 })
916 }
917 }
918
919 const OPENCHAIN: &str = "api.openchain.xyz";
920 const FOURBYTE: &str = "4byte.directory";
921
922 #[tokio::test]
923 async fn test_decode_calldata_names_the_function_from_openchain() {
924 let http = Routes::new(vec![(
925 OPENCHAIN,
926 r#"{"ok": true, "result": {"function": {"0xa9059cbb": [{"name": "transfer(address,uint256)"}, {"name": "other()"}]}}}"#,
927 )]);
928 let call = decode_calldata("0xa9059cbb000000", None, &http).await;
929 assert_eq!(call.selector, "0xa9059cbb");
930 assert_eq!(call.signature, "transfer(address,uint256)");
931 assert_eq!(call.function_name, "transfer");
932 assert!(call.parameters.is_empty());
933 assert!(matches!(call.source, DecodeSource::FourByteDirectory));
934 let urls = http.urls.lock().unwrap();
935 assert_eq!(
936 *urls,
937 vec![
938 "https://api.openchain.xyz/signature-database/v1/lookup?function=0xa9059cbb&filter=true"
939 ],
940 "4byte is not asked once OpenChain answers"
941 );
942 }
943
944 #[tokio::test]
945 async fn test_decode_calldata_falls_back_to_4byte_when_openchain_has_nothing() {
946 for openchain in [
947 r#"{"ok": false}"#,
948 r#"{"ok": true, "result": {"function": {"0x095ea7b3": []}}}"#,
949 r#"{"ok": true, "result": {"function": {"0xffffffff": [{"name": "x()"}]}}}"#,
950 r#"{"ok": true, "result": null}"#,
951 "not json",
952 ] {
953 let http = Routes::new(vec![
954 (OPENCHAIN, openchain),
955 (
956 FOURBYTE,
957 r#"{"results": [{"text_signature": "approve(address,uint256)"}]}"#,
958 ),
959 ]);
960 let call = decode_calldata("0x095ea7b3", None, &http).await;
961 assert_eq!(call.signature, "approve(address,uint256)", "{openchain}");
962 assert_eq!(call.function_name, "approve");
963 assert!(matches!(call.source, DecodeSource::FourByteDirectory));
964 let urls = http.urls.lock().unwrap();
965 assert_eq!(
966 urls[1],
967 "https://www.4byte.directory/api/v1/signatures/?hex_signature=0x095ea7b3"
968 );
969 }
970 }
971
972 #[tokio::test]
973 async fn test_decode_calldata_without_any_lookup_shows_the_raw_selector() {
974 for http in [
975 Routes::new(vec![]),
976 Routes::new(vec![
977 (OPENCHAIN, r#"{"ok": false}"#),
978 (FOURBYTE, r#"{"results": []}"#),
979 ]),
980 ] {
981 let call = decode_calldata("0xdeadbeef01", None, &http).await;
982 assert_eq!(call.selector, "0xdeadbeef");
983 assert_eq!(call.signature, "0xdeadbeef(...)");
984 assert_eq!(call.function_name, "0xdeadbeef");
985 assert!(matches!(call.source, DecodeSource::Unknown));
986 assert_eq!(http.urls.lock().unwrap().len(), 2);
987 }
988 }
989
990 #[test]
991 fn test_decode_abi_value_short_dynamic_type_is_shown_whole() {
992 assert_eq!(decode_abi_value("abcdef01", "string"), "0xabcdef01");
994 assert_eq!(decode_abi_value("abcdef01", "address[]"), "0xabcdef01");
995 }
996}