Skip to main content

foundry_common_fmt/
dynamic.rs

1use super::{format_int_exp, format_uint_exp};
2use alloy_dyn_abi::{DynSolType, DynSolValue};
3use alloy_primitives::hex;
4use eyre::Result;
5use serde_json::{Map, Value};
6use std::{
7    collections::{BTreeMap, HashMap},
8    fmt,
9};
10
11/// [`DynSolValue`] formatter.
12struct DynValueFormatter {
13    raw: bool,
14}
15
16impl DynValueFormatter {
17    /// Recursively formats a [`DynSolValue`].
18    fn value(&self, value: &DynSolValue, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        match value {
20            DynSolValue::Address(inner) => write!(f, "{inner}"),
21            DynSolValue::Function(inner) => write!(f, "{inner}"),
22            DynSolValue::Bytes(inner) => f.write_str(&hex::encode_prefixed(inner)),
23            DynSolValue::FixedBytes(word, size) => {
24                f.write_str(&hex::encode_prefixed(&word[..*size]))
25            }
26            DynSolValue::Uint(inner, _) => {
27                if self.raw {
28                    write!(f, "{inner}")
29                } else {
30                    f.write_str(&format_uint_exp(*inner))
31                }
32            }
33            DynSolValue::Int(inner, _) => {
34                if self.raw {
35                    write!(f, "{inner}")
36                } else {
37                    f.write_str(&format_int_exp(*inner))
38                }
39            }
40            DynSolValue::Array(values) | DynSolValue::FixedArray(values) => {
41                f.write_str("[")?;
42                self.list(values, f)?;
43                f.write_str("]")
44            }
45            DynSolValue::Tuple(values) => self.tuple(values, f),
46            DynSolValue::String(inner) => {
47                if self.raw {
48                    write!(f, "{}", inner.escape_debug())
49                } else {
50                    write!(f, "{inner:?}") // escape strings
51                }
52            }
53            DynSolValue::Bool(inner) => write!(f, "{inner}"),
54            DynSolValue::CustomStruct { name, prop_names, tuple } => {
55                if self.raw {
56                    return self.tuple(tuple, f);
57                }
58
59                f.write_str(name)?;
60
61                if prop_names.len() == tuple.len() {
62                    f.write_str("({ ")?;
63
64                    for (i, (prop_name, value)) in std::iter::zip(prop_names, tuple).enumerate() {
65                        if i > 0 {
66                            f.write_str(", ")?;
67                        }
68                        f.write_str(prop_name)?;
69                        f.write_str(": ")?;
70                        self.value(value, f)?;
71                    }
72
73                    f.write_str(" })")
74                } else {
75                    self.tuple(tuple, f)
76                }
77            }
78        }
79    }
80
81    /// Recursively formats a comma-separated list of [`DynSolValue`]s.
82    fn list(&self, values: &[DynSolValue], f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        for (i, value) in values.iter().enumerate() {
84            if i > 0 {
85                f.write_str(", ")?;
86            }
87            self.value(value, f)?;
88        }
89        Ok(())
90    }
91
92    /// Formats the given values as a tuple.
93    fn tuple(&self, values: &[DynSolValue], f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        f.write_str("(")?;
95        self.list(values, f)?;
96        f.write_str(")")
97    }
98}
99
100/// Wrapper that implements [`Display`](fmt::Display) for a [`DynSolValue`].
101struct DynValueDisplay<'a> {
102    /// The value to display.
103    value: &'a DynSolValue,
104    /// The formatter.
105    formatter: DynValueFormatter,
106}
107
108impl<'a> DynValueDisplay<'a> {
109    /// Creates a new [`Display`](fmt::Display) wrapper for the given value.
110    const fn new(value: &'a DynSolValue, raw: bool) -> Self {
111        Self { value, formatter: DynValueFormatter { raw } }
112    }
113}
114
115impl fmt::Display for DynValueDisplay<'_> {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        self.formatter.value(self.value, f)
118    }
119}
120
121/// Parses string input as Token against the expected ParamType
122pub fn parse_tokens<'a, I: IntoIterator<Item = (&'a DynSolType, &'a str)>>(
123    params: I,
124) -> alloy_dyn_abi::Result<Vec<DynSolValue>> {
125    params.into_iter().map(|(param, value)| DynSolType::coerce_str(param, value)).collect()
126}
127
128/// Pretty-prints a slice of tokens using [`format_token`].
129pub fn format_tokens(tokens: &[DynSolValue]) -> impl Iterator<Item = String> + '_ {
130    tokens.iter().map(format_token)
131}
132
133/// Pretty-prints a slice of tokens using [`format_token_raw`].
134pub fn format_tokens_raw(tokens: &[DynSolValue]) -> impl Iterator<Item = String> + '_ {
135    tokens.iter().map(format_token_raw)
136}
137
138/// Pretty-prints the given value into a string suitable for user output.
139pub fn format_token(value: &DynSolValue) -> String {
140    DynValueDisplay::new(value, false).to_string()
141}
142
143/// Pretty-prints the given value into a string suitable for re-parsing as values later.
144///
145/// This means:
146/// - integers are not formatted with exponential notation hints
147/// - structs are formatted as tuples, losing the struct and property names
148pub fn format_token_raw(value: &DynSolValue) -> String {
149    DynValueDisplay::new(value, true).to_string()
150}
151
152/// Serializes given [DynSolValue] into a [serde_json::Value].
153///
154/// If `strict` is `true`, numeric values are serialized according to their Solidity type width:
155/// integers with `bits <= 64` become JSON numbers, while wider integer types are serialized as
156/// strings, even when a particular runtime value would fit into `i64`/`u64`.
157pub fn serialize_value_as_json(
158    value: DynSolValue,
159    defs: Option<&StructDefinitions>,
160    strict: bool,
161) -> Result<Value> {
162    if let Some(defs) = defs {
163        _serialize_value_as_json(value, defs, strict)
164    } else {
165        _serialize_value_as_json(value, &StructDefinitions::default(), strict)
166    }
167}
168
169fn _serialize_value_as_json(
170    value: DynSolValue,
171    defs: &StructDefinitions,
172    strict: bool,
173) -> Result<Value> {
174    match value {
175        DynSolValue::Bool(b) => Ok(Value::Bool(b)),
176        DynSolValue::String(s) => {
177            // Strings are allowed to contain stringified JSON objects, so we try to parse it like
178            // one first.
179            if let Ok(map) = serde_json::from_str(&s) {
180                Ok(Value::Object(map))
181            } else {
182                Ok(Value::String(s))
183            }
184        }
185        DynSolValue::Bytes(b) => Ok(Value::String(hex::encode_prefixed(b))),
186        DynSolValue::FixedBytes(b, size) => Ok(Value::String(hex::encode_prefixed(&b[..size]))),
187        DynSolValue::Int(i, bits) => {
188            match (i64::try_from(i), strict) {
189                // In strict mode, return as number only if the type dictates so
190                (Ok(n), true) if bits <= 64 => Ok(Value::Number(n.into())),
191                // In normal mode, return as number if the number can be accurately represented.
192                (Ok(n), false) => Ok(Value::Number(n.into())),
193                // Otherwise, fallback to its string representation to preserve precision and ensure
194                // compatibility with alloy's `DynSolType` coercion.
195                _ => Ok(Value::String(i.to_string())),
196            }
197        }
198        DynSolValue::Uint(i, bits) => {
199            match (u64::try_from(i), strict) {
200                // In strict mode, return as number only if the type dictates so
201                (Ok(n), true) if bits <= 64 => Ok(Value::Number(n.into())),
202                // In normal mode, return as number if the number can be accurately represented.
203                (Ok(n), false) => Ok(Value::Number(n.into())),
204                // Otherwise, fallback to its string representation to preserve precision and ensure
205                // compatibility with alloy's `DynSolType` coercion.
206                _ => Ok(Value::String(i.to_string())),
207            }
208        }
209        DynSolValue::Address(a) => Ok(Value::String(a.to_string())),
210        DynSolValue::Array(e) | DynSolValue::FixedArray(e) => Ok(Value::Array(
211            e.into_iter()
212                .map(|v| _serialize_value_as_json(v, defs, strict))
213                .collect::<Result<_>>()?,
214        )),
215        DynSolValue::CustomStruct { name, prop_names, tuple } => {
216            let values = tuple
217                .into_iter()
218                .map(|v| _serialize_value_as_json(v, defs, strict))
219                .collect::<Result<Vec<_>>>()?;
220            let mut map: HashMap<String, Value> = prop_names.into_iter().zip(values).collect();
221
222            // If the struct def is known, manually build a `Map` to preserve the order.
223            if let Some(fields) = defs.get(&name)? {
224                let mut ordered_map = Map::with_capacity(fields.len());
225                for (field_name, _) in fields {
226                    if let Some(serialized_value) = map.remove(field_name) {
227                        ordered_map.insert(field_name.clone(), serialized_value);
228                    }
229                }
230                // Explicitly return a `Value::Object` to avoid ambiguity.
231                return Ok(Value::Object(ordered_map));
232            }
233
234            // Otherwise, fall back to alphabetical sorting for deterministic output.
235            Ok(Value::Object(map.into_iter().collect::<Map<String, Value>>()))
236        }
237        DynSolValue::Tuple(values) => Ok(Value::Array(
238            values
239                .into_iter()
240                .map(|v| _serialize_value_as_json(v, defs, strict))
241                .collect::<Result<_>>()?,
242        )),
243        DynSolValue::Function(_) => {
244            eyre::bail!("cannot serialize function pointer");
245        }
246    }
247}
248
249// -- STRUCT DEFINITIONS -------------------------------------------------------
250
251pub type TypeDefMap = BTreeMap<String, Vec<(String, String)>>;
252
253#[derive(Debug, Clone, Default)]
254pub struct StructDefinitions(TypeDefMap);
255
256impl From<TypeDefMap> for StructDefinitions {
257    fn from(map: TypeDefMap) -> Self {
258        Self::new(map)
259    }
260}
261
262impl StructDefinitions {
263    pub const fn new(map: TypeDefMap) -> Self {
264        Self(map)
265    }
266
267    pub fn keys(&self) -> impl Iterator<Item = &String> {
268        self.0.keys()
269    }
270
271    pub fn values(&self) -> impl Iterator<Item = &[(String, String)]> {
272        self.0.values().map(|v| v.as_slice())
273    }
274
275    pub fn get(&self, key: &str) -> eyre::Result<Option<&[(String, String)]>> {
276        if let Some(value) = self.0.get(key) {
277            return Ok(Some(value));
278        }
279
280        let matches: Vec<&[(String, String)]> = self
281            .0
282            .iter()
283            .filter_map(|(k, v)| {
284                if let Some((_, struct_name)) = k.split_once('.')
285                    && struct_name == key
286                {
287                    return Some(v.as_slice());
288                }
289                None
290            })
291            .collect();
292
293        match matches.len() {
294            0 => Ok(None),
295            1 => Ok(Some(matches[0])),
296            _ => {
297                eyre::bail!(
298                    "there are several structs with the same name. Use `<contract_name>.{key}` instead."
299                );
300            }
301        }
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308    use alloy_primitives::{U256, address};
309
310    #[test]
311    fn parse_hex_uint() {
312        let ty = DynSolType::Uint(256);
313
314        let values = parse_tokens(std::iter::once((&ty, "100"))).unwrap();
315        assert_eq!(values, [DynSolValue::Uint(U256::from(100), 256)]);
316
317        let val: U256 = U256::from(100u64);
318        let hex_val = format!("0x{val:x}");
319        let values = parse_tokens(std::iter::once((&ty, hex_val.as_str()))).unwrap();
320        assert_eq!(values, [DynSolValue::Uint(U256::from(100), 256)]);
321    }
322
323    #[test]
324    fn format_addr() {
325        // copied from testcases in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md
326        assert_eq!(
327            format_token(&DynSolValue::Address(address!(
328                "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
329            ))),
330            "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
331        );
332
333        // copied from testcases in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1191.md
334        assert_ne!(
335            format_token(&DynSolValue::Address(address!(
336                "0xFb6916095cA1Df60bb79ce92cE3EA74c37c5d359"
337            ))),
338            "0xFb6916095cA1Df60bb79ce92cE3EA74c37c5d359"
339        );
340    }
341
342    #[test]
343    fn strict_uint256_array_is_homogeneous() {
344        let small = U256::from(1u64);
345        let big = U256::from(1u64) << 200;
346
347        let arr =
348            DynSolValue::Array(vec![DynSolValue::Uint(small, 256), DynSolValue::Uint(big, 256)]);
349
350        let json = serialize_value_as_json(arr, None, true).unwrap();
351
352        assert_eq!(
353            json,
354            serde_json::json!([
355                "1",
356                "1606938044258990275541962092341162602522202993782792835301376"
357            ])
358        );
359    }
360
361    proptest::proptest! {
362        #[test]
363        fn test_serialize_uint_as_json(l in 0u64..u64::MAX, h in ((u64::MAX as u128) + 1)..u128::MAX) {
364            let l_min_bits = (64 - l.leading_zeros()) as usize;
365            let h_min_bits = (128 - h.leading_zeros()) as usize;
366
367            // values that fit in u64 should be serialized as a number in !strict mode
368            assert_eq!(
369                serialize_value_as_json(DynSolValue::Uint(l.try_into().unwrap(), l_min_bits), None, false).unwrap(),
370                serde_json::json!(l)
371            );
372            // values that dont fit in u64 should be serialized as a string in !strict mode
373            assert_eq!(
374                serialize_value_as_json(DynSolValue::Uint(h.try_into().unwrap(), h_min_bits), None, false).unwrap(),
375                serde_json::json!(h.to_string())
376            );
377
378            // values should be serialized according to the type
379            // since l_min_bits <= 64, expect the serialization to be a number
380            assert_eq!(
381                serialize_value_as_json(DynSolValue::Uint(l.try_into().unwrap(), l_min_bits), None, true).unwrap(),
382                serde_json::json!(l)
383            );
384            // since `h_min_bits` is specified for the number `l`, expect the serialization to be a string
385            // even though `l` fits in a u64
386            assert_eq!(
387                serialize_value_as_json(DynSolValue::Uint(l.try_into().unwrap(), h_min_bits), None, true).unwrap(),
388                serde_json::json!(l.to_string())
389            );
390            // since `h_min_bits` is specified for the number `h`, expect the serialization to be a string
391            assert_eq!(
392                serialize_value_as_json(DynSolValue::Uint(h.try_into().unwrap(), h_min_bits), None, true).unwrap(),
393                serde_json::json!(h.to_string())
394            );
395        }
396
397        #[test]
398        fn test_serialize_int_as_json(l in 0i64..=i64::MAX, h in ((i64::MAX as i128) + 1)..=i128::MAX) {
399            let l_min_bits = (64 - (l as u64).leading_zeros()) as usize + 1;
400            let h_min_bits = (128 - (h as u128).leading_zeros()) as usize + 1;
401
402            // values that fit in i64 should be serialized as a number in !strict mode
403            assert_eq!(
404                serialize_value_as_json(DynSolValue::Int(l.try_into().unwrap(), l_min_bits), None, false).unwrap(),
405                serde_json::json!(l)
406            );
407            // values that dont fit in i64 should be serialized as a string in !strict mode
408            assert_eq!(
409                serialize_value_as_json(DynSolValue::Int(h.try_into().unwrap(), h_min_bits), None, false).unwrap(),
410                serde_json::json!(h.to_string())
411            );
412
413            // values should be serialized according to the type
414            // since l_min_bits <= 64, expect the serialization to be a number
415            assert_eq!(
416                serialize_value_as_json(DynSolValue::Int(l.try_into().unwrap(), l_min_bits), None, true).unwrap(),
417                serde_json::json!(l)
418            );
419            // since `h_min_bits` is specified for the number `l`, expect the serialization to be a string
420            // even though `l` fits in an i64
421            assert_eq!(
422                serialize_value_as_json(DynSolValue::Int(l.try_into().unwrap(), h_min_bits), None, true).unwrap(),
423                serde_json::json!(l.to_string())
424            );
425            // since `h_min_bits` is specified for the number `h`, expect the serialization to be a string
426            assert_eq!(
427                serialize_value_as_json(DynSolValue::Int(h.try_into().unwrap(), h_min_bits), None, true).unwrap(),
428                serde_json::json!(h.to_string())
429            );
430        }
431    }
432}