use tiny_keccak::{Hasher, Keccak};
use crate::error::DecodeError;
#[derive(Debug, Clone)]
pub struct FunctionSignature {
pub name: String,
pub params: Vec<ParamType>,
pub param_names: Vec<Option<String>>,
pub canonical: String,
pub selector: [u8; 4],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParamType {
Address,
Uint(usize),
Int(usize),
Bool,
Bytes,
FixedBytes(usize),
String,
Array(Box<ParamType>),
FixedArray(Box<ParamType>, usize),
Tuple(Vec<(Option<String>, ParamType)>),
}
impl ParamType {
pub fn is_dynamic(&self) -> bool {
match self {
ParamType::Bytes | ParamType::String => true,
ParamType::Array(_) => true,
ParamType::FixedArray(inner, _) => inner.is_dynamic(),
ParamType::Tuple(members) => members.iter().any(|(_, m)| m.is_dynamic()),
_ => false,
}
}
pub fn head_size(&self) -> usize {
if self.is_dynamic() {
32 } else {
match self {
ParamType::Tuple(members) => members.iter().map(|(_, m)| m.head_size()).sum(),
ParamType::FixedArray(inner, len) => inner.head_size() * len,
_ => 32, }
}
}
}
#[derive(Debug, Clone)]
pub struct DecodedArguments {
pub function_name: String,
pub selector: [u8; 4],
pub args: Vec<DecodedArgument>,
}
#[derive(Debug, Clone)]
pub struct DecodedArgument {
pub index: usize,
pub name: Option<String>,
pub param_type: ParamType,
pub value: ArgumentValue,
}
#[derive(Debug, Clone)]
pub enum ArgumentValue {
Address([u8; 20]),
Uint(Vec<u8>),
Int(Vec<u8>),
Bool(bool),
Bytes(Vec<u8>),
FixedBytes(Vec<u8>),
String(std::string::String),
Array(Vec<ArgumentValue>),
Tuple(Vec<(Option<String>, ArgumentValue)>),
}
impl ArgumentValue {
pub fn to_json_value(&self) -> serde_json::Value {
match self {
ArgumentValue::Address(addr) => {
serde_json::Value::String(format!("0x{}", hex::encode(addr)))
}
ArgumentValue::Uint(bytes) => {
let hex_str = format!("0x{}", hex::encode(bytes));
serde_json::Value::String(hex_str)
}
ArgumentValue::Int(bytes) => {
let hex_str = format!("0x{}", hex::encode(bytes));
serde_json::Value::String(hex_str)
}
ArgumentValue::Bool(b) => serde_json::Value::Bool(*b),
ArgumentValue::Bytes(b) => serde_json::Value::String(format!("0x{}", hex::encode(b))),
ArgumentValue::FixedBytes(b) => {
serde_json::Value::String(format!("0x{}", hex::encode(b)))
}
ArgumentValue::String(s) => serde_json::Value::String(s.clone()),
ArgumentValue::Array(items) => {
serde_json::Value::Array(items.iter().map(|i| i.to_json_value()).collect())
}
ArgumentValue::Tuple(items) => {
serde_json::Value::Array(items.iter().map(|(_, i)| i.to_json_value()).collect())
}
}
}
pub fn as_uint_bytes(&self) -> Option<[u8; 32]> {
match self {
ArgumentValue::Uint(b) | ArgumentValue::Int(b) => {
let mut result = [0u8; 32];
let start = 32usize.saturating_sub(b.len());
let copy_len = b.len().min(32);
result[start..start + copy_len].copy_from_slice(&b[b.len() - copy_len..]);
Some(result)
}
_ => None,
}
}
}
pub fn parse_signature(sig: &str) -> Result<FunctionSignature, DecodeError> {
let sig = sig.trim();
let open = sig
.find('(')
.ok_or_else(|| DecodeError::InvalidSignature(format!("missing '(' in: {sig}")))?;
if !sig.ends_with(')') {
return Err(DecodeError::InvalidSignature(format!(
"missing ')' in: {sig}"
)));
}
let name = sig[..open].to_string();
if name.is_empty() {
return Err(DecodeError::InvalidSignature(
"empty function name".to_string(),
));
}
let params_str = &sig[open + 1..sig.len() - 1];
let (params, param_names) = if params_str.is_empty() {
(vec![], vec![])
} else {
let named = parse_param_list_named(params_str)?;
let params = named.iter().map(|(p, _)| p.clone()).collect();
let names = named.into_iter().map(|(_, n)| n).collect();
(params, names)
};
let canonical = format!("{}({})", name, canonical_params(¶ms));
let selector = selector_from_signature(&canonical);
Ok(FunctionSignature {
name,
params,
param_names,
canonical,
selector,
})
}
fn parse_param_list_named(s: &str) -> Result<Vec<(ParamType, Option<String>)>, DecodeError> {
let mut result = Vec::new();
let mut depth = 0usize;
let mut start = 0;
for (i, c) in s.char_indices() {
match c {
'(' => depth += 1,
')' => {
depth = depth
.checked_sub(1)
.ok_or_else(|| DecodeError::InvalidSignature("unbalanced ')'".to_string()))?;
}
',' if depth == 0 => {
result.push(parse_param_with_name(s[start..i].trim())?);
start = i + 1;
}
_ => {}
}
}
if depth != 0 {
return Err(DecodeError::InvalidSignature(
"unbalanced parentheses".to_string(),
));
}
let last = s[start..].trim();
if !last.is_empty() {
result.push(parse_param_with_name(last)?);
}
Ok(result)
}
fn parse_param_list_with_names(s: &str) -> Result<Vec<(Option<String>, ParamType)>, DecodeError> {
let mut result = Vec::new();
let mut depth = 0usize;
let mut start = 0;
for (i, c) in s.char_indices() {
match c {
'(' => depth += 1,
')' => {
depth = depth
.checked_sub(1)
.ok_or_else(|| DecodeError::InvalidSignature("unbalanced ')'".to_string()))?;
}
',' if depth == 0 => {
let (pt, name) = parse_param_with_name(s[start..i].trim())?;
result.push((name, pt));
start = i + 1;
}
_ => {}
}
}
if depth != 0 {
return Err(DecodeError::InvalidSignature(
"unbalanced parentheses".to_string(),
));
}
let last = s[start..].trim();
if !last.is_empty() {
let (pt, name) = parse_param_with_name(last)?;
result.push((name, pt));
}
Ok(result)
}
fn parse_param_with_name(s: &str) -> Result<(ParamType, Option<String>), DecodeError> {
let s = s.trim();
if let Some(pos) = s.rfind([')', ']']) {
let after = s[pos + 1..].trim();
if after.is_empty() {
return Ok((parse_param_type(s)?, None));
}
let type_str = &s[..pos + 1];
return Ok((parse_param_type(type_str)?, Some(after.to_string())));
}
if let Some(space_pos) = s.find(' ') {
let type_str = &s[..space_pos];
let name = s[space_pos..].trim();
return Ok((parse_param_type(type_str)?, Some(name.to_string())));
}
Ok((parse_param_type(s)?, None))
}
fn parse_param_type(s: &str) -> Result<ParamType, DecodeError> {
let s = s.trim();
if let Some(bracket_pos) = s.rfind('[') {
if s.ends_with(']') {
let inner_str = &s[..bracket_pos];
let size_str = &s[bracket_pos + 1..s.len() - 1];
let inner = parse_param_type(inner_str)?;
if size_str.is_empty() {
return Ok(ParamType::Array(Box::new(inner)));
} else {
let size: usize = size_str.parse().map_err(|_| {
DecodeError::InvalidSignature(format!("invalid array size: {size_str}"))
})?;
return Ok(ParamType::FixedArray(Box::new(inner), size));
}
}
}
if s.starts_with('(') && s.ends_with(')') {
let inner = &s[1..s.len() - 1];
let members = if inner.is_empty() {
vec![]
} else {
parse_param_list_with_names(inner)?
};
return Ok(ParamType::Tuple(members));
}
match s {
"address" => Ok(ParamType::Address),
"bool" => Ok(ParamType::Bool),
"string" => Ok(ParamType::String),
"bytes" => Ok(ParamType::Bytes),
_ if s.starts_with("uint") => {
let bits = if s == "uint" {
256
} else {
s[4..].parse::<usize>().map_err(|_| {
DecodeError::InvalidSignature(format!("invalid uint width: {s}"))
})?
};
Ok(ParamType::Uint(bits))
}
_ if s.starts_with("int") => {
let bits = if s == "int" {
256
} else {
s[3..]
.parse::<usize>()
.map_err(|_| DecodeError::InvalidSignature(format!("invalid int width: {s}")))?
};
Ok(ParamType::Int(bits))
}
_ if s.starts_with("bytes") => {
let size: usize = s[5..]
.parse()
.map_err(|_| DecodeError::InvalidSignature(format!("invalid bytes width: {s}")))?;
Ok(ParamType::FixedBytes(size))
}
_ => Err(DecodeError::InvalidSignature(format!("unknown type: {s}"))),
}
}
fn canonical_params(params: &[ParamType]) -> String {
params
.iter()
.map(canonical_param)
.collect::<Vec<_>>()
.join(",")
}
fn canonical_param(p: &ParamType) -> String {
match p {
ParamType::Address => "address".to_string(),
ParamType::Uint(bits) => format!("uint{bits}"),
ParamType::Int(bits) => format!("int{bits}"),
ParamType::Bool => "bool".to_string(),
ParamType::Bytes => "bytes".to_string(),
ParamType::FixedBytes(size) => format!("bytes{size}"),
ParamType::String => "string".to_string(),
ParamType::Array(inner) => format!("{}[]", canonical_param(inner)),
ParamType::FixedArray(inner, size) => format!("{}[{size}]", canonical_param(inner)),
ParamType::Tuple(members) => {
let inner = members
.iter()
.map(|(_, p)| canonical_param(p))
.collect::<Vec<_>>()
.join(",");
format!("({inner})")
}
}
}
fn selector_from_signature(canonical: &str) -> [u8; 4] {
let mut hasher = Keccak::v256();
hasher.update(canonical.as_bytes());
let mut hash = [0u8; 32];
hasher.finalize(&mut hash);
[hash[0], hash[1], hash[2], hash[3]]
}
pub fn decode_calldata(
sig: &FunctionSignature,
calldata: &[u8],
) -> Result<DecodedArguments, DecodeError> {
if calldata.len() < 4 {
return Err(DecodeError::CalldataTooShort {
expected: 4,
actual: calldata.len(),
});
}
let actual_selector = &calldata[..4];
if actual_selector != sig.selector {
return Err(DecodeError::SelectorMismatch {
expected: hex::encode(sig.selector),
actual: hex::encode(actual_selector),
});
}
let data = &calldata[4..];
let mut args = Vec::with_capacity(sig.params.len());
let mut offset = 0;
for (i, param) in sig.params.iter().enumerate() {
let value = decode_value(param, data, offset, 0)?;
args.push(DecodedArgument {
index: i,
name: sig.param_names.get(i).cloned().flatten(),
param_type: param.clone(),
value,
});
offset += param.head_size();
}
Ok(DecodedArguments {
function_name: sig.name.clone(),
selector: sig.selector,
args,
})
}
fn decode_value(
param: &ParamType,
data: &[u8],
head_offset: usize,
base_offset: usize,
) -> Result<ArgumentValue, DecodeError> {
if param.is_dynamic() {
let relative_offset = read_u256_as_usize(data, head_offset)?;
let absolute_offset = base_offset + relative_offset;
decode_value_at(param, data, absolute_offset)
} else {
decode_value_at(param, data, head_offset)
}
}
fn decode_value_at(
param: &ParamType,
data: &[u8],
offset: usize,
) -> Result<ArgumentValue, DecodeError> {
ensure_bytes(data, offset, 32)?;
match param {
ParamType::Address => {
let word = &data[offset..offset + 32];
let mut addr = [0u8; 20];
addr.copy_from_slice(&word[12..32]);
Ok(ArgumentValue::Address(addr))
}
ParamType::Uint(_) | ParamType::Int(_) => {
let word = data[offset..offset + 32].to_vec();
if matches!(param, ParamType::Uint(_)) {
Ok(ArgumentValue::Uint(word))
} else {
Ok(ArgumentValue::Int(word))
}
}
ParamType::Bool => {
let b = data[offset + 31] != 0;
Ok(ArgumentValue::Bool(b))
}
ParamType::FixedBytes(size) => {
let bytes = data[offset..offset + size].to_vec();
Ok(ArgumentValue::FixedBytes(bytes))
}
ParamType::Bytes => {
let len = read_u256_as_usize(data, offset)?;
let start = offset + 32;
ensure_bytes(data, start, len)?;
Ok(ArgumentValue::Bytes(data[start..start + len].to_vec()))
}
ParamType::String => {
let len = read_u256_as_usize(data, offset)?;
let start = offset + 32;
ensure_bytes(data, start, len)?;
let s = std::str::from_utf8(&data[start..start + len])
.map_err(|e| DecodeError::InvalidEncoding(format!("invalid UTF-8: {e}")))?;
Ok(ArgumentValue::String(s.to_string()))
}
ParamType::Array(inner) => {
let len = read_u256_as_usize(data, offset)?;
let elements_start = offset + 32;
decode_array_elements(inner, data, elements_start, len, elements_start)
}
ParamType::FixedArray(inner, len) => {
decode_array_elements(inner, data, offset, *len, offset)
}
ParamType::Tuple(members) => {
let mut values = Vec::with_capacity(members.len());
let mut member_offset = offset;
for (name, member_type) in members {
let value = decode_value(member_type, data, member_offset, offset)?;
values.push((name.clone(), value));
member_offset += member_type.head_size();
}
Ok(ArgumentValue::Tuple(values))
}
}
}
fn decode_array_elements(
inner: &ParamType,
data: &[u8],
offset: usize,
len: usize,
base_offset: usize,
) -> Result<ArgumentValue, DecodeError> {
let mut values = Vec::with_capacity(len);
let mut elem_offset = offset;
let step = inner.head_size();
for _ in 0..len {
let value = decode_value(inner, data, elem_offset, base_offset)?;
values.push(value);
elem_offset += step;
}
Ok(ArgumentValue::Array(values))
}
fn read_u256_as_usize(data: &[u8], offset: usize) -> Result<usize, DecodeError> {
ensure_bytes(data, offset, 32)?;
let word = &data[offset..offset + 32];
for &b in &word[..24] {
if b != 0 {
return Err(DecodeError::InvalidEncoding(
"offset too large for usize".to_string(),
));
}
}
let mut bytes = [0u8; 8];
bytes.copy_from_slice(&word[24..32]);
Ok(u64::from_be_bytes(bytes) as usize)
}
fn ensure_bytes(data: &[u8], offset: usize, len: usize) -> Result<(), DecodeError> {
if offset + len > data.len() {
Err(DecodeError::CalldataTooShort {
expected: offset + len,
actual: data.len(),
})
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_signature() {
let sig = parse_signature("transfer(address,uint256)").unwrap();
assert_eq!(sig.name, "transfer");
assert_eq!(sig.params.len(), 2);
assert_eq!(sig.params[0], ParamType::Address);
assert_eq!(sig.params[1], ParamType::Uint(256));
assert_eq!(sig.canonical, "transfer(address,uint256)");
}
#[test]
fn test_parse_no_params() {
let sig = parse_signature("pause()").unwrap();
assert_eq!(sig.name, "pause");
assert!(sig.params.is_empty());
}
#[test]
fn test_parse_tuple_signature() {
let sig = parse_signature("foo((address,uint256),bool)").unwrap();
assert_eq!(sig.params.len(), 2);
assert_eq!(
sig.params[0],
ParamType::Tuple(vec![
(None, ParamType::Address),
(None, ParamType::Uint(256))
])
);
assert_eq!(sig.params[1], ParamType::Bool);
}
#[test]
fn test_parse_named_tuple_members() {
let sig = parse_signature("foo((uint256 value, uint256 deadline) permit)").unwrap();
assert_eq!(sig.params.len(), 1);
assert_eq!(sig.param_names[0], Some("permit".to_string()));
if let ParamType::Tuple(members) = &sig.params[0] {
assert_eq!(members.len(), 2);
assert_eq!(members[0].0, Some("value".to_string()));
assert_eq!(members[0].1, ParamType::Uint(256));
assert_eq!(members[1].0, Some("deadline".to_string()));
assert_eq!(members[1].1, ParamType::Uint(256));
} else {
panic!("expected Tuple");
}
}
#[test]
fn test_canonical_strips_tuple_names() {
let named = parse_signature("foo((uint256 value, uint256 deadline) permit)").unwrap();
let unnamed = parse_signature("foo((uint256,uint256))").unwrap();
assert_eq!(named.selector, unnamed.selector);
assert_eq!(named.canonical, unnamed.canonical);
}
#[test]
fn test_parse_array_types() {
let sig = parse_signature("foo(uint256[],address[3])").unwrap();
assert_eq!(
sig.params[0],
ParamType::Array(Box::new(ParamType::Uint(256)))
);
assert_eq!(
sig.params[1],
ParamType::FixedArray(Box::new(ParamType::Address), 3)
);
}
#[test]
fn test_selector_computation() {
let sig = parse_signature("transfer(address,uint256)").unwrap();
assert_eq!(hex::encode(sig.selector), "a9059cbb");
}
#[test]
fn test_decode_transfer_calldata() {
let sig = parse_signature("transfer(address,uint256)").unwrap();
let mut calldata = Vec::new();
calldata.extend_from_slice(&sig.selector);
let mut addr_word = [0u8; 32];
addr_word[31] = 1;
calldata.extend_from_slice(&addr_word);
let mut amount_word = [0u8; 32];
amount_word[30] = 0x03;
amount_word[31] = 0xe8;
calldata.extend_from_slice(&amount_word);
let decoded = decode_calldata(&sig, &calldata).unwrap();
assert_eq!(decoded.function_name, "transfer");
assert_eq!(decoded.args.len(), 2);
if let ArgumentValue::Address(addr) = &decoded.args[0].value {
assert_eq!(addr[19], 1);
} else {
panic!("expected Address");
}
if let ArgumentValue::Uint(bytes) = &decoded.args[1].value {
assert_eq!(bytes[30], 0x03);
assert_eq!(bytes[31], 0xe8);
} else {
panic!("expected Uint");
}
}
#[test]
fn test_decode_bool() {
let sig = parse_signature("setApproval(bool)").unwrap();
let mut calldata = Vec::new();
calldata.extend_from_slice(&sig.selector);
let mut word = [0u8; 32];
word[31] = 1;
calldata.extend_from_slice(&word);
let decoded = decode_calldata(&sig, &calldata).unwrap();
if let ArgumentValue::Bool(b) = decoded.args[0].value {
assert!(b);
} else {
panic!("expected Bool");
}
}
#[test]
fn test_selector_mismatch() {
let sig = parse_signature("transfer(address,uint256)").unwrap();
let calldata = [0u8; 36]; let result = decode_calldata(&sig, &calldata);
assert!(result.is_err());
}
#[test]
fn test_parse_all_basic_types() {
let sig = parse_signature("f(address,uint256,int128,bool,bytes,bytes32,string)").unwrap();
assert_eq!(sig.params[0], ParamType::Address);
assert_eq!(sig.params[1], ParamType::Uint(256));
assert_eq!(sig.params[2], ParamType::Int(128));
assert_eq!(sig.params[3], ParamType::Bool);
assert_eq!(sig.params[4], ParamType::Bytes);
assert_eq!(sig.params[5], ParamType::FixedBytes(32));
assert_eq!(sig.params[6], ParamType::String);
}
#[test]
fn test_default_uint_int() {
let sig = parse_signature("f(uint,int)").unwrap();
assert_eq!(sig.params[0], ParamType::Uint(256));
assert_eq!(sig.params[1], ParamType::Int(256));
}
#[test]
fn test_parse_named_params() {
let sig = parse_signature(
"deposit(address asset,uint256 amount,address onBehalfOf,uint16 referralCode)",
)
.unwrap();
assert_eq!(sig.name, "deposit");
assert_eq!(sig.params.len(), 4);
assert_eq!(sig.params[0], ParamType::Address);
assert_eq!(sig.params[1], ParamType::Uint(256));
assert_eq!(sig.params[2], ParamType::Address);
assert_eq!(sig.params[3], ParamType::Uint(16));
assert_eq!(
sig.param_names,
vec![
Some("asset".to_string()),
Some("amount".to_string()),
Some("onBehalfOf".to_string()),
Some("referralCode".to_string()),
]
);
assert_eq!(sig.canonical, "deposit(address,uint256,address,uint16)");
}
#[test]
fn test_parse_mixed_named_unnamed() {
let sig = parse_signature("f(address,uint256 amount)").unwrap();
assert_eq!(sig.param_names, vec![None, Some("amount".to_string())]);
}
#[test]
fn test_named_params_selector_unchanged() {
let named = parse_signature(
"deposit(address asset,uint256 amount,address onBehalfOf,uint16 referralCode)",
)
.unwrap();
let unnamed = parse_signature("deposit(address,uint256,address,uint16)").unwrap();
assert_eq!(named.selector, unnamed.selector);
assert_eq!(named.canonical, unnamed.canonical);
}
}