1use chaincodec_core::types::NormalizedValue;
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18use std::collections::HashMap;
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
22pub struct Eip712TypeField {
23 pub name: String,
25 #[serde(rename = "type")]
27 pub ty: String,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct TypedData {
33 pub types: HashMap<String, Vec<Eip712TypeField>>,
35 pub primary_type: String,
37 pub domain: HashMap<String, TypedValue>,
39 pub message: HashMap<String, TypedValue>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
45#[serde(untagged)]
46pub enum TypedValue {
47 Null,
48 Bool(bool),
49 Number(serde_json::Number),
50 Str(String),
51 Array(Vec<TypedValue>),
52 Object(HashMap<String, TypedValue>),
53}
54
55impl TypedValue {
56 pub fn to_normalized(&self, type_hint: &str) -> NormalizedValue {
58 match self {
59 TypedValue::Null => NormalizedValue::Null,
60 TypedValue::Bool(b) => NormalizedValue::Bool(*b),
61 TypedValue::Str(s) => {
62 if type_hint == "address" {
63 NormalizedValue::Address(s.clone())
64 } else if type_hint.starts_with("bytes") {
65 let hex = s.strip_prefix("0x").unwrap_or(s);
67 match hex::decode(hex) {
68 Ok(b) => NormalizedValue::Bytes(b),
69 Err(_) => NormalizedValue::Str(s.clone()),
70 }
71 } else {
72 NormalizedValue::Str(s.clone())
73 }
74 }
75 TypedValue::Number(n) => {
76 if let Some(u) = n.as_u64() {
77 NormalizedValue::Uint(u as u128)
78 } else if let Some(i) = n.as_i64() {
79 NormalizedValue::Int(i as i128)
80 } else {
81 NormalizedValue::Str(n.to_string())
82 }
83 }
84 TypedValue::Array(elems) => {
85 let inner_type = type_hint.strip_suffix("[]").unwrap_or(type_hint);
87 NormalizedValue::Array(
88 elems.iter().map(|e| e.to_normalized(inner_type)).collect(),
89 )
90 }
91 TypedValue::Object(fields) => {
92 let named: Vec<(String, NormalizedValue)> = fields
94 .iter()
95 .map(|(k, v)| (k.clone(), v.to_normalized("unknown")))
96 .collect();
97 NormalizedValue::Tuple(named)
98 }
99 }
100 }
101}
102
103pub struct Eip712Parser;
105
106impl Eip712Parser {
107 pub fn parse(json: &str) -> Result<TypedData, String> {
112 let v: Value = serde_json::from_str(json).map_err(|e| e.to_string())?;
113
114 let types_raw = v.get("types").ok_or("missing 'types' field")?;
115 let types = parse_types(types_raw)?;
116
117 let primary_type = v
118 .get("primaryType")
119 .and_then(|v| v.as_str())
120 .ok_or("missing 'primaryType'")?
121 .to_string();
122
123 let domain_raw = v.get("domain").ok_or("missing 'domain'")?;
124 let domain = parse_object_values(domain_raw)?;
125
126 let message_raw = v.get("message").ok_or("missing 'message'")?;
127 let message = parse_object_values(message_raw)?;
128
129 Ok(TypedData {
130 types,
131 primary_type,
132 domain,
133 message,
134 })
135 }
136
137 pub fn primary_type_fields<'a>(td: &'a TypedData) -> Option<&'a Vec<Eip712TypeField>> {
139 td.types.get(&td.primary_type)
140 }
141
142 pub fn domain_separator_hex(td: &TypedData) -> String {
146 let domain_json = serde_json::to_string(&td.domain).unwrap_or_default();
150 let hash = tiny_keccak::keccak256(domain_json.as_bytes());
151 format!("0x{}", hex::encode(hash))
152 }
153}
154
155fn parse_types(v: &Value) -> Result<HashMap<String, Vec<Eip712TypeField>>, String> {
156 let obj = v.as_object().ok_or("'types' must be an object")?;
157 let mut types = HashMap::new();
158 for (type_name, fields_val) in obj {
159 let fields_arr = fields_val
160 .as_array()
161 .ok_or_else(|| format!("type '{}' fields must be an array", type_name))?;
162 let mut fields = Vec::new();
163 for field_val in fields_arr {
164 let name = field_val
165 .get("name")
166 .and_then(|v| v.as_str())
167 .ok_or_else(|| format!("field in '{}' missing 'name'", type_name))?
168 .to_string();
169 let ty = field_val
170 .get("type")
171 .and_then(|v| v.as_str())
172 .ok_or_else(|| format!("field '{}' in '{}' missing 'type'", name, type_name))?
173 .to_string();
174 fields.push(Eip712TypeField { name, ty });
175 }
176 types.insert(type_name.clone(), fields);
177 }
178 Ok(types)
179}
180
181fn parse_object_values(v: &Value) -> Result<HashMap<String, TypedValue>, String> {
182 let obj = v.as_object().ok_or("expected JSON object")?;
183 let mut map = HashMap::new();
184 for (k, v) in obj {
185 map.insert(k.clone(), json_to_typed_value(v));
186 }
187 Ok(map)
188}
189
190fn json_to_typed_value(v: &Value) -> TypedValue {
191 match v {
192 Value::Null => TypedValue::Null,
193 Value::Bool(b) => TypedValue::Bool(*b),
194 Value::Number(n) => TypedValue::Number(n.clone()),
195 Value::String(s) => TypedValue::Str(s.clone()),
196 Value::Array(arr) => TypedValue::Array(arr.iter().map(json_to_typed_value).collect()),
197 Value::Object(obj) => {
198 let map: HashMap<String, TypedValue> = obj
199 .iter()
200 .map(|(k, v)| (k.clone(), json_to_typed_value(v)))
201 .collect();
202 TypedValue::Object(map)
203 }
204 }
205}
206
207mod tiny_keccak {
209 pub fn keccak256(data: &[u8]) -> [u8; 32] {
210 use ::tiny_keccak::{Hasher, Keccak};
211 let mut k = Keccak::v256();
212 k.update(data);
213 let mut out = [0u8; 32];
214 k.finalize(&mut out);
215 out
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 const EIP712_EXAMPLE: &str = r#"{
225 "types": {
226 "EIP712Domain": [
227 {"name": "name", "type": "string"},
228 {"name": "version", "type": "string"},
229 {"name": "chainId", "type": "uint256"},
230 {"name": "verifyingContract", "type": "address"}
231 ],
232 "Mail": [
233 {"name": "from", "type": "Person"},
234 {"name": "to", "type": "Person"},
235 {"name": "contents", "type": "string"}
236 ],
237 "Person": [
238 {"name": "name", "type": "string"},
239 {"name": "wallet", "type": "address"}
240 ]
241 },
242 "primaryType": "Mail",
243 "domain": {
244 "name": "Ether Mail",
245 "version": "1",
246 "chainId": 1,
247 "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
248 },
249 "message": {
250 "from": {
251 "name": "Cow",
252 "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
253 },
254 "to": {
255 "name": "Bob",
256 "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
257 },
258 "contents": "Hello, Bob!"
259 }
260 }"#;
261
262 #[test]
263 fn parse_eip712_example() {
264 let td = Eip712Parser::parse(EIP712_EXAMPLE).unwrap();
265 assert_eq!(td.primary_type, "Mail");
266 assert!(td.types.contains_key("EIP712Domain"));
267 assert!(td.types.contains_key("Mail"));
268 assert!(td.types.contains_key("Person"));
269 }
270
271 #[test]
272 fn primary_type_fields_count() {
273 let td = Eip712Parser::parse(EIP712_EXAMPLE).unwrap();
274 let fields = Eip712Parser::primary_type_fields(&td).unwrap();
275 assert_eq!(fields.len(), 3); }
277
278 #[test]
279 fn domain_has_chain_id() {
280 let td = Eip712Parser::parse(EIP712_EXAMPLE).unwrap();
281 assert!(td.domain.contains_key("chainId"));
282 }
283
284 #[test]
285 fn message_contents() {
286 let td = Eip712Parser::parse(EIP712_EXAMPLE).unwrap();
287 let contents = td.message.get("contents").unwrap();
288 assert_eq!(*contents, TypedValue::Str("Hello, Bob!".into()));
289 }
290
291 #[test]
292 fn missing_fields_return_error() {
293 let bad_json = r#"{"types": {}}"#;
294 let result = Eip712Parser::parse(bad_json);
295 assert!(result.is_err());
296 }
297}