stratus/eth/
decode.rs

1use ethabi::ParamType;
2use ethabi::Token;
3
4use crate::eth::codegen::SIGNATURES_4_BYTES;
5use crate::eth::primitives::DecodeInputError;
6
7/// Decodes the input arguments of a transaction.
8pub fn decode_input_arguments(input: impl AsRef<[u8]>) -> Result<String, DecodeInputError> {
9    let Some(selector) = input.as_ref().get(..4) else {
10        return Err(DecodeInputError::InputTooShort {
11            message: format!("expected at least 4 bytes for function selector, got {} bytes", input.as_ref().len()),
12        });
13    };
14    let Some(signature) = SIGNATURES_4_BYTES.get(selector) else {
15        return Err(DecodeInputError::FunctionUnknown {
16            message: format!("selector 0x{} not found in signature mapping", const_hex::encode(selector)),
17        });
18    };
19    let param_types = parse_to_param_types(signature)?;
20    let param_data = input.as_ref().get(4..).ok_or(DecodeInputError::InputTooShort {
21        message: "input data too short for parameters after function selector".to_string(),
22    })?;
23    let tokens = ethabi::decode(&param_types, param_data)?;
24    Ok(format_tokens_human_readable(&tokens))
25}
26
27/// Parses a Solidity function signature string into parameter types.
28/// Example: "transfer(address,uint256)" -> [ParamType::Address, ParamType::Uint(256)]
29fn parse_to_param_types(signature: &str) -> Result<Vec<ParamType>, DecodeInputError> {
30    let start = signature.find('(').ok_or_else(|| DecodeInputError::InvalidAbi {
31        message: format!("invalid signature format: {signature} (missing opening parenthesis)"),
32    })?;
33    let end = signature.rfind(')').ok_or_else(|| DecodeInputError::InvalidAbi {
34        message: format!("invalid signature format: {signature} (missing closing parenthesis)"),
35    })?;
36    let params_str = &signature[start + 1..end];
37    if params_str.is_empty() {
38        return Ok(Vec::new());
39    }
40    tokenize_parameters(params_str)
41}
42
43/// Tokenizes parameter string while respecting nested parentheses for tuples.
44/// Example: "address,(uint32,uint32,uint64),bool" -> ["address", "(uint32,uint32,uint64)", "bool"]
45fn tokenize_parameters(params_str: &str) -> Result<Vec<ParamType>, DecodeInputError> {
46    let mut tokens = Vec::new();
47    let mut current_token = String::new();
48    let mut paren_depth = 0;
49    let mut bracket_open = false;
50
51    for ch in params_str.chars() {
52        match ch {
53            '(' => {
54                paren_depth += 1;
55                current_token.push(ch);
56            }
57            ')' => {
58                paren_depth -= 1;
59                current_token.push(ch);
60                if paren_depth < 0 {
61                    return Err(DecodeInputError::InvalidAbi {
62                        message: "unmatched closing parenthesis".to_string(),
63                    });
64                }
65            }
66            '[' => {
67                if bracket_open {
68                    return Err(DecodeInputError::InvalidAbi {
69                        message: "nested brackets are not allowed".to_string(),
70                    });
71                }
72                bracket_open = true;
73                current_token.push(ch);
74            }
75            ']' => {
76                if !bracket_open {
77                    return Err(DecodeInputError::InvalidAbi {
78                        message: "unmatched closing bracket".to_string(),
79                    });
80                }
81                bracket_open = false;
82                current_token.push(ch);
83            }
84            ',' => {
85                if paren_depth == 0 && !bracket_open {
86                    // We're at the top level, this comma separates parameters
87                    tokens.push(parse_solidity_type(current_token.trim())?);
88                    current_token.clear();
89                } else {
90                    // We're inside parentheses or brackets, this comma is part of the current token
91                    current_token.push(ch);
92                }
93            }
94            _ => {
95                current_token.push(ch);
96            }
97        }
98    }
99
100    // Add the last token
101    if !current_token.trim().is_empty() {
102        tokens.push(parse_solidity_type(current_token.trim())?);
103    }
104
105    // Check for unmatched parentheses/brackets
106    if paren_depth != 0 {
107        return Err(DecodeInputError::InvalidAbi {
108            message: "unmatched parentheses".to_string(),
109        });
110    }
111    if bracket_open {
112        return Err(DecodeInputError::InvalidAbi {
113            message: "unmatched brackets".to_string(),
114        });
115    }
116
117    Ok(tokens)
118}
119
120fn parse_solidity_type(type_str: impl AsRef<str>) -> Result<ParamType, DecodeInputError> {
121    let type_str = type_str.as_ref();
122    match type_str {
123        s if s.ends_with("[]") => {
124            let inner_type = parse_solidity_type(&s[..s.len() - 2])?;
125            Ok(ParamType::Array(Box::new(inner_type)))
126        }
127        s if s.contains('[') && s.ends_with(']') => {
128            let bracket_pos = s.find('[').unwrap();
129            let inner_type = parse_solidity_type(&s[..bracket_pos])?;
130            let size_str = &s[bracket_pos + 1..s.len() - 1];
131            let size = size_str.parse::<usize>().map_err(|_| DecodeInputError::InvalidAbi {
132                message: format!("invalid array size in type: {s}"),
133            })?;
134            Ok(ParamType::FixedArray(Box::new(inner_type), size))
135        }
136        s if s.starts_with('(') && s.ends_with(')') => {
137            // Parse tuple type: (uint32,uint32,uint64) -> ParamType::Tuple
138            let inner_str = &s[1..s.len() - 1]; // Remove outer parentheses
139
140            if inner_str.is_empty() {
141                return Ok(ParamType::Tuple(Vec::new()));
142            }
143
144            Ok(ParamType::Tuple(tokenize_parameters(inner_str)?))
145        }
146        "address" => Ok(ParamType::Address),
147        "bool" => Ok(ParamType::Bool),
148        "string" => Ok(ParamType::String),
149        "bytes" => Ok(ParamType::Bytes),
150        s if s.starts_with("uint") => {
151            let size = if s == "uint" {
152                256
153            } else {
154                s[4..].parse::<usize>().map_err(|_| DecodeInputError::InvalidAbi {
155                    message: format!("invalid uint size in type: {s}"),
156                })?
157            };
158            Ok(ParamType::Uint(size))
159        }
160        s if s.starts_with("int") => {
161            let size = if s == "int" {
162                256
163            } else {
164                s[3..].parse::<usize>().map_err(|_| DecodeInputError::InvalidAbi {
165                    message: format!("invalid int size in type: {s}"),
166                })?
167            };
168            Ok(ParamType::Int(size))
169        }
170        s if s.starts_with("bytes") && s.len() > 5 => {
171            let size = s[5..].parse::<usize>().map_err(|_| DecodeInputError::InvalidAbi {
172                message: format!("invalid bytes size in type: {s}"),
173            })?;
174            Ok(ParamType::FixedBytes(size))
175        }
176        _ => Err(DecodeInputError::InvalidAbi {
177            message: format!("unsupported Solidity type: {type_str}"),
178        }),
179    }
180}
181
182// Formats decoded tokens into a human-readable string.
183fn format_tokens_human_readable(tokens: &[Token]) -> String {
184    let items: Vec<String> = tokens.iter().map(format_token).collect();
185    format!("({})", items.join(", "))
186}
187
188fn format_token(token: &Token) -> String {
189    match token {
190        Token::Address(addr) => format!("0x{addr:x}"),
191        Token::Uint(val) | Token::Int(val) => format!("{val}"),
192        Token::Bool(val) => val.to_string(),
193        Token::String(val) => format!("\"{val}\""),
194        Token::Bytes(val) => format!("0x{}", const_hex::encode(val)),
195        Token::FixedBytes(val) => format!("0x{}", const_hex::encode(val)),
196        Token::Array(arr) | Token::FixedArray(arr) => {
197            let items: Vec<String> = arr.iter().map(format_token).collect();
198            format!("[{}]", items.join(", "))
199        }
200        Token::Tuple(tuple) => {
201            let items: Vec<String> = tuple.iter().map(format_token).collect();
202            format!("({})", items.join(", "))
203        }
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use ethabi::Address;
210    use ethabi::ParamType;
211    use hex_literal::hex;
212
213    use super::*;
214
215    #[test]
216    fn test_parse_transfer_transaction_input() {
217        // Test transfer(address,uint256)
218        let mut tx_transfer = Vec::from(hex!("a9059cbb"));
219        tx_transfer.extend_from_slice(&ethabi::encode(&[
220            Token::Address(Address::from(hex!("1234567890123456789012345678901234567890"))),
221            Token::Uint(1000000000000000000u64.into()),
222        ]));
223        let result = decode_input_arguments(&tx_transfer).unwrap();
224
225        assert_eq!(result, "(0x1234567890123456789012345678901234567890, 1000000000000000000)");
226    }
227
228    #[test]
229    fn test_no_parameter_input() {
230        // Test underlying()
231        let tx_no_parameter = Vec::from(hex!("18160ddd"));
232        let result = decode_input_arguments(&tx_no_parameter).unwrap();
233
234        assert_eq!(result, "()");
235    }
236
237    #[test]
238    fn test_complex_input() {
239        // Test test(uint256,(string,bool,(int256,uint256[]))[])
240        let signature = "test(uint256,(string,bool,(int256,uint256[]))[])";
241        let param_types = parse_to_param_types(signature).unwrap();
242        assert_eq!(
243            param_types,
244            vec![
245                ParamType::Uint(256),
246                ParamType::Array(Box::new(ParamType::Tuple(vec![
247                    ParamType::String,
248                    ParamType::Bool,
249                    ParamType::Tuple(vec![ParamType::Int(256), ParamType::Array(Box::new(ParamType::Uint(256)))]),
250                ])))
251            ]
252        );
253        let param_data = ethabi::encode(&[
254            Token::Uint(1000000000000000000u64.into()),
255            Token::Array(vec![Token::Tuple(vec![
256                Token::String("test".to_string()),
257                Token::Bool(true),
258                Token::Tuple(vec![
259                    Token::Int(200000000000000000i128.into()),
260                    Token::Array(vec![Token::Uint(3000000000000000000u64.into())]),
261                ]),
262            ])]),
263        ]);
264
265        let tokens = ethabi::decode(&param_types, &param_data).expect("failed to decode parameters");
266        let result = format_tokens_human_readable(&tokens);
267        assert_eq!(
268            result,
269            r#"(1000000000000000000, [("test", true, (200000000000000000, [3000000000000000000]))])"#
270        );
271    }
272
273    #[test]
274    fn test_invalid_input() {
275        let invalid_input = Vec::from(hex!("a9059cbb"));
276        let result = decode_input_arguments(&invalid_input);
277        assert!(matches!(result, Err(DecodeInputError::InvalidInput { source: _ })));
278    }
279
280    #[test]
281    fn test_invalid_length() {
282        let invalid_input = Vec::from(hex!("a9059c"));
283        let result = decode_input_arguments(&invalid_input);
284        assert!(matches!(result, Err(DecodeInputError::InputTooShort { message: _ })));
285    }
286
287    #[test]
288    fn test_invalid_signature() {
289        let invalid_input = Vec::from(hex!("42000042"));
290        let result = decode_input_arguments(&invalid_input);
291        assert!(matches!(result, Err(DecodeInputError::FunctionUnknown { message: _ })));
292    }
293}