ethers_core/types/transaction/
eip712.rs

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