Skip to main content

chaincodec_evm/
encoder.rs

1//! ABI encoder — the inverse of the ABI decoder.
2//!
3//! Converts `NormalizedValue` inputs into EVM ABI-encoded calldata.
4//! Supports function calls (with 4-byte selector prefix) and raw tuple encoding.
5//!
6//! # Usage
7//! ```ignore
8//! let encoder = EvmEncoder::from_abi_json(ABI_JSON)?;
9//! let calldata = encoder.encode_call("transfer", &[
10//!     NormalizedValue::Address("0xd8dA...".into()),
11//!     NormalizedValue::Uint(1_000_000),
12//! ])?;
13//! ```
14
15use alloy_dyn_abi::Specifier;
16use alloy_core::dyn_abi::{DynSolType, DynSolValue};
17use alloy_json_abi::JsonAbi;
18use alloy_primitives::{Address, FixedBytes, I256, U256};
19use chaincodec_core::{error::DecodeError, types::NormalizedValue};
20use std::str::FromStr;
21
22/// ABI encoder for EVM function calls.
23pub struct EvmEncoder {
24    abi: JsonAbi,
25}
26
27impl EvmEncoder {
28    /// Create an encoder from a standard Ethereum ABI JSON string.
29    pub fn from_abi_json(abi_json: &str) -> Result<Self, DecodeError> {
30        let abi: JsonAbi = serde_json::from_str(abi_json)
31            .map_err(|e| DecodeError::AbiDecodeFailed {
32                reason: format!("invalid ABI JSON: {e}"),
33            })?;
34        Ok(Self { abi })
35    }
36
37    /// Encode a function call to calldata bytes.
38    ///
39    /// Returns `selector ++ abi_encode_packed(args...)` — the standard
40    /// EVM calldata format suitable for `eth_sendRawTransaction`.
41    ///
42    /// # Arguments
43    /// * `function_name` - the Solidity function name
44    /// * `args` - values in declaration order (must match ABI parameter count & types)
45    pub fn encode_call(
46        &self,
47        function_name: &str,
48        args: &[NormalizedValue],
49    ) -> Result<Vec<u8>, DecodeError> {
50        let func = self
51            .abi
52            .functions()
53            .find(|f| f.name == function_name)
54            .ok_or_else(|| DecodeError::SchemaNotFound {
55                fingerprint: format!("function '{function_name}' not found in ABI"),
56            })?;
57
58        if args.len() != func.inputs.len() {
59            return Err(DecodeError::AbiDecodeFailed {
60                reason: format!(
61                    "argument count mismatch: ABI has {}, got {}",
62                    func.inputs.len(),
63                    args.len()
64                ),
65            });
66        }
67
68        let mut dyn_values = Vec::with_capacity(args.len());
69        for (i, (param, arg)) in func.inputs.iter().zip(args.iter()).enumerate() {
70            let sol_type = param.resolve().map_err(|e| DecodeError::AbiDecodeFailed {
71                reason: format!("param {i}: {e}"),
72            })?;
73            let dyn_val = normalized_to_dyn_value(arg, &sol_type).map_err(|e| {
74                DecodeError::AbiDecodeFailed {
75                    reason: format!("param '{}': {e}", param.name),
76                }
77            })?;
78            dyn_values.push(dyn_val);
79        }
80
81        // Selector: 4-byte function selector
82        let selector = func.selector();
83
84        // ABI-encode the tuple of arguments
85        let encoded = DynSolValue::Tuple(dyn_values).abi_encode();
86
87        let mut calldata = selector.to_vec();
88        calldata.extend_from_slice(&encoded);
89        Ok(calldata)
90    }
91
92    /// Encode raw ABI tuple without a function selector.
93    ///
94    /// Useful for encoding constructor arguments.
95    pub fn encode_tuple(
96        &self,
97        type_strings: &[&str],
98        args: &[NormalizedValue],
99    ) -> Result<Vec<u8>, DecodeError> {
100        if type_strings.len() != args.len() {
101            return Err(DecodeError::AbiDecodeFailed {
102                reason: "type_strings and args length mismatch".into(),
103            });
104        }
105
106        let mut dyn_values = Vec::new();
107        for (ty_str, arg) in type_strings.iter().zip(args.iter()) {
108            let sol_type: DynSolType = ty_str.parse().map_err(|e: alloy_core::dyn_abi::Error| {
109                DecodeError::AbiDecodeFailed {
110                    reason: format!("type parse '{ty_str}': {e}"),
111                }
112            })?;
113            let dyn_val = normalized_to_dyn_value(arg, &sol_type)
114                .map_err(|e| DecodeError::AbiDecodeFailed { reason: e })?;
115            dyn_values.push(dyn_val);
116        }
117
118        Ok(DynSolValue::Tuple(dyn_values).abi_encode())
119    }
120}
121
122/// Convert a `NormalizedValue` to the alloy `DynSolValue` for the given expected type.
123pub fn normalized_to_dyn_value(
124    val: &NormalizedValue,
125    expected: &DynSolType,
126) -> Result<DynSolValue, String> {
127    match (val, expected) {
128        (NormalizedValue::Bool(b), DynSolType::Bool) => Ok(DynSolValue::Bool(*b)),
129
130        (NormalizedValue::Uint(u), DynSolType::Uint(bits)) => {
131            Ok(DynSolValue::Uint(U256::from(*u), *bits))
132        }
133        (NormalizedValue::BigUint(s), DynSolType::Uint(bits)) => {
134            let u = U256::from_str(s).map_err(|e| format!("BigUint parse: {e}"))?;
135            Ok(DynSolValue::Uint(u, *bits))
136        }
137
138        (NormalizedValue::Int(i), DynSolType::Int(bits)) => {
139            Ok(DynSolValue::Int(I256::try_from(*i).map_err(|e| e.to_string())?, *bits))
140        }
141        (NormalizedValue::BigInt(s), DynSolType::Int(bits)) => {
142            let i = I256::from_str(s).map_err(|e| format!("BigInt parse: {e}"))?;
143            Ok(DynSolValue::Int(i, *bits))
144        }
145
146        (NormalizedValue::Address(s), DynSolType::Address) => {
147            let addr = Address::from_str(s).map_err(|e| format!("address parse: {e}"))?;
148            Ok(DynSolValue::Address(addr))
149        }
150
151        (NormalizedValue::Bytes(b), DynSolType::Bytes) => Ok(DynSolValue::Bytes(b.clone())),
152
153        (NormalizedValue::Bytes(b), DynSolType::FixedBytes(n)) => {
154            if b.len() > *n {
155                return Err(format!("bytes{n}: got {} bytes", b.len()));
156            }
157            let mut arr = [0u8; 32];
158            arr[..*n.min(&b.len())].copy_from_slice(&b[..*n.min(&b.len())]);
159            Ok(DynSolValue::FixedBytes(FixedBytes::from_slice(&arr[..*n]), *n))
160        }
161
162        (NormalizedValue::Str(s), DynSolType::String) => Ok(DynSolValue::String(s.clone())),
163
164        (NormalizedValue::Array(elems), DynSolType::Array(inner)) => {
165            let dyn_elems: Result<Vec<_>, _> =
166                elems.iter().map(|e| normalized_to_dyn_value(e, inner)).collect();
167            Ok(DynSolValue::Array(dyn_elems?))
168        }
169
170        (NormalizedValue::Array(elems), DynSolType::FixedArray(inner, len)) => {
171            if elems.len() != *len {
172                return Err(format!("fixed array length mismatch: expected {len}, got {}", elems.len()));
173            }
174            let dyn_elems: Result<Vec<_>, _> =
175                elems.iter().map(|e| normalized_to_dyn_value(e, inner)).collect();
176            Ok(DynSolValue::FixedArray(dyn_elems?))
177        }
178
179        (NormalizedValue::Tuple(fields), DynSolType::Tuple(types)) => {
180            let dyn_elems: Result<Vec<_>, _> = fields
181                .iter()
182                .zip(types.iter())
183                .map(|((_, v), t)| normalized_to_dyn_value(v, t))
184                .collect();
185            Ok(DynSolValue::Tuple(dyn_elems?))
186        }
187
188        _ => Err(format!(
189            "cannot convert {:?} to {:?}",
190            std::mem::discriminant(val),
191            expected
192        )),
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    const ERC20_ABI: &str = r#"[
201        {
202            "name": "transfer",
203            "type": "function",
204            "inputs": [
205                {"name": "to", "type": "address"},
206                {"name": "amount", "type": "uint256"}
207            ],
208            "outputs": [{"name": "", "type": "bool"}],
209            "stateMutability": "nonpayable"
210        }
211    ]"#;
212
213    #[test]
214    fn encode_transfer() {
215        let encoder = EvmEncoder::from_abi_json(ERC20_ABI).unwrap();
216        let calldata = encoder
217            .encode_call(
218                "transfer",
219                &[
220                    NormalizedValue::Address(
221                        "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045".into(),
222                    ),
223                    NormalizedValue::Uint(1_000_000),
224                ],
225            )
226            .unwrap();
227
228        // First 4 bytes = selector for transfer(address,uint256) = 0xa9059cbb
229        assert_eq!(&calldata[..4], hex::decode("a9059cbb").unwrap().as_slice());
230        // Total length = 4 + 32 + 32 = 68 bytes
231        assert_eq!(calldata.len(), 68);
232    }
233
234    #[test]
235    fn wrong_arg_count_returns_error() {
236        let encoder = EvmEncoder::from_abi_json(ERC20_ABI).unwrap();
237        let result = encoder.encode_call("transfer", &[NormalizedValue::Uint(1)]);
238        assert!(result.is_err());
239    }
240
241    #[test]
242    fn roundtrip_encode_decode() {
243        use crate::call_decoder::EvmCallDecoder;
244
245        let encoder = EvmEncoder::from_abi_json(ERC20_ABI).unwrap();
246        let decoder = EvmCallDecoder::from_abi_json(ERC20_ABI).unwrap();
247
248        let original_to = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
249        let original_amount: u128 = 999_888;
250
251        let calldata = encoder
252            .encode_call(
253                "transfer",
254                &[
255                    NormalizedValue::Address(original_to.to_lowercase()),
256                    NormalizedValue::Uint(original_amount),
257                ],
258            )
259            .unwrap();
260
261        let decoded = decoder.decode_call(&calldata, None).unwrap();
262        assert_eq!(decoded.function_name, "transfer");
263
264        // uint256 normalizes to BigUint (bits > 128)
265        if let NormalizedValue::BigUint(amount_str) = &decoded.inputs[1].1 {
266            assert_eq!(amount_str.parse::<u128>().unwrap(), original_amount);
267        } else {
268            panic!("expected BigUint for uint256 amount, got {:?}", decoded.inputs[1].1);
269        }
270    }
271}