ssi_eip712/
ty.rs

1use std::{collections::BTreeMap, fmt, num::ParseIntError, str::FromStr};
2
3use iref::Uri;
4use serde::{Deserialize, Serialize};
5
6use crate::Value;
7
8pub type StructName = String;
9
10/// Errors that can occur while fetching remote EIP712 type definitions.
11#[derive(Debug, thiserror::Error)]
12pub enum TypesFetchError {
13    /// Error for applications that do not support remote types.
14    ///
15    /// This is the error always returned by the `()` implementation of
16    /// `TypesProvider`.
17    #[error("remote EIP712 types are not supported")]
18    Unsupported,
19}
20
21/// Type providing remote EIP712 type definitions from an URI.
22///
23/// A default implementation is provided for the `()` type that always return
24/// `TypesFetchError::Unsupported`.
25pub trait TypesLoader {
26    /// Fetches the type definitions located behind the given `uri`.
27    ///
28    /// This is an asynchronous function returning a `Self::Fetch` future that
29    /// resolves into ether the EIP712 [`Types`] or an error
30    /// of type `TypesFetchError`.
31    #[allow(async_fn_in_trait)]
32    async fn fetch_types(&self, uri: &Uri) -> Result<Types, TypesFetchError>;
33}
34
35/// Simple EIP712 loader implementation that always return
36/// `TypesFetchError::Unsupported`.
37impl TypesLoader for () {
38    async fn fetch_types(&self, _uri: &Uri) -> Result<Types, TypesFetchError> {
39        Err(TypesFetchError::Unsupported)
40    }
41}
42
43impl<T: TypesLoader> TypesLoader for &T {
44    async fn fetch_types(&self, uri: &Uri) -> Result<Types, TypesFetchError> {
45        T::fetch_types(*self, uri).await
46    }
47}
48
49pub trait Eip712TypesLoaderProvider {
50    type Loader: TypesLoader;
51
52    fn eip712_types(&self) -> &Self::Loader;
53}
54
55impl<E: Eip712TypesLoaderProvider> Eip712TypesLoaderProvider for &E {
56    type Loader = E::Loader;
57
58    fn eip712_types(&self) -> &Self::Loader {
59        E::eip712_types(*self)
60    }
61}
62
63/// EIP-712 types
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
65#[serde(untagged)]
66#[serde(try_from = "String", into = "String")]
67pub enum TypeRef {
68    BytesN(usize),
69    UintN(usize),
70    IntN(usize),
71    Bool,
72    Address,
73    Bytes,
74    String,
75    Array(Box<TypeRef>),
76    ArrayN(Box<TypeRef>, usize),
77    Struct(StructName),
78}
79
80impl TypeRef {
81    /// Return name of struct if this type is a reference to a struct or array of structs
82    pub fn as_struct_name(&self) -> Option<&StructName> {
83        match self {
84            Self::Struct(name) => Some(name),
85            Self::Array(type_box) | Self::ArrayN(type_box, _) => type_box.as_struct_name(),
86            _ => None,
87        }
88    }
89}
90
91#[derive(Debug, thiserror::Error)]
92pub enum TypeParseError {
93    #[error("Unmatched bracket")]
94    UnmatchedBracket,
95    #[error("Unable to parse data type size: {0}")]
96    SizeParse(#[from] ParseIntError),
97}
98
99impl FromStr for TypeRef {
100    type Err = TypeParseError;
101
102    fn from_str(string: &str) -> Result<Self, Self::Err> {
103        match string {
104            "bytes" => return Ok(TypeRef::Bytes),
105            "string" => return Ok(TypeRef::String),
106            "address" => return Ok(TypeRef::Address),
107            "bool" => return Ok(TypeRef::Bool),
108            _ => {}
109        }
110
111        if string.ends_with(']') {
112            let mut parts = string.rsplitn(2, '[');
113            let amount_str = parts.next().unwrap().split(']').next().unwrap();
114            let inner = parts.next().ok_or(TypeParseError::UnmatchedBracket)?;
115            let base = inner.parse()?;
116            if amount_str.is_empty() {
117                return Ok(TypeRef::Array(Box::new(base)));
118            } else {
119                return Ok(TypeRef::ArrayN(
120                    Box::new(base),
121                    usize::from_str(amount_str)?,
122                ));
123            }
124        } else if let Some(suffix) = string.strip_prefix("uint") {
125            return Ok(TypeRef::UintN(usize::from_str(suffix)?));
126        } else if let Some(suffix) = string.strip_prefix("int") {
127            return Ok(TypeRef::IntN(usize::from_str(suffix)?));
128        } else if let Some(suffix) = string.strip_prefix("bytes") {
129            return Ok(TypeRef::BytesN(usize::from_str(suffix)?));
130        }
131
132        Ok(TypeRef::Struct(string.to_owned()))
133    }
134}
135
136impl TryFrom<String> for TypeRef {
137    type Error = TypeParseError;
138
139    fn try_from(value: String) -> Result<Self, Self::Error> {
140        value.parse()
141    }
142}
143
144impl fmt::Display for TypeRef {
145    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
146        match self {
147            TypeRef::Bytes => write!(f, "bytes"),
148            TypeRef::String => write!(f, "string"),
149            TypeRef::BytesN(n) => write!(f, "bytes{}", n),
150            TypeRef::UintN(n) => write!(f, "uint{}", n),
151            TypeRef::IntN(n) => write!(f, "int{}", n),
152            TypeRef::Bool => write!(f, "bool"),
153            TypeRef::Address => write!(f, "address"),
154            TypeRef::Array(type_) => {
155                write!(f, "{}[]", *type_)
156            }
157            TypeRef::ArrayN(type_, n) => {
158                write!(f, "{}[{}]", *type_, n)
159            }
160            TypeRef::Struct(name) => {
161                write!(f, "{}", name)
162            }
163        }
164    }
165}
166
167impl From<TypeRef> for String {
168    fn from(type_: TypeRef) -> String {
169        match type_ {
170            TypeRef::Struct(name) => name,
171            _ => {
172                format!("{}", &type_)
173            }
174        }
175    }
176}
177
178/// Structured typed data as described in
179/// [Definition of typed structured data 𝕊](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-typed-structured-data-%F0%9D%95%8A)
180#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
181pub struct TypeDefinition(Vec<MemberVariable>);
182
183impl TypeDefinition {
184    pub fn new(member_variables: Vec<MemberVariable>) -> Self {
185        Self(member_variables)
186    }
187
188    pub fn member_variables(&self) -> &[MemberVariable] {
189        &self.0
190    }
191
192    pub fn push(&mut self, m: MemberVariable) {
193        self.0.push(m)
194    }
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
198pub struct MemberVariable {
199    pub name: String,
200
201    #[serde(rename = "type")]
202    pub type_: TypeRef,
203}
204
205impl MemberVariable {
206    pub fn new(name: String, type_: TypeRef) -> Self {
207        Self { name, type_ }
208    }
209}
210
211#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
212pub struct Types {
213    #[serde(rename = "EIP712Domain")]
214    pub eip712_domain: TypeDefinition,
215
216    #[serde(flatten)]
217    pub types: BTreeMap<StructName, TypeDefinition>,
218}
219
220impl Types {
221    pub fn get(&self, struct_name: &str) -> Option<&TypeDefinition> {
222        if struct_name == "EIP712Domain" {
223            Some(&self.eip712_domain)
224        } else {
225            self.types.get(struct_name)
226        }
227    }
228
229    /// Generate EIP-712 types from a value.
230    ///
231    /// See: <https://w3c-ccg.github.io/ethereum-eip712-signature-2021-spec/#types-generation>
232    pub fn generate(
233        doc: &Value,
234        primary_type: StructName,
235        domain_type: TypeDefinition,
236    ) -> Result<Self, TypesGenerationError> {
237        Ok(Self {
238            eip712_domain: domain_type,
239            types: Self::generate_inner(doc, primary_type)?,
240        })
241    }
242
243    /// Generate EIP-712 types from a value, without the toplevel `EIP712Domain` type.
244    ///
245    /// See: <https://w3c-ccg.github.io/ethereum-eip712-signature-2021-spec/#types-generation>
246    fn generate_inner(
247        doc: &Value,
248        primary_type: StructName,
249    ) -> Result<BTreeMap<StructName, TypeDefinition>, TypesGenerationError> {
250        // 1
251        let mut output = BTreeMap::default();
252        // 2
253        // TypedDataField == MemberVariable
254        let mut types = TypeDefinition::default();
255        // 4
256        // Done already.
257        // 3
258        // Using JCS here probably has no effect:
259        // https://github.com/davidpdrsn/assert-json-diff
260        let doc_jcs = serde_jcs::to_string(doc).map_err(TypesGenerationError::JCS)?;
261        let doc: Value = serde_json::from_str(&doc_jcs).map_err(TypesGenerationError::JCS)?;
262        // 5
263        let object = doc
264            .as_struct()
265            .ok_or(TypesGenerationError::ExpectedObject)?;
266        let mut props: Vec<(&String, &Value)> = object.iter().collect();
267        // Iterate through object properties in the order JCS would sort them.
268        // https://datatracker.ietf.org/doc/html/rfc8785#section-3.2.3
269        props.sort_by_cached_key(|(name, _value)| name.encode_utf16().collect::<Vec<u16>>());
270        for (property_name, value) in props {
271            match value {
272                // 6
273                Value::Bool(_) => {
274                    // 6.1
275                    types.push(MemberVariable {
276                        type_: TypeRef::Bool,
277                        name: String::from(property_name),
278                    });
279                }
280                Value::Integer(_) => {
281                    // 6.2
282                    types.push(MemberVariable {
283                        type_: TypeRef::UintN(256),
284                        name: String::from(property_name),
285                    });
286                }
287                Value::String(_) => {
288                    // 6.3
289                    types.push(MemberVariable {
290                        type_: TypeRef::String,
291                        name: String::from(property_name),
292                    });
293                }
294                // 7
295                Value::Array(array) => {
296                    // Ensure values have same primitive type.
297                    let mut values = array.iter();
298                    let first_value = values
299                        .next()
300                        .ok_or_else(|| TypesGenerationError::EmptyArray(property_name.clone()))?;
301                    match first_value {
302                        Value::Bool(_) => {
303                            // 7.1
304                            for value in values {
305                                if !matches!(value, Value::Bool(_)) {
306                                    return Err(TypesGenerationError::ArrayInconsistency(
307                                        "boolean",
308                                        property_name.clone(),
309                                    ));
310                                }
311                            }
312                            types.push(MemberVariable {
313                                type_: TypeRef::Array(Box::new(TypeRef::Bool)),
314                                name: String::from(property_name),
315                            });
316                        }
317                        Value::Integer(_) => {
318                            // 7.2
319                            for value in values {
320                                if !matches!(value, Value::Integer(_)) {
321                                    return Err(TypesGenerationError::ArrayInconsistency(
322                                        "number",
323                                        property_name.clone(),
324                                    ));
325                                }
326                            }
327                            types.push(MemberVariable {
328                                type_: TypeRef::Array(Box::new(TypeRef::UintN(256))),
329                                name: String::from(property_name),
330                            });
331                        }
332                        Value::String(_) => {
333                            // 7.3
334                            for value in values {
335                                if !matches!(value, Value::String(_)) {
336                                    return Err(TypesGenerationError::ArrayInconsistency(
337                                        "string",
338                                        property_name.clone(),
339                                    ));
340                                }
341                            }
342                            types.push(MemberVariable {
343                                type_: TypeRef::Array(Box::new(TypeRef::String)),
344                                name: String::from(property_name),
345                            });
346                        }
347                        _ => {
348                            return Err(TypesGenerationError::ComplexArrayValue(
349                                property_name.clone(),
350                            ));
351                        }
352                    }
353                }
354                Value::Struct(object) => {
355                    // 8
356                    let mut recursive_output =
357                        Self::generate_inner(&Value::Struct(object.clone()), primary_type.clone())?;
358                    // 8.1
359                    let recursive_types =
360                        recursive_output.remove(&primary_type).ok_or_else(|| {
361                            TypesGenerationError::MissingPrimaryTypeInRecursiveOutput(
362                                primary_type.clone(),
363                            )
364                        })?;
365                    // 8.2
366                    let property_type = property_to_struct_name(property_name);
367                    types.push(MemberVariable {
368                        name: String::from(property_name),
369                        type_: TypeRef::Struct(property_type.clone()),
370                    });
371                    // 8.3
372                    output.insert(property_type, recursive_types);
373                    // 8.4
374                    for (prop, type_) in recursive_output.into_iter() {
375                        output.insert(prop, type_);
376                    }
377                }
378                _ => {
379                    return Err(TypesGenerationError::ComplexValue(property_name.clone()));
380                }
381            }
382        }
383        // 9
384        output.insert(primary_type, types);
385        Ok(output)
386    }
387}
388
389#[derive(Debug, thiserror::Error)]
390pub enum TypesGenerationError {
391    #[error("Expected object")]
392    ExpectedObject,
393    #[error("Found empty array under property: {0}")]
394    EmptyArray(String),
395    #[error("Array inconsistency: expected type {0} under property: {1}")]
396    ArrayInconsistency(&'static str, String),
397    #[error("Array value must be boolean, number or string. Property: {0}")]
398    ComplexArrayValue(String),
399    #[error("Value must be boolean, number, string, array or struct. Property: {0}")]
400    ComplexValue(String),
401    #[error("Missing primaryType in recursive output. primaryType: {0}")]
402    MissingPrimaryTypeInRecursiveOutput(String),
403    #[error("JCS: {0}")]
404    JCS(serde_json::Error),
405    #[error("Proof type already exists")]
406    ProofAlreadyExists,
407}
408
409fn property_to_struct_name(property_name: &str) -> StructName {
410    // CamelCase
411    let mut chars = property_name.chars();
412    let first_char = chars.next().unwrap_or_default();
413    first_char.to_uppercase().chain(chars).collect()
414}
415
416#[cfg(test)]
417lazy_static::lazy_static! {
418    // https://github.com/w3c-ccg/ethereum-eip712-signature-2021-spec/blob/28bd5edecde8395242aea8ba64e9be25f59585d0/index.html#L917-L966
419    // https://github.com/w3c-ccg/ethereum-eip712-signature-2021-spec/pull/26/files#r798853853
420    pub static ref EXAMPLE_TYPES: serde_json::Value = {
421        serde_json::json!({
422            "Data": [
423            {
424                "name": "job",
425                "type": "Job"
426            },
427            {
428                "name": "name",
429                "type": "Name"
430            }
431            ],
432            "Job": [
433            {
434                "name": "employer",
435                "type": "string"
436            },
437            {
438                "name": "jobTitle",
439                "type": "string"
440            }
441            ],
442            "Name": [
443            {
444                "name": "firstName",
445                "type": "string"
446            },
447            {
448                "name": "lastName",
449                "type": "string"
450            }
451            ],
452            "Document": [
453            {
454                "name": "@context",
455                "type": "string[]"
456            },
457            {
458                "name": "@type",
459                "type": "string"
460            },
461            {
462                "name": "data",
463                "type": "Data"
464            },
465            {
466                "name": "proof",
467                "type": "Proof"
468            },
469            {
470                "name": "telephone",
471                "type": "string"
472            }
473            ],
474            "Proof": [
475            {
476                "name": "created",
477                "type": "string"
478            },
479            {
480                "name": "proofPurpose",
481                "type": "string"
482            },
483            {
484                "name": "type",
485                "type": "string"
486            },
487            {
488                "name": "verificationMethod",
489                "type": "string"
490            }
491            ]
492        })
493    };
494}