1use 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
14pub type Types = BTreeMap<String, Vec<Eip712DomainType>>;
16
17pub type Value = BTreeMap<String, serde_json::Value>;
19
20pub 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#[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#[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
82pub trait Eip712 {
95 type Error: std::error::Error + Send + Sync + std::fmt::Debug;
97
98 fn domain_separator(&self) -> Result<[u8; 32], Self::Error> {
100 Ok(self.domain()?.separator())
101 }
102
103 fn domain(&self) -> Result<EIP712Domain, Self::Error>;
109
110 fn type_hash() -> Result<[u8; 32], Self::Error>;
114
115 fn struct_hash(&self) -> Result<[u8; 32], Self::Error>;
117
118 fn encode_eip712(&self) -> Result<[u8; 32], Self::Error> {
122 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#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct EIP712Domain {
142 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub name: Option<String>,
145
146 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub version: Option<String>,
150
151 #[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 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub verifying_contract: Option<Address>,
163
164 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub salt: Option<[u8; 32]>,
168}
169
170impl EIP712Domain {
171 pub fn separator(&self) -> [u8; 32] {
174 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#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
265#[serde(deny_unknown_fields)]
266pub struct TypedData {
267 pub domain: EIP712Domain,
271 pub types: Types,
273 #[serde(rename = "primaryType")]
274 pub primary_type: String,
276 pub message: BTreeMap<String, serde_json::Value>,
278}
279
280impl<'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
338impl 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 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 digest_input.extend(&self.struct_hash()?[..])
370 }
371 Ok(keccak256(digest_input))
372 }
373}
374
375#[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
384pub 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 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
423pub 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
440pub fn hash_type(primary_type: &str, types: &Types) -> Result<[u8; 32], Eip712Error> {
442 encode_type(primary_type, types).map(keccak256)
443}
444
445pub 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 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
480fn 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 let ty = field.r#type.split('[').next().unwrap();
494 find_type_dependencies(ty, types, found)
495 }
496 }
497}
498
499pub 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 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 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 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 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
600pub 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 Token::Uint(Uint::from(t as i32))
609 }
610 Token::Int(t) => {
611 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 token
637 }
638 }
639}
640
641#[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}