Skip to main content

chaincodec_evm/
normalizer.rs

1//! Converts alloy-core `DynSolValue` → ChainCodec `NormalizedValue`.
2//!
3//! This is where EVM ABI types are mapped to the canonical cross-chain
4//! type system defined in `chaincodec-core`.
5
6use alloy_core::dyn_abi::DynSolValue;
7use alloy_primitives::U256;
8use chaincodec_core::types::NormalizedValue;
9
10/// Convert a decoded `DynSolValue` into a `NormalizedValue`.
11pub fn normalize(val: DynSolValue) -> NormalizedValue {
12    match val {
13        DynSolValue::Bool(b) => NormalizedValue::Bool(b),
14
15        DynSolValue::Int(i, bits) => {
16            // For ints that fit in i128 return Int, else BigInt string
17            if bits <= 128 {
18                // alloy stores as two's-complement I256; safe to narrow
19                match i128::try_from(i) {
20                    Ok(v) => NormalizedValue::Int(v),
21                    Err(_) => NormalizedValue::BigInt(i.to_string()),
22                }
23            } else {
24                NormalizedValue::BigInt(i.to_string())
25            }
26        }
27
28        DynSolValue::Uint(u, bits) => {
29            if bits <= 128 {
30                match u128::try_from(u) {
31                    Ok(v) => NormalizedValue::Uint(v),
32                    Err(_) => NormalizedValue::BigUint(u.to_string()),
33                }
34            } else {
35                NormalizedValue::BigUint(u.to_string())
36            }
37        }
38
39        DynSolValue::FixedBytes(bytes, _size) => {
40            NormalizedValue::Bytes(bytes.to_vec())
41        }
42
43        DynSolValue::Bytes(b) => NormalizedValue::Bytes(b),
44
45        DynSolValue::String(s) => NormalizedValue::Str(s),
46
47        DynSolValue::Address(a) => {
48            // EIP-55 checksum encoding
49            NormalizedValue::Address(format!("{a:#x}"))
50        }
51
52        DynSolValue::Array(vals) | DynSolValue::FixedArray(vals) => {
53            NormalizedValue::Array(vals.into_iter().map(normalize).collect())
54        }
55
56        DynSolValue::Tuple(fields) => {
57            // Unnamed tuple fields get positional names "0", "1", ...
58            let named: Vec<(String, NormalizedValue)> = fields
59                .into_iter()
60                .enumerate()
61                .map(|(i, v)| (i.to_string(), normalize(v)))
62                .collect();
63            NormalizedValue::Tuple(named)
64        }
65
66        // Custom types (e.g. function selector) — fall back to bytes
67        DynSolValue::Function(f) => NormalizedValue::Bytes(f.to_vec()),
68    }
69}
70
71/// Parse a U256 big-endian hex string into a NormalizedValue.
72pub fn normalize_u256_hex(hex_str: &str) -> NormalizedValue {
73    let hex = hex_str.strip_prefix("0x").unwrap_or(hex_str);
74    match U256::from_str_radix(hex, 16) {
75        Ok(u) => match u128::try_from(u) {
76            Ok(v) => NormalizedValue::Uint(v),
77            Err(_) => NormalizedValue::BigUint(u.to_string()),
78        },
79        Err(_) => NormalizedValue::Null,
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use alloy_primitives::{Address, U256};
87
88    #[test]
89    fn normalize_bool() {
90        let v = normalize(DynSolValue::Bool(true));
91        assert_eq!(v, NormalizedValue::Bool(true));
92    }
93
94    #[test]
95    fn normalize_uint256_small() {
96        // 256-bit uints always go through BigUint path (bits > 128)
97        let u = DynSolValue::Uint(U256::from(42u64), 256);
98        let v = normalize(u);
99        assert_eq!(v, NormalizedValue::BigUint("42".into()));
100    }
101
102    #[test]
103    fn normalize_address() {
104        let addr: Address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
105            .parse()
106            .unwrap();
107        let v = normalize(DynSolValue::Address(addr));
108        assert!(matches!(v, NormalizedValue::Address(_)));
109        if let NormalizedValue::Address(s) = v {
110            assert!(s.starts_with("0x"));
111        }
112    }
113}