eip712_enc/
eip712.rs

1// Copyright 2015-2019 Parity Technologies (UK) Ltd.
2// This file is part of Parity Ethereum.
3
4// Parity Ethereum is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Parity Ethereum is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Parity Ethereum.  If not, see <http://www.gnu.org/licenses/>.
16
17//! EIP712 structs
18use 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    // match solidity identifier with the addition of '[(\d)*]*'
30    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/// EIP-712 struct
66#[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}