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        // Uint can represent Timestamp/Decimal too
189        (NormalizedValue::Uint(u), DynSolType::Uint(bits)) => {
190            Ok(DynSolValue::Uint(U256::from(*u), *bits))
191        }
192
193        _ => Err(format!(
194            "cannot convert {:?} to {:?}",
195            std::mem::discriminant(val),
196            expected
197        )),
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    const ERC20_ABI: &str = r#"[
206        {
207            "name": "transfer",
208            "type": "function",
209            "inputs": [
210                {"name": "to", "type": "address"},
211                {"name": "amount", "type": "uint256"}
212            ],
213            "outputs": [{"name": "", "type": "bool"}],
214            "stateMutability": "nonpayable"
215        }
216    ]"#;
217
218    #[test]
219    fn encode_transfer() {
220        let encoder = EvmEncoder::from_abi_json(ERC20_ABI).unwrap();
221        let calldata = encoder
222            .encode_call(
223                "transfer",
224                &[
225                    NormalizedValue::Address(
226                        "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045".into(),
227                    ),
228                    NormalizedValue::Uint(1_000_000),
229                ],
230            )
231            .unwrap();
232
233        // First 4 bytes = selector for transfer(address,uint256) = 0xa9059cbb
234        assert_eq!(&calldata[..4], hex::decode("a9059cbb").unwrap().as_slice());
235        // Total length = 4 + 32 + 32 = 68 bytes
236        assert_eq!(calldata.len(), 68);
237    }
238
239    #[test]
240    fn wrong_arg_count_returns_error() {
241        let encoder = EvmEncoder::from_abi_json(ERC20_ABI).unwrap();
242        let result = encoder.encode_call("transfer", &[NormalizedValue::Uint(1)]);
243        assert!(result.is_err());
244    }
245
246    #[test]
247    fn roundtrip_encode_decode() {
248        use crate::call_decoder::EvmCallDecoder;
249
250        let encoder = EvmEncoder::from_abi_json(ERC20_ABI).unwrap();
251        let decoder = EvmCallDecoder::from_abi_json(ERC20_ABI).unwrap();
252
253        let original_to = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
254        let original_amount: u128 = 999_888;
255
256        let calldata = encoder
257            .encode_call(
258                "transfer",
259                &[
260                    NormalizedValue::Address(original_to.to_lowercase()),
261                    NormalizedValue::Uint(original_amount),
262                ],
263            )
264            .unwrap();
265
266        let decoded = decoder.decode_call(&calldata, None).unwrap();
267        assert_eq!(decoded.function_name, "transfer");
268
269        if let NormalizedValue::Uint(amount) = &decoded.inputs[1].1 {
270            assert_eq!(*amount, original_amount);
271        } else {
272            panic!("expected Uint for amount");
273        }
274    }
275}