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
11pub type Types = BTreeMap<String, Vec<Eip712DomainType>>;
13
14pub 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
23pub 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#[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
49pub trait Eip712 {
61 type Error: std::error::Error + Send + Sync + std::fmt::Debug;
63
64 fn domain_separator(&self) -> Result<[u8; 32], Self::Error> {
66 Ok(self.domain()?.separator())
67 }
68
69 fn domain(&self) -> Result<EIP712Domain, Self::Error>;
75
76 fn type_hash() -> Result<[u8; 32], Self::Error>;
80
81 fn struct_hash(&self) -> Result<[u8; 32], Self::Error>;
83
84 fn encode_eip712(&self) -> Result<[u8; 32], Self::Error> {
88 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#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
106#[serde(rename_all = "camelCase")]
107pub struct EIP712Domain {
108 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub name: Option<String>,
111
112 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub version: Option<String>,
116
117 #[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 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub verifying_contract: Option<Address>,
129
130 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub salt: Option<[u8; 32]>,
134}
135
136impl EIP712Domain {
137 pub fn separator(&self) -> [u8; 32] {
140 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#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
271#[serde(deny_unknown_fields)]
272pub struct TypedData {
273 pub domain: EIP712Domain,
277 pub types: Types,
279 #[serde(rename = "primaryType")]
280 pub primary_type: String,
282 pub message: BTreeMap<String, serde_json::Value>,
284}
285
286impl<'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
325impl 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 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 digest_input.extend(&self.struct_hash()?[..])
357 }
358 Ok(keccak256(digest_input))
359 }
360}
361
362#[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
371pub 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 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
407pub 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
424pub fn hash_type(primary_type: &str, types: &Types) -> Result<[u8; 32], Eip712Error> {
426 encode_type(primary_type, types).map(keccak256)
427}
428
429pub 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 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
464fn 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 let ty = field.r#type.split('[').next().unwrap();
478 find_type_dependencies(ty, types, found)
479 }
480 }
481}
482
483pub 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 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 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 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 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
570pub 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
580pub 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 Token::Uint(U256::from(t as i32))
589 }
590 Token::Int(t) => {
591 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 token
608 }
609 }
610}
611
612#[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}