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: &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    // 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(
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
189/// Look up a function signature on 4byte.directory.
190async 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
205/// Find an ABI entry matching a 4-byte selector.
206fn 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
213/// Decode calldata parameters using ABI type information.
214///
215/// This performs basic hex-to-value decoding for common Solidity types.
216/// For complex types (dynamic arrays, nested tuples), returns hex representation.
217fn 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, &param.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
249/// Decode a single ABI-encoded value from hex.
250fn 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            // Address is right-padded in 32 bytes, take last 40 chars
258            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            // Parse as big number (show decimal for reasonable values)
267            let trimmed = hex_value.trim_start_matches('0');
268            if trimmed.is_empty() {
269                "0".to_string()
270            } else if trimmed.len() <= 16 {
271                // Fits in u64
272                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            // Fixed-size bytes
289            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            // Dynamic types (string, bytes, arrays): show truncated hex
300            if hex_value.len() > 16 {
301                format!("0x{}...", &hex_value[..16])
302            } else {
303                format!("0x{}", hex_value)
304            }
305        }
306    }
307}
308
309/// Build a selector-to-signature map from a contract's parsed ABI.
310pub 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
317/// Get all state-changing functions from an ABI.
318pub fn get_state_changing_functions(abi: &[AbiEntry]) -> Vec<&AbiEntry> {
319    abi.iter().filter(|e| e.is_state_changing()).collect()
320}
321
322/// Get all events from an ABI.
323pub 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); // Only functions, not events
416        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    /// Answers each request with the body of the first route whose URL
883    /// fragment the request URL contains, and notes every URL. A URL no
884    /// route matches is a transport error.
885    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        // "bytes" alone takes the fixed-size branch; "string" is dynamic.
993        assert_eq!(decode_abi_value("abcdef01", "string"), "0xabcdef01");
994        assert_eq!(decode_abi_value("abcdef01", "address[]"), "0xabcdef01");
995    }
996}