Skip to main content

scope/contract/
abi.rs

1//! # ABI Parsing and Function Signature Decoding
2//!
3//! Provides function selector lookup via 4byte.directory and ABI-based
4//! parameter decoding for transaction calldata.
5//!
6//! ## Function Signature Resolution
7//!
8//! 1. If contract ABI is available (verified source), decode directly from ABI
9//! 2. Otherwise, look up the 4-byte selector on 4byte.directory (free, no API key)
10//! 3. Fallback: display raw selector hex
11//!
12//! ## Calldata Decoding
13//!
14//! For verified contracts with ABI, decodes full calldata parameters using
15//! ABI type information. For unverified contracts, only the function name
16//! is resolved from the selector.
17
18use crate::contract::source::{AbiEntry, ContractSource};
19use serde::Deserialize;
20
21/// A decoded function call from transaction input data.
22#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
23pub struct DecodedCall {
24    /// 4-byte selector (e.g., "0xa9059cbb").
25    pub selector: String,
26    /// Function signature (e.g., "transfer(address,uint256)").
27    pub signature: String,
28    /// Human-readable function name (e.g., "transfer").
29    pub function_name: String,
30    /// Decoded parameters (if ABI available).
31    pub parameters: Vec<DecodedParam>,
32    /// Whether the decoding came from verified ABI or 4byte.directory.
33    pub source: DecodeSource,
34}
35
36/// Source of the function signature resolution.
37#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
38pub enum DecodeSource {
39    /// Decoded from contract's verified ABI.
40    VerifiedAbi,
41    /// Looked up from 4byte.directory.
42    FourByteDirectory,
43    /// Only raw selector available.
44    Unknown,
45}
46
47/// A decoded parameter from calldata.
48#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
49pub struct DecodedParam {
50    /// Parameter name (from ABI, or "arg0", "arg1" if unknown).
51    pub name: String,
52    /// Solidity type.
53    pub param_type: String,
54    /// Decoded value as string.
55    pub value: String,
56}
57
58/// 4byte.directory API response.
59#[derive(Deserialize)]
60struct FourByteResponse {
61    results: Vec<FourByteResult>,
62}
63
64#[derive(Deserialize)]
65struct FourByteResult {
66    text_signature: String,
67}
68
69/// OpenChain (Samczsun) signature lookup response.
70#[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
86/// Decode transaction input data using available sources.
87///
88/// Resolution order:
89/// 1. ABI from verified source (most accurate)
90/// 2. OpenChain/Samczsun signature database
91/// 3. 4byte.directory
92/// 4. Raw selector fallback
93pub async fn decode_calldata(
94    input: &str,
95    contract_source: Option<&ContractSource>,
96    http_client: &reqwest::Client,
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    // Try ABI-based decoding first
115    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    // Try OpenChain (Samczsun) lookup
129    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    // Try 4byte.directory lookup
141    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    // Fallback: raw selector
153    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
162/// Look up a function signature on OpenChain (Samczsun's database).
163async fn lookup_openchain(selector: &str, http_client: &reqwest::Client) -> Option<String> {
164    let url = format!(
165        "https://api.openchain.xyz/signature-database/v1/lookup?function={}&filter=true",
166        selector
167    );
168
169    let response = http_client.get(&url).send().await.ok()?;
170    let body: OpenChainResponse = response.json().await.ok()?;
171
172    if !body.ok {
173        return None;
174    }
175
176    body.result
177        .and_then(|r| r.function)
178        .and_then(|mut f| f.remove(selector))
179        .and_then(|entries| entries.into_iter().next())
180        .map(|e| e.name)
181}
182
183/// Look up a function signature on 4byte.directory.
184async fn lookup_4byte(selector: &str, http_client: &reqwest::Client) -> Option<String> {
185    let hex = selector.trim_start_matches("0x");
186    let url = format!(
187        "https://www.4byte.directory/api/v1/signatures/?hex_signature=0x{}",
188        hex
189    );
190
191    let response = http_client.get(&url).send().await.ok()?;
192    let body: FourByteResponse = response.json().await.ok()?;
193    body.results.into_iter().next().map(|r| r.text_signature)
194}
195
196/// Find an ABI entry matching a 4-byte selector.
197fn find_abi_by_selector<'a>(selector: &str, abi: &'a [AbiEntry]) -> Option<&'a AbiEntry> {
198    let selector_lower = selector.to_lowercase();
199    abi.iter().find(|entry| {
200        entry.entry_type == "function" && entry.selector().to_lowercase() == selector_lower
201    })
202}
203
204/// Decode calldata parameters using ABI type information.
205///
206/// This performs basic hex-to-value decoding for common Solidity types.
207/// For complex types (dynamic arrays, nested tuples), returns hex representation.
208fn decode_params_from_abi(input: &str, entry: &AbiEntry) -> Vec<DecodedParam> {
209    let data = if input.len() > 10 { &input[10..] } else { "" };
210
211    entry
212        .inputs
213        .iter()
214        .enumerate()
215        .map(|(i, param)| {
216            let offset = i * 64;
217            let raw_value = if offset + 64 <= data.len() {
218                &data[offset..offset + 64]
219            } else if offset < data.len() {
220                &data[offset..]
221            } else {
222                ""
223            };
224
225            let value = decode_abi_value(raw_value, &param.param_type);
226
227            DecodedParam {
228                name: if param.name.is_empty() {
229                    format!("arg{}", i)
230                } else {
231                    param.name.clone()
232                },
233                param_type: param.param_type.clone(),
234                value,
235            }
236        })
237        .collect()
238}
239
240/// Decode a single ABI-encoded value from hex.
241fn decode_abi_value(hex_value: &str, solidity_type: &str) -> String {
242    if hex_value.is_empty() {
243        return "(empty)".to_string();
244    }
245
246    match solidity_type {
247        "address" => {
248            // Address is right-padded in 32 bytes, take last 40 chars
249            let addr = if hex_value.len() >= 40 {
250                &hex_value[hex_value.len() - 40..]
251            } else {
252                hex_value
253            };
254            format!("0x{}", addr)
255        }
256        t if t.starts_with("uint") || t.starts_with("int") => {
257            // Parse as big number (show decimal for reasonable values)
258            let trimmed = hex_value.trim_start_matches('0');
259            if trimmed.is_empty() {
260                "0".to_string()
261            } else if trimmed.len() <= 16 {
262                // Fits in u64
263                u64::from_str_radix(trimmed, 16)
264                    .map(|v| v.to_string())
265                    .unwrap_or_else(|_| format!("0x{}", hex_value))
266            } else {
267                format!("0x{}", trimmed)
268            }
269        }
270        "bool" => {
271            let last_char = hex_value.chars().last().unwrap_or('0');
272            if last_char == '1' {
273                "true".to_string()
274            } else {
275                "false".to_string()
276            }
277        }
278        t if t.starts_with("bytes") && !t.contains('[') => {
279            // Fixed-size bytes
280            let n: usize = t.trim_start_matches("bytes").parse().unwrap_or(32);
281            let hex_chars = n * 2;
282            let bytes_hex = if hex_value.len() >= hex_chars {
283                &hex_value[..hex_chars]
284            } else {
285                hex_value
286            };
287            format!("0x{}", bytes_hex)
288        }
289        _ => {
290            // Dynamic types (string, bytes, arrays): show truncated hex
291            if hex_value.len() > 16 {
292                format!("0x{}...", &hex_value[..16])
293            } else {
294                format!("0x{}", hex_value)
295            }
296        }
297    }
298}
299
300/// Build a selector-to-signature map from a contract's parsed ABI.
301pub fn build_selector_map(abi: &[AbiEntry]) -> std::collections::HashMap<String, String> {
302    abi.iter()
303        .filter(|e| e.entry_type == "function")
304        .map(|e| (e.selector().to_lowercase(), e.signature()))
305        .collect()
306}
307
308/// Get all state-changing functions from an ABI.
309pub fn get_state_changing_functions(abi: &[AbiEntry]) -> Vec<&AbiEntry> {
310    abi.iter().filter(|e| e.is_state_changing()).collect()
311}
312
313/// Get all events from an ABI.
314pub fn get_events(abi: &[AbiEntry]) -> Vec<&AbiEntry> {
315    abi.iter().filter(|e| e.entry_type == "event").collect()
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321    use crate::contract::source::AbiParam;
322
323    #[test]
324    fn test_decode_address() {
325        let hex = "000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7";
326        let result = decode_abi_value(hex, "address");
327        assert_eq!(result, "0xdac17f958d2ee523a2206206994597c13d831ec7");
328    }
329
330    #[test]
331    fn test_decode_uint256() {
332        let hex = "0000000000000000000000000000000000000000000000000000000005f5e100";
333        let result = decode_abi_value(hex, "uint256");
334        assert_eq!(result, "100000000");
335    }
336
337    #[test]
338    fn test_decode_uint256_zero() {
339        let hex = "0000000000000000000000000000000000000000000000000000000000000000";
340        let result = decode_abi_value(hex, "uint256");
341        assert_eq!(result, "0");
342    }
343
344    #[test]
345    fn test_decode_bool_true() {
346        let hex = "0000000000000000000000000000000000000000000000000000000000000001";
347        let result = decode_abi_value(hex, "bool");
348        assert_eq!(result, "true");
349    }
350
351    #[test]
352    fn test_decode_bool_false() {
353        let hex = "0000000000000000000000000000000000000000000000000000000000000000";
354        let result = decode_abi_value(hex, "bool");
355        assert_eq!(result, "false");
356    }
357
358    #[test]
359    fn test_decode_empty() {
360        let result = decode_abi_value("", "uint256");
361        assert_eq!(result, "(empty)");
362    }
363
364    #[test]
365    fn test_native_transfer_decoding() {
366        let rt = tokio::runtime::Runtime::new().unwrap();
367        let result = rt.block_on(async {
368            let client = reqwest::Client::new();
369            decode_calldata("0x", None, &client).await
370        });
371        assert_eq!(result.function_name, "Native Transfer");
372    }
373
374    #[test]
375    fn test_build_selector_map() {
376        let abi = vec![
377            AbiEntry {
378                entry_type: "function".to_string(),
379                name: "transfer".to_string(),
380                inputs: vec![
381                    AbiParam {
382                        name: "to".to_string(),
383                        param_type: "address".to_string(),
384                        indexed: false,
385                        components: vec![],
386                    },
387                    AbiParam {
388                        name: "amount".to_string(),
389                        param_type: "uint256".to_string(),
390                        indexed: false,
391                        components: vec![],
392                    },
393                ],
394                outputs: vec![],
395                state_mutability: "nonpayable".to_string(),
396            },
397            AbiEntry {
398                entry_type: "event".to_string(),
399                name: "Transfer".to_string(),
400                inputs: vec![],
401                outputs: vec![],
402                state_mutability: String::new(),
403            },
404        ];
405        let map = build_selector_map(&abi);
406        assert_eq!(map.len(), 1); // Only functions, not events
407        assert!(map.values().any(|v| v == "transfer(address,uint256)"));
408    }
409
410    #[test]
411    fn test_get_state_changing_functions() {
412        let abi = vec![
413            AbiEntry {
414                entry_type: "function".to_string(),
415                name: "transfer".to_string(),
416                inputs: vec![],
417                outputs: vec![],
418                state_mutability: "nonpayable".to_string(),
419            },
420            AbiEntry {
421                entry_type: "function".to_string(),
422                name: "balanceOf".to_string(),
423                inputs: vec![],
424                outputs: vec![],
425                state_mutability: "view".to_string(),
426            },
427        ];
428        let sc_fns = get_state_changing_functions(&abi);
429        assert_eq!(sc_fns.len(), 1);
430        assert_eq!(sc_fns[0].name, "transfer");
431    }
432
433    #[test]
434    fn test_get_events() {
435        let abi = vec![
436            AbiEntry {
437                entry_type: "function".to_string(),
438                name: "transfer".to_string(),
439                inputs: vec![],
440                outputs: vec![],
441                state_mutability: "nonpayable".to_string(),
442            },
443            AbiEntry {
444                entry_type: "event".to_string(),
445                name: "Transfer".to_string(),
446                inputs: vec![],
447                outputs: vec![],
448                state_mutability: String::new(),
449            },
450            AbiEntry {
451                entry_type: "event".to_string(),
452                name: "Approval".to_string(),
453                inputs: vec![],
454                outputs: vec![],
455                state_mutability: String::new(),
456            },
457        ];
458        let events = get_events(&abi);
459        assert_eq!(events.len(), 2);
460        assert_eq!(events[0].name, "Transfer");
461        assert_eq!(events[1].name, "Approval");
462    }
463
464    #[test]
465    fn test_find_abi_by_selector_match() {
466        let entry = AbiEntry {
467            entry_type: "function".to_string(),
468            name: "transfer".to_string(),
469            inputs: vec![
470                AbiParam {
471                    name: "to".to_string(),
472                    param_type: "address".to_string(),
473                    indexed: false,
474                    components: vec![],
475                },
476                AbiParam {
477                    name: "amount".to_string(),
478                    param_type: "uint256".to_string(),
479                    indexed: false,
480                    components: vec![],
481                },
482            ],
483            outputs: vec![],
484            state_mutability: "nonpayable".to_string(),
485        };
486        let selector = entry.selector();
487        let abi = vec![entry];
488        let found = find_abi_by_selector(&selector, &abi);
489        assert!(found.is_some());
490        assert_eq!(found.unwrap().name, "transfer");
491    }
492
493    #[test]
494    fn test_find_abi_by_selector_no_match() {
495        let abi = vec![AbiEntry {
496            entry_type: "function".to_string(),
497            name: "transfer".to_string(),
498            inputs: vec![],
499            outputs: vec![],
500            state_mutability: "nonpayable".to_string(),
501        }];
502        let found = find_abi_by_selector("0xdeadbeef", &abi);
503        assert!(found.is_none());
504    }
505
506    #[test]
507    fn test_find_abi_by_selector_skips_events() {
508        let abi = vec![AbiEntry {
509            entry_type: "event".to_string(),
510            name: "Transfer".to_string(),
511            inputs: vec![],
512            outputs: vec![],
513            state_mutability: String::new(),
514        }];
515        let found = find_abi_by_selector("0xdeadbeef", &abi);
516        assert!(found.is_none());
517    }
518
519    #[test]
520    fn test_decode_params_from_abi_basic() {
521        let entry = AbiEntry {
522            entry_type: "function".to_string(),
523            name: "transfer".to_string(),
524            inputs: vec![
525                AbiParam {
526                    name: "to".to_string(),
527                    param_type: "address".to_string(),
528                    indexed: false,
529                    components: vec![],
530                },
531                AbiParam {
532                    name: "amount".to_string(),
533                    param_type: "uint256".to_string(),
534                    indexed: false,
535                    components: vec![],
536                },
537            ],
538            outputs: vec![],
539            state_mutability: "nonpayable".to_string(),
540        };
541        let selector = entry.selector();
542        let addr_pad = "000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7";
543        let amount_pad = "0000000000000000000000000000000000000000000000000000000005f5e100";
544        let input = format!("{}{}{}", selector, addr_pad, amount_pad);
545        let params = decode_params_from_abi(&input, &entry);
546        assert_eq!(params.len(), 2);
547        assert_eq!(params[0].name, "to");
548        assert_eq!(params[0].param_type, "address");
549        assert!(
550            params[0]
551                .value
552                .contains("dac17f958d2ee523a2206206994597c13d831ec7")
553        );
554        assert_eq!(params[1].name, "amount");
555        assert_eq!(params[1].value, "100000000");
556    }
557
558    #[test]
559    fn test_decode_params_from_abi_unnamed_params() {
560        let entry = AbiEntry {
561            entry_type: "function".to_string(),
562            name: "test".to_string(),
563            inputs: vec![AbiParam {
564                name: String::new(),
565                param_type: "bool".to_string(),
566                indexed: false,
567                components: vec![],
568            }],
569            outputs: vec![],
570            state_mutability: "nonpayable".to_string(),
571        };
572        let selector = entry.selector();
573        let bool_pad = "0000000000000000000000000000000000000000000000000000000000000001";
574        let input = format!("{}{}", selector, bool_pad);
575        let params = decode_params_from_abi(&input, &entry);
576        assert_eq!(params.len(), 1);
577        assert_eq!(params[0].name, "arg0");
578        assert_eq!(params[0].value, "true");
579    }
580
581    #[test]
582    fn test_decode_params_from_abi_short_data() {
583        let entry = AbiEntry {
584            entry_type: "function".to_string(),
585            name: "test".to_string(),
586            inputs: vec![
587                AbiParam {
588                    name: "a".to_string(),
589                    param_type: "uint256".to_string(),
590                    indexed: false,
591                    components: vec![],
592                },
593                AbiParam {
594                    name: "b".to_string(),
595                    param_type: "uint256".to_string(),
596                    indexed: false,
597                    components: vec![],
598                },
599            ],
600            outputs: vec![],
601            state_mutability: "nonpayable".to_string(),
602        };
603        let input = format!(
604            "0xa1b2c3d4{}",
605            "00000000000000000000000000000000000000000000000000000000000000ff00"
606        );
607        let params = decode_params_from_abi(&input, &entry);
608        assert_eq!(params.len(), 2);
609        assert_eq!(params[0].name, "a");
610        assert!(!params[1].value.is_empty());
611    }
612
613    #[test]
614    fn test_decode_params_no_data() {
615        let entry = AbiEntry {
616            entry_type: "function".to_string(),
617            name: "test".to_string(),
618            inputs: vec![AbiParam {
619                name: "x".to_string(),
620                param_type: "uint256".to_string(),
621                indexed: false,
622                components: vec![],
623            }],
624            outputs: vec![],
625            state_mutability: "nonpayable".to_string(),
626        };
627        let input = "0xa1b2c3d4";
628        let params = decode_params_from_abi(input, &entry);
629        assert_eq!(params.len(), 1);
630        assert_eq!(params[0].value, "(empty)");
631    }
632
633    #[test]
634    fn test_decode_abi_value_int() {
635        let hex = "0000000000000000000000000000000000000000000000000000000000000042";
636        assert_eq!(decode_abi_value(hex, "int256"), "66");
637    }
638
639    #[test]
640    fn test_decode_abi_value_large_uint() {
641        let hex = "00000000000000000000000000000000ffffffffffffffffffffffffffffffff";
642        let result = decode_abi_value(hex, "uint256");
643        assert!(result.starts_with("0x"));
644    }
645
646    #[test]
647    fn test_decode_abi_value_bytes4() {
648        let hex = "a9059cbb00000000000000000000000000000000000000000000000000000000";
649        let result = decode_abi_value(hex, "bytes4");
650        assert_eq!(result, "0xa9059cbb");
651    }
652
653    #[test]
654    fn test_decode_abi_value_bytes32() {
655        let hex = "a9059cbb000000000000000000000000000000000000000000000000deadbeef";
656        let result = decode_abi_value(hex, "bytes32");
657        assert!(result.starts_with("0x"));
658        assert_eq!(result.len(), 2 + 64);
659    }
660
661    #[test]
662    fn test_decode_abi_value_short_address() {
663        let hex = "deadbeef";
664        let result = decode_abi_value(hex, "address");
665        assert_eq!(result, "0xdeadbeef");
666    }
667
668    #[test]
669    fn test_decode_abi_value_dynamic_type_long() {
670        let hex = "0000000000000000000000000000000000000000000000000000000000000020";
671        let result = decode_abi_value(hex, "string");
672        assert!(result.starts_with("0x"));
673        assert!(result.ends_with("..."));
674    }
675
676    #[test]
677    fn test_decode_abi_value_dynamic_type_short() {
678        let hex = "abcdef01";
679        let result = decode_abi_value(hex, "bytes");
680        assert_eq!(result, "0xabcdef01");
681    }
682
683    #[test]
684    fn test_decode_calldata_empty_string() {
685        let rt = tokio::runtime::Runtime::new().unwrap();
686        let result = rt.block_on(async {
687            let client = reqwest::Client::new();
688            decode_calldata("", None, &client).await
689        });
690        assert_eq!(result.function_name, "Native Transfer");
691        assert_eq!(result.selector, "0x");
692    }
693
694    #[test]
695    fn test_decode_calldata_short_selector() {
696        let rt = tokio::runtime::Runtime::new().unwrap();
697        let result = rt.block_on(async {
698            let client = reqwest::Client::new();
699            decode_calldata("0xabcd", None, &client).await
700        });
701        assert_eq!(result.selector, "0xabcd");
702    }
703
704    #[test]
705    fn test_decode_calldata_with_abi() {
706        let entry = AbiEntry {
707            entry_type: "function".to_string(),
708            name: "transfer".to_string(),
709            inputs: vec![
710                AbiParam {
711                    name: "to".to_string(),
712                    param_type: "address".to_string(),
713                    indexed: false,
714                    components: vec![],
715                },
716                AbiParam {
717                    name: "value".to_string(),
718                    param_type: "uint256".to_string(),
719                    indexed: false,
720                    components: vec![],
721                },
722            ],
723            outputs: vec![],
724            state_mutability: "nonpayable".to_string(),
725        };
726        let selector = entry.selector();
727        let source = ContractSource {
728            contract_name: "Token".to_string(),
729            source_code: String::new(),
730            abi: String::new(),
731            compiler_version: String::new(),
732            optimization_used: false,
733            optimization_runs: 0,
734            evm_version: String::new(),
735            license_type: String::new(),
736            is_proxy: false,
737            implementation_address: None,
738            constructor_arguments: String::new(),
739            library: String::new(),
740            swarm_source: String::new(),
741            parsed_abi: vec![entry],
742        };
743        let addr_pad = "000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7";
744        let amt_pad = "0000000000000000000000000000000000000000000000000000000005f5e100";
745        let input = format!("{}{}{}", selector, addr_pad, amt_pad);
746
747        let rt = tokio::runtime::Runtime::new().unwrap();
748        let result = rt.block_on(async {
749            let client = reqwest::Client::new();
750            decode_calldata(&input, Some(&source), &client).await
751        });
752        assert_eq!(result.function_name, "transfer");
753        assert_eq!(result.parameters.len(), 2);
754        matches!(result.source, DecodeSource::VerifiedAbi);
755    }
756
757    #[test]
758    fn test_decode_abi_value_uint_parse_error() {
759        let hex = "zzzzzzzzzzzzzzzz";
760        let result = decode_abi_value(hex, "uint256");
761        assert!(result.starts_with("0x"));
762    }
763
764    #[test]
765    fn test_decode_abi_value_bytes_without_size() {
766        let hex = "a9059cbb00000000000000000000000000000000000000000000000000000000";
767        let result = decode_abi_value(hex, "bytes");
768        assert!(result.starts_with("0x"));
769    }
770
771    #[test]
772    fn test_decode_abi_value_bool_non_one_zero() {
773        let hex = "0000000000000000000000000000000000000000000000000000000000000002";
774        let result = decode_abi_value(hex, "bool");
775        assert_eq!(result, "false");
776    }
777
778    #[test]
779    fn test_decode_params_from_abi_offset_exceeds_data() {
780        let entry = AbiEntry {
781            entry_type: "function".to_string(),
782            name: "twoArgs".to_string(),
783            inputs: vec![
784                AbiParam {
785                    name: "a".to_string(),
786                    param_type: "uint256".to_string(),
787                    indexed: false,
788                    components: vec![],
789                },
790                AbiParam {
791                    name: "b".to_string(),
792                    param_type: "uint256".to_string(),
793                    indexed: false,
794                    components: vec![],
795                },
796            ],
797            outputs: vec![],
798            state_mutability: "nonpayable".to_string(),
799        };
800        let selector = entry.selector();
801        let single_param_data = "0000000000000000000000000000000000000000000000000000000000000042";
802        let input = format!("{}{}", selector, single_param_data);
803        let params = decode_params_from_abi(&input, &entry);
804        assert_eq!(params.len(), 2);
805        assert_eq!(params[0].value, "66");
806        assert_eq!(params[1].value, "(empty)");
807    }
808
809    #[test]
810    fn test_decode_params_from_abi_partial_second_param() {
811        let entry = AbiEntry {
812            entry_type: "function".to_string(),
813            name: "twoArgs".to_string(),
814            inputs: vec![
815                AbiParam {
816                    name: "a".to_string(),
817                    param_type: "uint256".to_string(),
818                    indexed: false,
819                    components: vec![],
820                },
821                AbiParam {
822                    name: "b".to_string(),
823                    param_type: "uint256".to_string(),
824                    indexed: false,
825                    components: vec![],
826                },
827            ],
828            outputs: vec![],
829            state_mutability: "nonpayable".to_string(),
830        };
831        let selector = entry.selector();
832        let first_full = "0000000000000000000000000000000000000000000000000000000000000042";
833        let second_partial = "000000000000000000000000000000000000000000000000000000000000";
834        let input = format!("{}{}{}", selector, first_full, second_partial);
835        let params = decode_params_from_abi(&input, &entry);
836        assert_eq!(params.len(), 2);
837        assert_eq!(params[0].value, "66");
838    }
839
840    #[test]
841    fn test_decode_calldata_raw_selector_fallback() {
842        let rt = tokio::runtime::Runtime::new().unwrap();
843        let result = rt.block_on(async {
844            let client = reqwest::Client::new();
845            decode_calldata("0xdeadbeef", None, &client).await
846        });
847        assert_eq!(result.selector, "0xdeadbeef");
848        matches!(
849            result.source,
850            DecodeSource::Unknown | DecodeSource::FourByteDirectory
851        );
852    }
853
854    #[test]
855    fn test_decoded_call_serialize_deserialize() {
856        let call = DecodedCall {
857            selector: "0xa9059cbb".to_string(),
858            signature: "transfer(address,uint256)".to_string(),
859            function_name: "transfer".to_string(),
860            parameters: vec![DecodedParam {
861                name: "to".to_string(),
862                param_type: "address".to_string(),
863                value: "0xdac17f958d2ee523a2206206994597c13d831ec7".to_string(),
864            }],
865            source: DecodeSource::VerifiedAbi,
866        };
867        let json = serde_json::to_string(&call).unwrap();
868        let restored: DecodedCall = serde_json::from_str(&json).unwrap();
869        assert_eq!(restored.selector, call.selector);
870        assert_eq!(restored.function_name, call.function_name);
871        assert_eq!(restored.parameters.len(), 1);
872    }
873}