alloy_dyn_abi/eip712/
parser.rs

1//! EIP-712 specific parsing structures.
2
3// TODO: move to `sol-type-parser`
4
5use crate::{
6    Error,
7    eip712::resolver::{PropertyDef, TypeDef},
8};
9use alloc::{
10    string::{String, ToString},
11    vec::Vec,
12};
13use parser::{Error as TypeParserError, TypeSpecifier};
14
15use super::Resolver;
16
17/// A property is a type and a name. Of the form `type name`. E.g.
18/// `uint256 foo` or `(MyStruct[23],bool) bar`.
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct PropDef<'a> {
21    /// The prop type specifier.
22    pub ty: TypeSpecifier<'a>,
23    /// The prop name.
24    pub name: &'a str,
25}
26
27impl PropDef<'_> {
28    /// Convert to an owned `PropertyDef`
29    pub fn to_owned(&self) -> PropertyDef {
30        PropertyDef::new(self.ty.span, self.name).unwrap()
31    }
32}
33
34impl<'a> TryFrom<&'a str> for PropDef<'a> {
35    type Error = Error;
36
37    #[inline]
38    fn try_from(input: &'a str) -> Result<Self, Self::Error> {
39        Self::parse(input)
40    }
41}
42
43impl<'a> PropDef<'a> {
44    /// Parse a string into property definition.
45    ///
46    /// Unlike the std solidity struct parser, the EIP-712 parser supports `:` in type names for
47    /// namespace separation, which is used by some protocols like Hyperliquid. See the relevant
48    /// discussion at: <https://github.com/foundry-rs/foundry/issues/10765>.
49    ///
50    /// # Example
51    ///
52    /// ```
53    /// # use alloy_dyn_abi::eip712::parser::PropDef;
54    /// let prop = PropDef::parse("Hyperliquid:Message msg").unwrap();
55    /// assert_eq!(prop.ty.span(), "Hyperliquid:Message");
56    /// assert_eq!(prop.name, "msg");
57    /// ```
58    pub fn parse(input: &'a str) -> Result<Self, Error> {
59        let (ty, name) =
60            input.rsplit_once(' ').ok_or_else(|| Error::invalid_property_def(input))?;
61        Ok(PropDef { ty: TypeSpecifier::parse_eip712(ty.trim())?, name: name.trim() })
62    }
63}
64
65/// Represents a single component type in an EIP-712 `encodeType` type string.
66///
67/// <https://eips.ethereum.org/EIPS/eip-712#definition-of-encodetype>
68#[derive(Clone, Debug, PartialEq, Eq)]
69pub struct ComponentType<'a> {
70    /// The span.
71    pub span: &'a str,
72    /// The name of the component type.
73    pub type_name: &'a str,
74    /// Properties of the component type.
75    pub props: Vec<PropDef<'a>>,
76}
77
78impl<'a> TryFrom<&'a str> for ComponentType<'a> {
79    type Error = Error;
80
81    #[inline]
82    fn try_from(input: &'a str) -> Result<Self, Self::Error> {
83        Self::parse(input)
84    }
85}
86
87impl<'a> ComponentType<'a> {
88    /// Parse a string into a component type.
89    pub fn parse(input: &'a str) -> Result<Self, Error> {
90        let (name, props_str) = input
91            .split_once('(')
92            .ok_or_else(|| Error::TypeParser(TypeParserError::invalid_type_string(input)))?;
93
94        let mut props = vec![];
95        let mut depth = 1; // 1 to account for the ( in the split above
96        let mut last = 0;
97
98        for (i, c) in props_str.char_indices() {
99            match c {
100                '(' => depth += 1,
101                ')' => {
102                    depth -= 1;
103                    if depth == 0 {
104                        let candidate = &props_str[last..i];
105                        if !candidate.is_empty() {
106                            props.push(candidate.try_into()?);
107                        }
108                        last = i + c.len_utf8();
109                        break;
110                    }
111                }
112                ',' => {
113                    if depth == 1 {
114                        props.push(props_str[last..i].try_into()?);
115                        last = i + c.len_utf8();
116                    }
117                }
118                _ => {}
119            }
120        }
121
122        Ok(Self { span: &input[..last + name.len() + 1], type_name: name.trim(), props })
123    }
124
125    /// Convert to an owned TypeDef.
126    pub fn to_owned(&self) -> TypeDef {
127        TypeDef::new(self.type_name, self.props.iter().map(|p| p.to_owned()).collect()).unwrap()
128    }
129}
130
131/// Represents a list of component types in an EIP-712 `encodeType` type string.
132#[derive(Debug, PartialEq, Eq)]
133pub struct EncodeType<'a> {
134    /// The list of component types.
135    pub types: Vec<ComponentType<'a>>,
136}
137
138impl<'a> TryFrom<&'a str> for EncodeType<'a> {
139    type Error = Error;
140
141    #[inline]
142    fn try_from(input: &'a str) -> Result<Self, Self::Error> {
143        Self::parse(input)
144    }
145}
146
147impl<'a> EncodeType<'a> {
148    /// Parse a string into a list of component types.
149    pub fn parse(input: &'a str) -> Result<Self, Error> {
150        let mut types = vec![];
151        let mut remaining = input;
152
153        while let Ok(t) = ComponentType::parse(remaining) {
154            remaining = &remaining[t.span.len()..];
155            types.push(t);
156        }
157
158        Ok(Self { types })
159    }
160
161    /// Computes the canonical string representation of the type.
162    ///
163    /// Orders the `ComponentTypes` based on the EIP-712 rules, removes unsupported whitespaces, and
164    /// validates them.
165    pub fn canonicalize(&self) -> Result<String, Error> {
166        // Ensure no unintended whitespaces
167        let mut resolver = Resolver::default();
168        for component_type in &self.types {
169            resolver.ingest(component_type.to_owned());
170        }
171
172        // Resolve and validate non-dependent types
173        let mut non_dependent = resolver.non_dependent_types();
174
175        let first = non_dependent
176            .next()
177            .ok_or_else(|| Error::MissingType("primary component".to_string()))?;
178        if let Some(second) = non_dependent.next() {
179            let all_types = vec![first.type_name(), second.type_name()]
180                .into_iter()
181                .chain(non_dependent.map(|t| t.type_name()))
182                .collect::<Vec<_>>()
183                .join(", ");
184
185            return Err(Error::MissingType(format!("primary component: {all_types}")));
186        };
187
188        let primary = first.type_name();
189        _ = resolver.resolve(primary)?;
190
191        // Encode primary type
192        resolver.encode_type(primary)
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    const CANONICAL: &str = "Transaction(Person from,Person to,Asset tx)Asset(address token,uint256 amount)Person(address wallet,string name)";
201    const MISSING_COMPONENT: &str =
202        r#"Transaction(Person from, Person to, Asset tx) Person(address wallet, string name)"#;
203    const MISSING_PRIMARY: &str =
204        r#"Person(address wallet, string name) Asset(address token, uint256 amount)"#;
205    const CIRCULAR: &str = r#"
206        Transaction(Person from, Person to, Asset tx)
207        Asset(Person token, uint256 amount)
208        Person(Asset wallet, string name)
209        "#;
210    const MESSY: &str = r#"
211        Person(address wallet, string name)
212        Asset(address token, uint256 amount)
213        Transaction(Person from, Person to, Asset tx)
214        "#;
215
216    #[test]
217    fn empty_type() {
218        let empty_domain_type =
219            ComponentType { span: "EIP712Domain()", type_name: "EIP712Domain", props: vec![] };
220        assert_eq!(ComponentType::parse("EIP712Domain()"), Ok(empty_domain_type.clone()));
221
222        assert_eq!(
223            EncodeType::try_from("EIP712Domain()"),
224            Ok(EncodeType { types: vec![empty_domain_type] })
225        );
226    }
227
228    #[test]
229    fn test_component_type() {
230        assert_eq!(
231            ComponentType::parse("Transaction(Person from,Person to,Asset tx)"),
232            Ok(ComponentType {
233                span: "Transaction(Person from,Person to,Asset tx)",
234                type_name: "Transaction",
235                props: vec![
236                    "Person from".try_into().unwrap(),
237                    "Person to".try_into().unwrap(),
238                    "Asset tx".try_into().unwrap(),
239                ],
240            })
241        );
242    }
243
244    #[test]
245    fn test_encode_type() {
246        assert_eq!(
247            EncodeType::parse(CANONICAL),
248            Ok(EncodeType {
249                types: vec![
250                    "Transaction(Person from,Person to,Asset tx)".try_into().unwrap(),
251                    "Asset(address token,uint256 amount)".try_into().unwrap(),
252                    "Person(address wallet,string name)".try_into().unwrap(),
253                ]
254            })
255        );
256        assert_eq!(EncodeType::parse(CANONICAL).unwrap().canonicalize(), Ok(CANONICAL.to_string()));
257    }
258
259    #[test]
260    fn test_encode_type_messy() {
261        assert_eq!(EncodeType::parse(MESSY).unwrap().canonicalize(), Ok(CANONICAL.to_string()));
262    }
263
264    #[test]
265    fn test_fails_encode_type_missing_type() {
266        assert_eq!(
267            EncodeType::parse(MISSING_COMPONENT).unwrap().canonicalize(),
268            Err(Error::MissingType("Asset".into()))
269        );
270    }
271
272    #[test]
273    fn test_fails_encode_type_multi_primary() {
274        assert_eq!(
275            EncodeType::parse(MISSING_PRIMARY).unwrap().canonicalize(),
276            Err(Error::MissingType("primary component: Asset, Person".into()))
277        );
278    }
279
280    #[test]
281    fn test_fails_encode_type_circular() {
282        assert_eq!(
283            EncodeType::parse(CIRCULAR).unwrap().canonicalize(),
284            Err(Error::CircularDependency("Transaction".into()))
285        );
286    }
287}