1use ethereum_types::{Address, H256, U256};
19use lazy_static::lazy_static;
20use regex::Regex;
21use serde::{Deserialize, Serialize};
22use serde_json::Value;
23use std::collections::HashMap;
24use validator::{Validate, ValidationError, ValidationErrors};
25
26pub(crate) type MessageTypes = HashMap<String, Vec<FieldType>>;
27
28lazy_static! {
29 static ref TYPE_REGEX: Regex = Regex::new(r"^[a-zA-Z_$][a-zA-Z_$0-9]*(\[([1-9]\d*)*\])*$").unwrap();
31 static ref IDENT_REGEX: Regex = Regex::new(r"^[a-zA-Z_$][a-zA-Z_$0-9]*$").unwrap();
32}
33
34#[derive(Deserialize, Serialize, Validate, Debug, Clone)]
35#[serde(rename_all = "camelCase")]
36#[validate(schema(function = "validate_domain"))]
37pub(crate) struct EIP712Domain {
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub(crate) name: Option<String>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub(crate) version: Option<String>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub(crate) chain_id: Option<U256>,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub(crate) verifying_contract: Option<Address>,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub(crate) salt: Option<H256>,
48}
49
50fn validate_domain(domain: &EIP712Domain) -> Result<(), ValidationError> {
51 match (
52 domain.name.as_ref(),
53 domain.version.as_ref(),
54 domain.chain_id,
55 domain.verifying_contract,
56 domain.salt,
57 ) {
58 (None, None, None, None, None) => Err(ValidationError::new(
59 "EIP712Domain must include at least one field",
60 )),
61 _ => Ok(()),
62 }
63}
64
65#[derive(Deserialize, Debug, Clone)]
67#[serde(rename_all = "camelCase")]
68#[serde(deny_unknown_fields)]
69pub struct EIP712 {
70 pub(crate) types: MessageTypes,
71 pub(crate) primary_type: String,
72 pub(crate) message: Value,
73 pub(crate) domain: EIP712Domain,
74}
75
76impl Validate for EIP712 {
77 fn validate(&self) -> Result<(), ValidationErrors> {
78 self.domain.validate()?;
79 for field_types in self.types.values() {
80 for field_type in field_types {
81 field_type.validate()?;
82 }
83 }
84 Ok(())
85 }
86}
87
88#[derive(Serialize, Deserialize, Validate, Debug, Clone)]
89pub(crate) struct FieldType {
90 #[validate(regex(path = "*IDENT_REGEX"))]
91 pub name: String,
92 #[serde(rename = "type")]
93 #[validate(regex(path = "*TYPE_REGEX"))]
94 pub type_: String,
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use serde_json::from_str;
101
102 #[test]
103 fn test_regex() {
104 let test_cases = vec![
105 "unint bytes32",
106 "Seun\\[]",
107 "byte[]uint",
108 "byte[7[]uint][]",
109 "Person[0]",
110 ];
111 for case in test_cases {
112 assert_eq!(TYPE_REGEX.is_match(case), false)
113 }
114
115 let test_cases = vec![
116 "bytes32",
117 "Foo[]",
118 "bytes1",
119 "bytes32[][]",
120 "byte[9]",
121 "contents",
122 ];
123 for case in test_cases {
124 assert_eq!(TYPE_REGEX.is_match(case), true)
125 }
126 }
127
128 #[test]
129 fn test_deserialization() {
130 let string = r#"{
131 "primaryType": "Mail",
132 "domain": {
133 "name": "Ether Mail",
134 "version": "1",
135 "chainId": "0x1",
136 "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
137 },
138 "message": {
139 "from": {
140 "name": "Cow",
141 "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
142 },
143 "to": {
144 "name": "Bob",
145 "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
146 },
147 "contents": "Hello, Bob!"
148 },
149 "types": {
150 "EIP712Domain": [
151 { "name": "name", "type": "string" },
152 { "name": "version", "type": "string" },
153 { "name": "chainId", "type": "uint256" },
154 { "name": "verifyingContract", "type": "address" }
155 ],
156 "Person": [
157 { "name": "name", "type": "string" },
158 { "name": "wallet", "type": "address" }
159 ],
160 "Mail": [
161 { "name": "from", "type": "Person" },
162 { "name": "to", "type": "Person" },
163 { "name": "contents", "type": "string" }
164 ]
165 }
166 }"#;
167 let _ = from_str::<EIP712>(string).unwrap();
168 }
169
170 #[test]
171 fn test_failing_deserialization() {
172 let string = r#"{
173 "primaryType": "Mail",
174 "domain": {
175 "name": "Ether Mail",
176 "version": "1",
177 "chainId": "0x1",
178 "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
179 },
180 "message": {
181 "from": {
182 "name": "Cow",
183 "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
184 },
185 "to": {
186 "name": "Bob",
187 "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
188 },
189 "contents": "Hello, Bob!"
190 },
191 "types": {
192 "EIP712Domain": [
193 { "name": "name", "type": "string" },
194 { "name": "version", "type": "string" },
195 { "name": "chainId", "type": "7uint256[x] Seun" },
196 { "name": "verifyingContract", "type": "address" },
197 { "name": "salt", "type": "bytes32" }
198 ],
199 "Person": [
200 { "name": "name", "type": "string" },
201 { "name": "wallet amen", "type": "address" }
202 ],
203 "Mail": [
204 { "name": "from", "type": "Person" },
205 { "name": "to", "type": "Person" },
206 { "name": "contents", "type": "string" }
207 ]
208 }
209 }"#;
210 let data = from_str::<EIP712>(string).unwrap();
211 assert_eq!(data.validate().is_err(), true);
212 }
213
214 #[test]
215 fn test_valid_domain() {
216 let string = r#"{
217 "primaryType": "Test",
218 "domain": {
219 "name": "Ether Mail",
220 "version": "1",
221 "chainId": "0x1",
222 "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
223 "salt": "0x0000000000000000000000000000000000000000000000000000000000000001"
224 },
225 "message": {
226 "test": "It works!"
227 },
228 "types": {
229 "EIP712Domain": [
230 { "name": "name", "type": "string" },
231 { "name": "version", "type": "string" },
232 { "name": "chainId", "type": "uint256" },
233 { "name": "verifyingContract", "type": "address" },
234 { "name": "salt", "type": "bytes32" }
235 ],
236 "Test": [
237 { "name": "test", "type": "string" }
238 ]
239 }
240 }"#;
241 let data = from_str::<EIP712>(string).unwrap();
242 assert_eq!(data.validate().is_err(), false);
243 }
244
245 #[test]
246 fn domain_needs_at_least_one_field() {
247 let string = r#"{
248 "primaryType": "Test",
249 "domain": {},
250 "message": {
251 "test": "It works!"
252 },
253 "types": {
254 "EIP712Domain": [
255 { "name": "name", "type": "string" },
256 { "name": "version", "type": "string" },
257 { "name": "chainId", "type": "uint256" },
258 { "name": "verifyingContract", "type": "address" }
259 ],
260 "Test": [
261 { "name": "test", "type": "string" }
262 ]
263 }
264 }"#;
265 let data = from_str::<EIP712>(string).unwrap();
266 assert_eq!(data.validate().is_err(), true);
267 }
268
269 #[test]
270 fn test_deserialization_no_chainid() {
271 let string = r#"{
272 "primaryType": "Mail",
273 "domain": {
274 "name": "Ether Mail",
275 "version": "1"
276 },
277 "message": {
278 "from": {
279 "name": "Cow",
280 "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
281 },
282 "to": {
283 "name": "Bob",
284 "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
285 },
286 "contents": "Hello, Bob!"
287 },
288 "types": {
289 "EIP712Domain": [
290 { "name": "name", "type": "string" },
291 { "name": "version", "type": "string" }
292 ],
293 "Person": [
294 { "name": "name", "type": "string" },
295 { "name": "wallet", "type": "address" }
296 ],
297 "Mail": [
298 { "name": "from", "type": "Person" },
299 { "name": "to", "type": "Person" },
300 { "name": "contents", "type": "string" }
301 ]
302 }
303 }"#;
304 let data = from_str::<EIP712>(string).unwrap();
305 assert!(data.validate().is_ok());
306 }
307
308 #[test]
309 fn test_deserialization_from_value() {
310 let value = serde_json::json!({
311 "primaryType": "Mail",
312 "domain": {
313 "name": "Ether Mail",
314 "version": "1"
315 },
316 "message": {
317 "from": {
318 "name": "Cow",
319 "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
320 },
321 "to": {
322 "name": "Bob",
323 "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
324 },
325 "contents": "Hello, Bob!"
326 },
327 "types": {
328 "EIP712Domain": [
329 { "name": "name", "type": "string" },
330 { "name": "version", "type": "string" }
331 ],
332 "Person": [
333 { "name": "name", "type": "string" },
334 { "name": "wallet", "type": "address" }
335 ],
336 "Mail": [
337 { "name": "from", "type": "Person" },
338 { "name": "to", "type": "Person" },
339 { "name": "contents", "type": "string" }
340 ]
341 }
342 });
343 let data = serde_json::from_value::<EIP712>(value).unwrap();
344 assert!(data.validate().is_ok());
345 }
346}