ethers_types_rs/
eip712.rs

1//! This implementation adapted from [`ethers-core`](https://github.com/gakonst/ethers-rs/blob/64b7f1ef1ac71fefcacc44d8ff1ddfcb8e6b5417/ethers-core/src/types/transaction/eip712.rs)
2
3use std::{
4    collections::{BTreeMap, HashSet},
5    str::FromStr,
6};
7
8use ethabi::{encode, Bytes, ParamType, Token, Uint};
9use ethers_hash_rs::keccak256;
10use serde::{Deserialize, Deserializer, Serialize};
11
12use crate::Address;
13
14/// Custom types for `TypedData`
15pub type Types = BTreeMap<String, Vec<Eip712DomainType>>;
16
17/// `TypedData` value
18pub type Value = BTreeMap<String, serde_json::Value>;
19
20/// Supports parsing numbers as strings
21///
22/// See <https://github.com/gakonst/ethers-rs/issues/1507>
23pub fn deserialize_stringified_numeric_opt<'de, D>(
24    deserializer: D,
25) -> Result<Option<Uint>, D::Error>
26where
27    D: Deserializer<'de>,
28{
29    if let Some(num) = Option::<StringifiedNumeric>::deserialize(deserializer)? {
30        num.try_into().map(Some).map_err(serde::de::Error::custom)
31    } else {
32        Ok(None)
33    }
34}
35
36/// Helper type to parse numeric strings, `u64` and `U256`
37#[derive(Deserialize, Debug, Clone)]
38#[serde(untagged)]
39pub enum StringifiedNumeric {
40    String(String),
41    U256(Uint),
42    Num(u64),
43}
44
45impl TryFrom<StringifiedNumeric> for Uint {
46    type Error = String;
47
48    fn try_from(value: StringifiedNumeric) -> Result<Self, Self::Error> {
49        match value {
50            StringifiedNumeric::U256(n) => Ok(n),
51            StringifiedNumeric::Num(n) => Ok(Uint::from(n)),
52            StringifiedNumeric::String(s) => {
53                if let Ok(val) = s.parse::<u128>() {
54                    Ok(val.into())
55                } else if s.starts_with("0x") {
56                    Uint::from_str(&s).map_err(|err| err.to_string())
57                } else {
58                    Uint::from_dec_str(&s).map_err(|err| err.to_string())
59                }
60            }
61        }
62    }
63}
64
65/// Error typed used by Eip712 derive macro
66#[derive(Debug, thiserror::Error)]
67pub enum Eip712Error {
68    #[error("Failed to serialize serde JSON object")]
69    SerdeJsonError(#[from] serde_json::Error),
70    #[error("Failed to decode hex value")]
71    FromHexError(#[from] hex::FromHexError),
72    #[error("Failed to make struct hash from values")]
73    FailedToEncodeStruct,
74    #[error("Failed to convert slice into byte array")]
75    TryFromSliceError(#[from] std::array::TryFromSliceError),
76    #[error("Nested Eip712 struct not implemented. Failed to parse.")]
77    NestedEip712StructNotImplemented,
78    #[error("Error from Eip712 struct: {0:?}")]
79    Message(String),
80}
81
82/// The Eip712 trait provides helper methods for computing
83/// the typed data hash used in `eth_signTypedData`.
84///
85/// The ethers-rs `derive_eip712` crate provides a derive macro to
86/// implement the trait for a given struct. See documentation
87/// for `derive_eip712` for more information and example usage.
88///
89/// For those who wish to manually implement this trait, see:
90/// <https://eips.ethereum.org/EIPS/eip-712>
91///
92/// Any rust struct implementing Eip712 must also have a corresponding
93/// struct in the verifying ethereum contract that matches its signature.
94pub trait Eip712 {
95    /// User defined error type;
96    type Error: std::error::Error + Send + Sync + std::fmt::Debug;
97
98    /// Default implementation of the domain separator;
99    fn domain_separator(&self) -> Result<[u8; 32], Self::Error> {
100        Ok(self.domain()?.separator())
101    }
102
103    /// Returns the current domain. The domain depends on the contract and unique domain
104    /// for which the user is targeting. In the derive macro, these attributes
105    /// are passed in as arguments to the macro. When manually deriving, the user
106    /// will need to know the name of the domain, version of the contract, chain ID of
107    /// where the contract lives and the address of the verifying contract.
108    fn domain(&self) -> Result<EIP712Domain, Self::Error>;
109
110    /// This method is used for calculating the hash of the type signature of the
111    /// struct. The field types of the struct must map to primitive
112    /// ethereum types or custom types defined in the contract.
113    fn type_hash() -> Result<[u8; 32], Self::Error>;
114
115    /// Hash of the struct, according to EIP-712 definition of `hashStruct`
116    fn struct_hash(&self) -> Result<[u8; 32], Self::Error>;
117
118    /// When using the derive macro, this is the primary method used for computing the final
119    /// EIP-712 encoded payload. This method relies on the aforementioned methods for computing
120    /// the final encoded payload.
121    fn encode_eip712(&self) -> Result<[u8; 32], Self::Error> {
122        // encode the digest to be compatible with solidity abi.encodePacked()
123        // See: https://github.com/gakonst/ethers-rs/blob/master/examples/permit_hash.rs#L72
124
125        let domain_separator = self.domain_separator()?;
126        let struct_hash = self.struct_hash()?;
127
128        let digest_input = [&[0x19, 0x01], &domain_separator[..], &struct_hash[..]].concat();
129
130        Ok(keccak256(digest_input))
131    }
132}
133
134/// Eip712 Domain attributes used in determining the domain separator;
135/// Unused fields are left out of the struct type.
136///
137/// Protocol designers only need to include the fields that make sense for their signing domain.
138/// Unused fields are left out of the struct type.
139#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct EIP712Domain {
142    ///  The user readable name of signing domain, i.e. the name of the DApp or the protocol.
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub name: Option<String>,
145
146    /// The current major version of the signing domain. Signatures from different versions are not
147    /// compatible.
148    #[serde(default, skip_serializing_if = "Option::is_none")]
149    pub version: Option<String>,
150
151    /// The EIP-155 chain id. The user-agent should refuse signing if it does not match the
152    /// currently active chain.
153    #[serde(
154        default,
155        skip_serializing_if = "Option::is_none",
156        deserialize_with = "deserialize_stringified_numeric_opt"
157    )]
158    pub chain_id: Option<Uint>,
159
160    /// The address of the contract that will verify the signature.
161    #[serde(default, skip_serializing_if = "Option::is_none")]
162    pub verifying_contract: Option<Address>,
163
164    /// A disambiguating salt for the protocol. This can be used as a domain separator of last
165    /// resort.
166    #[serde(default, skip_serializing_if = "Option::is_none")]
167    pub salt: Option<[u8; 32]>,
168}
169
170impl EIP712Domain {
171    // Compute the domain separator;
172    // See: https://github.com/gakonst/ethers-rs/blob/master/examples/permit_hash.rs#L41
173    pub fn separator(&self) -> [u8; 32] {
174        // full name is `EIP712Domain(string name,string version,uint256 chainId,address
175        // verifyingContract,bytes32 salt)`
176        let mut ty = "EIP712Domain(".to_string();
177
178        let mut tokens = Vec::new();
179        let mut needs_comma = false;
180        if let Some(ref name) = self.name {
181            ty += "string name";
182            tokens.push(Token::Uint(Uint::from(keccak256(name))));
183            needs_comma = true;
184        }
185
186        if let Some(ref version) = self.version {
187            if needs_comma {
188                ty.push(',');
189            }
190            ty += "string version";
191            tokens.push(Token::Uint(Uint::from(keccak256(version))));
192            needs_comma = true;
193        }
194
195        if let Some(chain_id) = self.chain_id {
196            if needs_comma {
197                ty.push(',');
198            }
199            ty += "uint256 chainId";
200            tokens.push(Token::Uint(chain_id));
201            needs_comma = true;
202        }
203
204        if let Some(verifying_contract) = self.verifying_contract {
205            if needs_comma {
206                ty.push(',');
207            }
208            ty += "address verifyingContract";
209            tokens.push(Token::Address(verifying_contract));
210            needs_comma = true;
211        }
212
213        if let Some(salt) = self.salt {
214            if needs_comma {
215                ty.push(',');
216            }
217            ty += "bytes32 salt";
218            tokens.push(Token::Uint(Uint::from(salt)));
219        }
220
221        ty.push(')');
222
223        tokens.insert(0, Token::Uint(Uint::from(keccak256(ty))));
224
225        keccak256(encode(&tokens))
226    }
227}
228
229// Represents the [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed data object.
230///
231/// Typed data is a JSON object containing type information, domain separator parameters and the
232/// message object which has the following schema
233///
234/// ```js
235/// {
236//   type: 'object',
237//   properties: {
238//     types: {
239//       type: 'object',
240//       properties: {
241//         EIP712Domain: {type: 'array'},
242//       },
243//       additionalProperties: {
244//         type: 'array',
245//         items: {
246//           type: 'object',
247//           properties: {
248//             name: {type: 'string'},
249//             type: {type: 'string'}
250//           },
251//           required: ['name', 'type']
252//         }
253//       },
254//       required: ['EIP712Domain']
255//     },
256//     primaryType: {type: 'string'},
257//     domain: {type: 'object'},
258//     message: {type: 'object'}
259//   },
260//   required: ['types', 'primaryType', 'domain', 'message']
261// }
262/// ```
263///
264#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
265#[serde(deny_unknown_fields)]
266pub struct TypedData {
267    /// Signing domain metadata. The signing domain is the intended context for the signature (e.g.
268    /// the dapp, protocol, etc. that it's intended for). This data is used to construct the domain
269    /// seperator of the message.
270    pub domain: EIP712Domain,
271    /// The custom types used by this message.
272    pub types: Types,
273    #[serde(rename = "primaryType")]
274    /// The type of the message.
275    pub primary_type: String,
276    /// The message to be signed.
277    pub message: BTreeMap<String, serde_json::Value>,
278}
279
280/// According to the MetaMask implementation,
281/// the message parameter may be JSON stringified in versions later than V1
282/// See <https://github.com/MetaMask/metamask-extension/blob/0dfdd44ae7728ed02cbf32c564c75b74f37acf77/app/scripts/metamask-controller.js#L1736>
283/// In fact, ethers.js JSON stringifies the message at the time of writing.
284impl<'de> Deserialize<'de> for TypedData {
285    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
286    where
287        D: Deserializer<'de>,
288    {
289        #[derive(Deserialize)]
290        struct TypedDataHelper {
291            domain: EIP712Domain,
292            types: Types,
293            #[serde(rename = "primaryType")]
294            primary_type: String,
295            message: BTreeMap<String, serde_json::Value>,
296        }
297
298        #[derive(Deserialize)]
299        #[serde(untagged)]
300        enum Type {
301            Val(TypedDataHelper),
302            String(String),
303        }
304
305        match Type::deserialize(deserializer)? {
306            Type::Val(v) => {
307                let TypedDataHelper {
308                    domain,
309                    types,
310                    primary_type,
311                    message,
312                } = v;
313                Ok(TypedData {
314                    domain,
315                    types,
316                    primary_type,
317                    message,
318                })
319            }
320            Type::String(s) => {
321                let TypedDataHelper {
322                    domain,
323                    types,
324                    primary_type,
325                    message,
326                } = serde_json::from_str(&s).map_err(serde::de::Error::custom)?;
327                Ok(TypedData {
328                    domain,
329                    types,
330                    primary_type,
331                    message,
332                })
333            }
334        }
335    }
336}
337
338// === impl TypedData ===
339
340impl Eip712 for TypedData {
341    type Error = Eip712Error;
342
343    fn domain(&self) -> Result<EIP712Domain, Self::Error> {
344        Ok(self.domain.clone())
345    }
346
347    fn type_hash() -> Result<[u8; 32], Self::Error> {
348        Err(Eip712Error::Message("dynamic type".to_string()))
349    }
350
351    fn struct_hash(&self) -> Result<[u8; 32], Self::Error> {
352        let tokens = encode_data(
353            &self.primary_type,
354            &serde_json::Value::Object(serde_json::Map::from_iter(self.message.clone())),
355            &self.types,
356        )?;
357        Ok(keccak256(encode(&tokens)))
358    }
359
360    /// Hash a typed message according to EIP-712. The returned message starts with the EIP-712
361    /// prefix, which is "1901", followed by the hash of the domain separator, then the data (if
362    /// any). The result is hashed again and returned.
363    fn encode_eip712(&self) -> Result<[u8; 32], Self::Error> {
364        let domain_separator = self.domain.separator();
365        let mut digest_input = [&[0x19, 0x01], &domain_separator[..]].concat().to_vec();
366
367        if self.primary_type != "EIP712Domain" {
368            // compatibility with <https://github.com/MetaMask/eth-sig-util>
369            digest_input.extend(&self.struct_hash()?[..])
370        }
371        Ok(keccak256(digest_input))
372    }
373}
374
375/// Represents the name and type pair
376#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
377#[serde(deny_unknown_fields)]
378pub struct Eip712DomainType {
379    pub name: String,
380    #[serde(rename = "type")]
381    pub r#type: String,
382}
383
384/// Encodes an object by encoding and concatenating each of its members.
385///
386/// The encoding of a struct instance is `enc(value₁) ‖ enc(value₂) ‖ … ‖ enc(valueₙ)`, i.e. the
387/// concatenation of the encoded member values in the order that they appear in the type. Each
388/// encoded member value is exactly 32-byte long.
389///
390///   - `primaryType`: The root type.
391///   - `data`: The object to encode.
392///   - `types`: Type definitions for all types included in the message.
393///
394/// Returns an encoded representation of an object
395pub fn encode_data(
396    primary_type: &str,
397    data: &serde_json::Value,
398    types: &Types,
399) -> Result<Vec<Token>, Eip712Error> {
400    let hash = hash_type(primary_type, types)?;
401    let mut tokens = vec![Token::Uint(Uint::from(hash))];
402
403    if let Some(fields) = types.get(primary_type) {
404        for field in fields {
405            // handle recursive types
406            if let Some(value) = data.get(&field.name) {
407                let field = encode_field(types, &field.name, &field.r#type, value)?;
408                tokens.push(field);
409            } else if types.contains_key(&field.r#type) {
410                tokens.push(Token::Uint(Uint::zero()));
411            } else {
412                return Err(Eip712Error::Message(format!(
413                    "No data found for: `{}`",
414                    field.name
415                )));
416            }
417        }
418    }
419
420    Ok(tokens)
421}
422
423/// Hashes an object
424///
425///   - `primary_type`: The root type to encode.
426///   - `data`: The object to hash.
427///   - `types`: All type definitions.
428///
429/// Returns the hash of the `primary_type` object
430pub fn hash_struct(
431    primary_type: &str,
432    data: &serde_json::Value,
433    types: &Types,
434) -> Result<[u8; 32], Eip712Error> {
435    let tokens = encode_data(primary_type, data, types)?;
436    let encoded = encode(&tokens);
437    Ok(keccak256(encoded))
438}
439
440/// Returns the hashed encoded type of `primary_type`
441pub fn hash_type(primary_type: &str, types: &Types) -> Result<[u8; 32], Eip712Error> {
442    encode_type(primary_type, types).map(keccak256)
443}
444
445///  Encodes the type of an object by encoding a comma delimited list of its members.
446///
447///   - `primary_type`: The root type to encode.
448///   - `types`: All type definitions.
449///
450/// Returns the encoded representation of the field.
451pub fn encode_type(primary_type: &str, types: &Types) -> Result<String, Eip712Error> {
452    let mut names = HashSet::new();
453    find_type_dependencies(primary_type, types, &mut names);
454    // need to ensure primary_type is first in the list
455    names.remove(primary_type);
456    let mut deps: Vec<_> = names.into_iter().collect();
457    deps.sort_unstable();
458    deps.insert(0, primary_type);
459
460    let mut res = String::new();
461
462    for dep in deps.into_iter() {
463        let fields = types.get(dep).ok_or_else(|| {
464            Eip712Error::Message(format!("No type definition found for: `{dep}`"))
465        })?;
466
467        res += dep;
468        res.push('(');
469        res += &fields
470            .iter()
471            .map(|ty| format!("{} {}", ty.r#type, ty.name))
472            .collect::<Vec<_>>()
473            .join(",");
474
475        res.push(')');
476    }
477    Ok(res)
478}
479
480/// Returns all the custom types used in the `primary_type`
481fn find_type_dependencies<'a>(
482    primary_type: &'a str,
483    types: &'a Types,
484    found: &mut HashSet<&'a str>,
485) {
486    if found.contains(primary_type) {
487        return;
488    }
489    if let Some(fields) = types.get(primary_type) {
490        found.insert(primary_type);
491        for field in fields {
492            // need to strip the array tail
493            let ty = field.r#type.split('[').next().unwrap();
494            find_type_dependencies(ty, types, found)
495        }
496    }
497}
498
499/// Encode a single field.
500///
501///   - `types`: All type definitions.
502///   - `field`: The name and type of the field being encoded.
503///   - `value`: The value to encode.
504///
505/// Returns the encoded representation of the field.
506pub fn encode_field(
507    types: &Types,
508    _field_name: &str,
509    field_type: &str,
510    value: &serde_json::Value,
511) -> Result<Token, Eip712Error> {
512    let token = {
513        // check if field is custom data type
514        if types.contains_key(field_type) {
515            let tokens = encode_data(field_type, value, types)?;
516            let encoded = encode(&tokens);
517            encode_eip712_type(Token::Bytes(encoded.to_vec()))
518        } else {
519            match field_type {
520                s if s.contains('[') => {
521                    let (stripped_type, _) = s.rsplit_once('[').unwrap();
522                    // ensure value is an array
523                    let values = value.as_array().ok_or_else(|| {
524                        Eip712Error::Message(format!(
525                            "Expected array for type `{s}`, but got `{value}`",
526                        ))
527                    })?;
528                    let tokens = values
529                        .iter()
530                        .map(|value| encode_field(types, _field_name, stripped_type, value))
531                        .collect::<Result<Vec<_>, _>>()?;
532
533                    let encoded = encode(&tokens);
534                    encode_eip712_type(Token::Bytes(encoded))
535                }
536                s => {
537                    // parse as param type
538                    let param = parse_field_type(s)?;
539
540                    match param {
541                        ParamType::Address => {
542                            Token::Address(serde_json::from_value(value.clone())?)
543                        }
544                        ParamType::Bytes => {
545                            let data: Bytes = serde_json::from_value(value.clone())?;
546                            encode_eip712_type(Token::Bytes(data.to_vec()))
547                        }
548                        ParamType::Int(_) => Token::Uint(serde_json::from_value(value.clone())?),
549                        ParamType::Uint(_) => {
550                            // uints are commonly stringified due to how ethers-js encodes
551                            let val: StringifiedNumeric = serde_json::from_value(value.clone())?;
552                            let val = val.try_into().map_err(|err| {
553                                Eip712Error::Message(format!("Failed to parse uint {err}"))
554                            })?;
555
556                            Token::Uint(val)
557                        }
558                        ParamType::Bool => {
559                            encode_eip712_type(Token::Bool(serde_json::from_value(value.clone())?))
560                        }
561                        ParamType::String => {
562                            let s: String = serde_json::from_value(value.clone())?;
563                            encode_eip712_type(Token::String(s))
564                        }
565                        ParamType::FixedArray(_, _) | ParamType::Array(_) => {
566                            unreachable!("is handled in separate arm")
567                        }
568                        ParamType::FixedBytes(_) => {
569                            let data: Bytes = serde_json::from_value(value.clone())?;
570                            encode_eip712_type(Token::FixedBytes(data.to_vec()))
571                        }
572                        ParamType::Tuple(_) => {
573                            return Err(Eip712Error::Message(format!("Unexpected tuple type {s}",)))
574                        }
575                    }
576                }
577            }
578        }
579    };
580
581    Ok(token)
582}
583
584fn parse_field_type(source: &str) -> Result<ParamType, Eip712Error> {
585    match source {
586        "bytes1" => Ok(ParamType::FixedBytes(1)),
587        "bytes32" => Ok(ParamType::Uint(256)),
588        "uint256" => Ok(ParamType::Uint(256)),
589        "int8" => Ok(ParamType::Int(8)),
590        "int256" => Ok(ParamType::Int(256)),
591        "bool" => Ok(ParamType::Bool),
592        "address" => Ok(ParamType::Address),
593
594        "bytes" => Ok(ParamType::Bytes),
595        "string" => Ok(ParamType::String),
596        _ => Err(Eip712Error::Message(format!("Unsupport types: {}", source))),
597    }
598}
599
600/// Parse token into Eip712 compliant ABI encoding
601pub fn encode_eip712_type(token: Token) -> Token {
602    match token {
603        Token::Bytes(t) => Token::Uint(Uint::from(keccak256(&t))),
604        Token::FixedBytes(t) => Token::Uint(Uint::from(&t[..])),
605        Token::String(t) => Token::Uint(Uint::from(keccak256(t.as_bytes()))),
606        Token::Bool(t) => {
607            // Boolean false and true are encoded as uint256 values 0 and 1 respectively
608            Token::Uint(Uint::from(t as i32))
609        }
610        Token::Int(t) => {
611            // Integer values are sign-extended to 256-bit and encoded in big endian order.
612            Token::Uint(t)
613        }
614        Token::Array(tokens) => Token::Uint(Uint::from(keccak256(&encode(
615            &tokens
616                .into_iter()
617                .map(encode_eip712_type)
618                .collect::<Vec<Token>>(),
619        )))),
620        Token::FixedArray(tokens) => Token::Uint(Uint::from(keccak256(&encode(
621            &tokens
622                .into_iter()
623                .map(encode_eip712_type)
624                .collect::<Vec<Token>>(),
625        )))),
626        Token::Tuple(tuple) => {
627            let tokens = tuple
628                .into_iter()
629                .map(encode_eip712_type)
630                .collect::<Vec<Token>>();
631            let encoded = encode(&tokens);
632            Token::Uint(Uint::from(keccak256(&encoded)))
633        }
634        _ => {
635            // Return the ABI encoded token;
636            token
637        }
638    }
639}
640
641// Adapted tests from <https://github.com/MetaMask/eth-sig-util/blob/main/src/sign-typed-data.test.ts>
642#[cfg(test)]
643mod tests {
644    use serde_json::json;
645
646    use super::*;
647
648    #[test]
649    fn test_stringified_numeric() {
650        let json_data = json!([1, "0x1"]);
651
652        let _array: Vec<StringifiedNumeric> =
653            serde_json::from_value(json_data).expect("Parse stringified numeric");
654    }
655
656    #[test]
657    fn test_full_domain() {
658        let json = serde_json::json!({
659          "types": {
660            "EIP712Domain": [
661              {
662                "name": "name",
663                "type": "string"
664              },
665              {
666                "name": "version",
667                "type": "string"
668              },
669              {
670                "name": "chainId",
671                "type": "uint256"
672              },
673              {
674                "name": "verifyingContract",
675                "type": "address"
676              },
677              {
678                "name": "salt",
679                "type": "bytes32"
680              }
681            ]
682          },
683          "primaryType": "EIP712Domain",
684          "domain": {
685            "name": "example.metamask.io",
686            "version": "1",
687            "chainId": 1,
688            "verifyingContract": "0x0000000000000000000000000000000000000000"
689          },
690          "message": {}
691        });
692
693        let typed_data: TypedData = serde_json::from_value(json).unwrap();
694
695        let hash = typed_data.encode_eip712().unwrap();
696        assert_eq!(
697            "122d1c8ef94b76dad44dcb03fa772361e20855c63311a15d5afe02d1b38f6077",
698            hex::encode(&hash[..])
699        );
700    }
701
702    #[test]
703    fn test_minimal_message() {
704        let json = serde_json::json!( {"types":{"EIP712Domain":[]},"primaryType":"EIP712Domain","domain":{},"message":{}});
705
706        let typed_data: TypedData = serde_json::from_value(json).unwrap();
707
708        let hash = typed_data.encode_eip712().unwrap();
709        assert_eq!(
710            "8d4a3f4082945b7879e2b55f181c31a77c8c0a464b70669458abbaaf99de4c38",
711            hex::encode(&hash[..])
712        );
713    }
714
715    #[test]
716    fn test_encode_custom_array_type() {
717        let json = serde_json::json!({"domain":{},"types":{"EIP712Domain":[],"Person":[{"name":"name","type":"string"},{"name":"wallet","type":"address[]"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person[]"},{"name":"contents","type":"string"}]},"primaryType":"Mail","message":{"from":{"name":"Cow","wallet":["0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826","0xDD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"]},"to":[{"name":"Bob","wallet":["0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"]}],"contents":"Hello, Bob!"}});
718
719        let typed_data: TypedData = serde_json::from_value(json).unwrap();
720
721        let hash = typed_data.encode_eip712().unwrap();
722        assert_eq!(
723            "80a3aeb51161cfc47884ddf8eac0d2343d6ae640efe78b6a69be65e3045c1321",
724            hex::encode(&hash[..])
725        );
726    }
727
728    #[test]
729    fn test_hash_typed_message_with_data() {
730        let json = serde_json::json!( {
731          "types": {
732            "EIP712Domain": [
733              {
734                "name": "name",
735                "type": "string"
736              },
737              {
738                "name": "version",
739                "type": "string"
740              },
741              {
742                "name": "chainId",
743                "type": "uint256"
744              },
745              {
746                "name": "verifyingContract",
747                "type": "address"
748              }
749            ],
750            "Message": [
751              {
752                "name": "data",
753                "type": "string"
754              }
755            ]
756          },
757          "primaryType": "Message",
758          "domain": {
759            "name": "example.metamask.io",
760            "version": "1",
761            "chainId": "1",
762            "verifyingContract": "0x0000000000000000000000000000000000000000"
763          },
764          "message": {
765            "data": "Hello!"
766          }
767        });
768
769        let typed_data: TypedData = serde_json::from_value(json).unwrap();
770
771        let hash = typed_data.encode_eip712().unwrap();
772        assert_eq!(
773            "232cd3ec058eb935a709f093e3536ce26cc9e8e193584b0881992525f6236eef",
774            hex::encode(&hash[..])
775        );
776    }
777
778    #[test]
779    fn test_hash_custom_data_type() {
780        let json = serde_json::json!(  {"domain":{},"types":{"EIP712Domain":[],"Person":[{"name":"name","type":"string"},{"name":"wallet","type":"address"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person"},{"name":"contents","type":"string"}]},"primaryType":"Mail","message":{"from":{"name":"Cow","wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"},"to":{"name":"Bob","wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"},"contents":"Hello, Bob!"}});
781
782        let typed_data: TypedData = serde_json::from_value(json).unwrap();
783
784        let hash = typed_data.encode_eip712().unwrap();
785        assert_eq!(
786            "25c3d40a39e639a4d0b6e4d2ace5e1281e039c88494d97d8d08f99a6ea75d775",
787            hex::encode(&hash[..])
788        );
789    }
790
791    #[test]
792    fn test_hash_recursive_types() {
793        let json = serde_json::json!( {
794          "domain": {},
795          "types": {
796            "EIP712Domain": [],
797            "Person": [
798              {
799                "name": "name",
800                "type": "string"
801              },
802              {
803                "name": "wallet",
804                "type": "address"
805              }
806            ],
807            "Mail": [
808              {
809                "name": "from",
810                "type": "Person"
811              },
812              {
813                "name": "to",
814                "type": "Person"
815              },
816              {
817                "name": "contents",
818                "type": "string"
819              },
820              {
821                "name": "replyTo",
822                "type": "Mail"
823              }
824            ]
825          },
826          "primaryType": "Mail",
827          "message": {
828            "from": {
829              "name": "Cow",
830              "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
831            },
832            "to": {
833              "name": "Bob",
834              "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
835            },
836            "contents": "Hello, Bob!",
837            "replyTo": {
838              "to": {
839                "name": "Cow",
840                "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
841              },
842              "from": {
843                "name": "Bob",
844                "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
845              },
846              "contents": "Hello!"
847            }
848          }
849        });
850
851        let typed_data: TypedData = serde_json::from_value(json).unwrap();
852
853        let hash = typed_data.encode_eip712().unwrap();
854        assert_eq!(
855            "0808c17abba0aef844b0470b77df9c994bc0fa3e244dc718afd66a3901c4bd7b",
856            hex::encode(&hash[..])
857        );
858    }
859
860    #[test]
861    fn test_hash_nested_struct_array() {
862        let json = serde_json::json!({
863          "types": {
864            "EIP712Domain": [
865              {
866                "name": "name",
867                "type": "string"
868              },
869              {
870                "name": "version",
871                "type": "string"
872              },
873              {
874                "name": "chainId",
875                "type": "uint256"
876              },
877              {
878                "name": "verifyingContract",
879                "type": "address"
880              }
881            ],
882            "OrderComponents": [
883              {
884                "name": "offerer",
885                "type": "address"
886              },
887              {
888                "name": "zone",
889                "type": "address"
890              },
891              {
892                "name": "offer",
893                "type": "OfferItem[]"
894              },
895              {
896                "name": "startTime",
897                "type": "uint256"
898              },
899              {
900                "name": "endTime",
901                "type": "uint256"
902              },
903              {
904                "name": "zoneHash",
905                "type": "bytes32"
906              },
907              {
908                "name": "salt",
909                "type": "uint256"
910              },
911              {
912                "name": "conduitKey",
913                "type": "bytes32"
914              },
915              {
916                "name": "counter",
917                "type": "uint256"
918              }
919            ],
920            "OfferItem": [
921              {
922                "name": "token",
923                "type": "address"
924              }
925            ],
926            "ConsiderationItem": [
927              {
928                "name": "token",
929                "type": "address"
930              },
931              {
932                "name": "identifierOrCriteria",
933                "type": "uint256"
934              },
935              {
936                "name": "startAmount",
937                "type": "uint256"
938              },
939              {
940                "name": "endAmount",
941                "type": "uint256"
942              },
943              {
944                "name": "recipient",
945                "type": "address"
946              }
947            ]
948          },
949          "primaryType": "OrderComponents",
950          "domain": {
951            "name": "Seaport",
952            "version": "1.1",
953            "chainId": "1",
954            "verifyingContract": "0x00000000006c3852cbEf3e08E8dF289169EdE581"
955          },
956          "message": {
957            "offerer": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
958            "offer": [
959              {
960                "token": "0xA604060890923Ff400e8c6f5290461A83AEDACec"
961              }
962            ],
963            "startTime": "1658645591",
964            "endTime": "1659250386",
965            "zone": "0x004C00500000aD104D7DBd00e3ae0A5C00560C00",
966            "zoneHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
967            "salt": "16178208897136618",
968            "conduitKey": "0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000",
969            "totalOriginalConsiderationItems": "2",
970            "counter": "0"
971          }
972        }
973                );
974
975        let typed_data: TypedData = serde_json::from_value(json).unwrap();
976
977        let hash = typed_data.encode_eip712().unwrap();
978        assert_eq!(
979            "0b8aa9f3712df0034bc29fe5b24dd88cfdba02c7f499856ab24632e2969709a8",
980            hex::encode(&hash[..])
981        );
982    }
983}