df_hyperliquid_core/utils/sign/eip712/
wallet.rs1use crate::utils::hex;
4use crate::utils::json::{BadJson, Json};
5use crate::utils::sign::eip712::word::{address_word, decimal_word, word};
6use crate::utils::sign::keccak::{Keccak256, keccak256};
7
8#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
10pub enum BadTypedData {
11 #[error("not JSON: {0}")]
12 Json(#[from] BadJson),
13 #[error("{0}")]
14 Shape(String),
15}
16
17pub fn digest_of_wallet_json(text: &str) -> Result<[u8; 32], BadTypedData> {
23 let shape = |what: &str| BadTypedData::Shape(what.to_owned());
24 let json = Json::parse(text)?;
25 let types = json.get("types").ok_or_else(|| shape("no `types`"))?;
26 let primary = json.get("primaryType").and_then(Json::as_str).ok_or_else(|| shape("no `primaryType`"))?;
27 let list = |name: &str| {
28 types.get(name).and_then(Json::as_array).ok_or_else(|| BadTypedData::Shape(format!("no type `{name}`")))
29 };
30 let domain = json.get("domain").ok_or_else(|| shape("no `domain`"))?;
31 let message = json.get("message").ok_or_else(|| shape("no `message`"))?;
32 let separator = struct_hash("EIP712Domain", list("EIP712Domain")?, domain)?;
33 let data = struct_hash(primary, list(primary)?, message)?;
34 Ok(Keccak256::new().chain(&[0x19, 0x01]).chain(&separator).chain(&data).finalize())
35}
36
37fn struct_hash(name: &str, fields: &[Json], values: &Json) -> Result<[u8; 32], BadTypedData> {
38 let mut signature = format!("{name}(");
39 let mut words = Vec::with_capacity(fields.len());
40 for (index, field) in fields.iter().enumerate() {
41 let (Some(field_name), Some(kind)) =
42 (field.get("name").and_then(Json::as_str), field.get("type").and_then(Json::as_str))
43 else {
44 return Err(BadTypedData::Shape(format!("a field of `{name}` without a name and a type")));
45 };
46 if index > 0 {
47 signature.push(',');
48 }
49 signature.push_str(kind);
50 signature.push(' ');
51 signature.push_str(field_name);
52 let value = values
53 .get(field_name)
54 .ok_or_else(|| BadTypedData::Shape(format!("`{name}` has no value for `{field_name}`")))?;
55 let word = encode_json(kind, value)
56 .ok_or_else(|| BadTypedData::Shape(format!("`{field_name}` is not a `{kind}`: {}", value.compact())))?;
57 words.push(word);
58 }
59 signature.push(')');
60 let mut sponge = Keccak256::new().chain(&keccak256(signature.as_bytes()));
61 for word in &words {
62 sponge = sponge.chain(word);
63 }
64 Ok(sponge.finalize())
65}
66
67fn encode_json(kind: &str, value: &Json) -> Option<[u8; 32]> {
68 match (kind, value) {
69 ("string", Json::String(text)) => Some(keccak256(text.as_bytes())),
70 ("address", Json::String(text)) => text.parse().ok().map(address_word),
71 ("bool", Json::Bool(flag)) => Some(word(u64::from(*flag))),
72 ("bytes32", Json::String(text)) => hex::decode_array(text).ok(),
73 ("uint256[]", Json::Array(items)) => {
74 let mut sponge = Keccak256::new();
75 for item in items {
76 sponge = sponge.chain(&encode_json("uint256", item)?);
77 }
78 Some(sponge.finalize())
79 }
80 (_, Json::Number(digits) | Json::String(digits)) => {
82 let bits: usize = kind.strip_prefix("uint")?.parse().ok()?;
83 let value = decimal_word(digits)?;
84 let spare = 256usize.checked_sub(bits)? / 8;
85 value[..spare].iter().all(|byte| *byte == 0).then_some(value)
86 }
87 _ => None,
88 }
89}
90
91#[cfg(test)]
92#[path = "tests/wallet.rs"]
93mod tests;