1use crate::client::network::{L1_CHAIN_ID, Network};
6use crate::utils::address::Address;
7use crate::utils::hex;
8use crate::utils::json::{BadJson, Json};
9use crate::utils::sign::keccak::{Keccak256, keccak256};
10use crate::utils::sign::msgpack::Value;
11
12pub const DOMAIN_TYPE: &str = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)";
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct Domain {
16 pub name: &'static str,
17 pub version: &'static str,
18 pub chain_id: u64,
19 pub verifying_contract: Address,
20}
21
22impl Domain {
23 pub const EXCHANGE: Domain =
27 Domain { name: "Exchange", version: "1", chain_id: L1_CHAIN_ID, verifying_contract: Address::ZERO };
28
29 #[must_use]
32 pub const fn user_signed(network: Network) -> Domain {
33 Domain {
34 name: "HyperliquidSignTransaction",
35 version: "1",
36 chain_id: network.signature_chain_id(),
37 verifying_contract: Address::ZERO,
38 }
39 }
40
41 #[must_use]
42 pub fn separator(&self) -> [u8; 32] {
43 Keccak256::new()
44 .chain(&keccak256(DOMAIN_TYPE.as_bytes()))
45 .chain(&keccak256(self.name.as_bytes()))
46 .chain(&keccak256(self.version.as_bytes()))
47 .chain(&word(self.chain_id))
48 .chain(&address_word(self.verifying_contract))
49 .finalize()
50 }
51
52 fn to_json(self) -> Json {
54 Json::Object(vec![
55 ("name".to_owned(), Json::String(self.name.to_owned())),
56 ("version".to_owned(), Json::String(self.version.to_owned())),
57 ("chainId".to_owned(), Json::Number(self.chain_id.to_string())),
58 ("verifyingContract".to_owned(), Json::String(self.verifying_contract.to_hex())),
59 ])
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum Field<'a> {
65 String(&'a str),
66 Address(Address),
67 Uint64(u64),
68 Bytes32([u8; 32]),
69 Bool(bool),
70 Uint32(u32),
71 Uint128(u128),
72 Uint256([u8; 32]),
74 Uint256Array(&'a [[u8; 32]]),
76}
77
78#[must_use]
80pub const fn uint256(value: u128) -> [u8; 32] {
81 let bytes = value.to_be_bytes();
82 let mut word = [0u8; 32];
83 let mut index = 0;
84 while index < 16 {
85 word[16 + index] = bytes[index];
86 index += 1;
87 }
88 word
89}
90
91impl Field<'_> {
92 #[must_use]
93 pub const fn solidity_type(&self) -> &'static str {
94 match self {
95 Field::String(_) => "string",
96 Field::Address(_) => "address",
97 Field::Uint64(_) => "uint64",
98 Field::Bytes32(_) => "bytes32",
99 Field::Bool(_) => "bool",
100 Field::Uint32(_) => "uint32",
101 Field::Uint128(_) => "uint128",
102 Field::Uint256(_) => "uint256",
103 Field::Uint256Array(_) => "uint256[]",
104 }
105 }
106
107 #[must_use]
110 pub fn encode(&self) -> [u8; 32] {
111 match self {
112 Field::String(text) => keccak256(text.as_bytes()),
113 Field::Address(address) => address_word(*address),
114 Field::Uint64(number) => word(*number),
115 Field::Bytes32(bytes) | Field::Uint256(bytes) => *bytes,
116 Field::Bool(value) => word(u64::from(*value)),
117 Field::Uint32(number) => word(u64::from(*number)),
118 Field::Uint128(number) => uint256(*number),
119 Field::Uint256Array(words) => {
120 words.iter().fold(Keccak256::new(), |sponge, element| sponge.chain(element)).finalize()
121 }
122 }
123 }
124
125 #[must_use]
130 pub fn to_json(&self) -> Json {
131 match self {
132 Field::String(text) => Json::String((*text).to_owned()),
133 Field::Address(address) => Json::String(address.to_hex()),
134 Field::Uint64(number) => Json::Number(number.to_string()),
135 Field::Bytes32(bytes) => Json::String(hex::encode_prefixed(bytes)),
136 Field::Bool(value) => Json::Bool(*value),
137 Field::Uint32(number) => Json::Number(number.to_string()),
138 Field::Uint128(number) => Json::String(number.to_string()),
139 Field::Uint256(bytes) => Json::String(word_decimal(bytes)),
140 Field::Uint256Array(words) => {
141 Json::Array(words.iter().map(|element| Json::String(word_decimal(element))).collect())
142 }
143 }
144 }
145
146 #[must_use]
149 pub fn to_wire(&self) -> Value {
150 match self {
151 Field::String(text) => Value::from((*text).to_owned()),
152 Field::Address(address) => Value::from(address.to_hex()),
153 Field::Uint64(number) => Value::Uint(*number),
154 Field::Bytes32(bytes) => Value::from(hex::encode_prefixed(bytes)),
155 Field::Bool(value) => Value::Bool(*value),
156 Field::Uint32(number) => Value::Uint(u64::from(*number)),
157 Field::Uint128(number) => Value::from(number.to_string()),
158 Field::Uint256(bytes) => Value::from(word_decimal(bytes)),
159 Field::Uint256Array(words) => {
160 Value::Array(words.iter().map(|element| Value::from(word_decimal(element))).collect())
161 }
162 }
163 }
164}
165
166#[must_use]
168pub fn word_decimal(bytes: &[u8; 32]) -> String {
169 const GROUP: u128 = 10_000_000_000_000_000_000;
171 let mut limbs = [0u64; 4];
172 for (limb, chunk) in limbs.iter_mut().zip(bytes.chunks_exact(8)) {
173 let mut eight = [0u8; 8];
174 eight.copy_from_slice(chunk);
175 *limb = u64::from_be_bytes(eight);
176 }
177 let mut groups = Vec::new();
178 while limbs.iter().any(|limb| *limb != 0) {
179 let mut remainder = 0u128;
180 for limb in &mut limbs {
181 let value = (remainder << 64) | u128::from(*limb);
182 *limb = (value / GROUP) as u64;
184 remainder = value % GROUP;
185 }
186 groups.push(remainder as u64);
187 }
188 let Some((first, rest)) = groups.split_last() else {
189 return "0".to_owned();
190 };
191 let mut text = first.to_string();
192 for group in rest.iter().rev() {
193 text.push_str(&format!("{group:019}"));
194 }
195 text
196}
197
198#[must_use]
200pub fn decimal_word(digits: &str) -> Option<[u8; 32]> {
201 if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
202 return None;
203 }
204 let mut limbs = [0u64; 4];
205 for byte in digits.bytes() {
206 let mut carry = u128::from(byte - b'0');
207 for limb in limbs.iter_mut().rev() {
208 let value = u128::from(*limb) * 10 + carry;
209 *limb = value as u64;
211 carry = value >> 64;
212 }
213 if carry != 0 {
214 return None;
215 }
216 }
217 let mut word = [0u8; 32];
218 for (chunk, limb) in word.chunks_exact_mut(8).zip(limbs) {
219 chunk.copy_from_slice(&limb.to_be_bytes());
220 }
221 Some(word)
222}
223
224#[derive(Debug, Clone, PartialEq, Eq)]
225pub struct TypedStruct<'a> {
226 primary_type: &'a str,
227 fields: Vec<(&'a str, Field<'a>)>,
228}
229
230impl<'a> TypedStruct<'a> {
231 #[must_use]
232 pub const fn new(primary_type: &'a str) -> Self {
233 TypedStruct { primary_type, fields: Vec::new() }
234 }
235
236 #[must_use]
238 pub fn field(mut self, name: &'a str, value: Field<'a>) -> Self {
239 self.fields.push((name, value));
240 self
241 }
242
243 #[must_use]
244 pub const fn primary_type(&self) -> &'a str {
245 self.primary_type
246 }
247
248 #[must_use]
249 pub fn fields(&self) -> &[(&'a str, Field<'a>)] {
250 &self.fields
251 }
252
253 #[must_use]
254 pub fn type_signature(&self) -> String {
255 let mut text = String::with_capacity(self.primary_type.len() + 32);
256 text.push_str(self.primary_type);
257 text.push('(');
258 for (index, (name, value)) in self.fields.iter().enumerate() {
259 if index > 0 {
260 text.push(',');
261 }
262 text.push_str(value.solidity_type());
263 text.push(' ');
264 text.push_str(name);
265 }
266 text.push(')');
267 text
268 }
269
270 #[must_use]
271 pub fn type_hash(&self) -> [u8; 32] {
272 keccak256(self.type_signature().as_bytes())
273 }
274
275 #[must_use]
276 pub fn hash(&self) -> [u8; 32] {
277 let mut sponge = Keccak256::new().chain(&self.type_hash());
278 for (_, value) in &self.fields {
279 sponge = sponge.chain(&value.encode());
280 }
281 sponge.finalize()
282 }
283
284 #[must_use]
286 pub fn digest(&self, domain: &Domain) -> [u8; 32] {
287 digest(domain, self)
288 }
289
290 #[must_use]
294 pub fn wallet_json(&self, domain: &Domain) -> String {
295 let list = |pairs: Vec<(&str, &str)>| {
296 Json::Array(
297 pairs
298 .into_iter()
299 .map(|(name, kind)| {
300 Json::Object(vec![
301 ("name".to_owned(), Json::String(name.to_owned())),
302 ("type".to_owned(), Json::String(kind.to_owned())),
303 ])
304 })
305 .collect(),
306 )
307 };
308 let domain_fields = list(vec![
309 ("name", "string"),
310 ("version", "string"),
311 ("chainId", "uint256"),
312 ("verifyingContract", "address"),
313 ]);
314 let struct_fields = list(self.fields.iter().map(|(name, value)| (*name, value.solidity_type())).collect());
315 let message =
316 Json::Object(self.fields.iter().map(|(name, value)| ((*name).to_owned(), value.to_json())).collect());
317 Json::Object(vec![
318 (
319 "types".to_owned(),
320 Json::Object(vec![
321 ("EIP712Domain".to_owned(), domain_fields),
322 (self.primary_type.to_owned(), struct_fields),
323 ]),
324 ),
325 ("primaryType".to_owned(), Json::String(self.primary_type.to_owned())),
326 ("domain".to_owned(), domain.to_json()),
327 ("message".to_owned(), message),
328 ])
329 .compact()
330 }
331}
332
333#[must_use]
335pub fn digest(domain: &Domain, data: &TypedStruct<'_>) -> [u8; 32] {
336 Keccak256::new().chain(&[0x19, 0x01]).chain(&domain.separator()).chain(&data.hash()).finalize()
337}
338
339#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
341pub enum BadTypedData {
342 #[error("not JSON: {0}")]
343 Json(#[from] BadJson),
344 #[error("{0}")]
345 Shape(String),
346}
347
348pub fn digest_of_wallet_json(text: &str) -> Result<[u8; 32], BadTypedData> {
353 let shape = |what: &str| BadTypedData::Shape(what.to_owned());
354 let json = Json::parse(text)?;
355 let types = json.get("types").ok_or_else(|| shape("no `types`"))?;
356 let primary = json.get("primaryType").and_then(Json::as_str).ok_or_else(|| shape("no `primaryType`"))?;
357 let list = |name: &str| {
358 types.get(name).and_then(Json::as_array).ok_or_else(|| BadTypedData::Shape(format!("no type `{name}`")))
359 };
360 let domain = json.get("domain").ok_or_else(|| shape("no `domain`"))?;
361 let message = json.get("message").ok_or_else(|| shape("no `message`"))?;
362 let separator = struct_hash("EIP712Domain", list("EIP712Domain")?, domain)?;
363 let data = struct_hash(primary, list(primary)?, message)?;
364 Ok(Keccak256::new().chain(&[0x19, 0x01]).chain(&separator).chain(&data).finalize())
365}
366
367fn struct_hash(name: &str, fields: &[Json], values: &Json) -> Result<[u8; 32], BadTypedData> {
368 let mut signature = format!("{name}(");
369 let mut words = Vec::with_capacity(fields.len());
370 for (index, field) in fields.iter().enumerate() {
371 let (Some(field_name), Some(kind)) =
372 (field.get("name").and_then(Json::as_str), field.get("type").and_then(Json::as_str))
373 else {
374 return Err(BadTypedData::Shape(format!("a field of `{name}` without a name and a type")));
375 };
376 if index > 0 {
377 signature.push(',');
378 }
379 signature.push_str(kind);
380 signature.push(' ');
381 signature.push_str(field_name);
382 let value = values
383 .get(field_name)
384 .ok_or_else(|| BadTypedData::Shape(format!("`{name}` has no value for `{field_name}`")))?;
385 let word = encode_json(kind, value)
386 .ok_or_else(|| BadTypedData::Shape(format!("`{field_name}` is not a `{kind}`: {}", value.compact())))?;
387 words.push(word);
388 }
389 signature.push(')');
390 let mut sponge = Keccak256::new().chain(&keccak256(signature.as_bytes()));
391 for word in &words {
392 sponge = sponge.chain(word);
393 }
394 Ok(sponge.finalize())
395}
396
397fn encode_json(kind: &str, value: &Json) -> Option<[u8; 32]> {
398 match (kind, value) {
399 ("string", Json::String(text)) => Some(keccak256(text.as_bytes())),
400 ("address", Json::String(text)) => text.parse().ok().map(address_word),
401 ("bool", Json::Bool(flag)) => Some(word(u64::from(*flag))),
402 ("bytes32", Json::String(text)) => hex::decode_array(text).ok(),
403 ("uint256[]", Json::Array(items)) => {
404 let mut sponge = Keccak256::new();
405 for item in items {
406 sponge = sponge.chain(&encode_json("uint256", item)?);
407 }
408 Some(sponge.finalize())
409 }
410 (_, Json::Number(digits) | Json::String(digits)) => {
412 let bits: usize = kind.strip_prefix("uint")?.parse().ok()?;
413 let value = decimal_word(digits)?;
414 let spare = 256usize.checked_sub(bits)? / 8;
415 value[..spare].iter().all(|byte| *byte == 0).then_some(value)
416 }
417 _ => None,
418 }
419}
420
421fn word(value: u64) -> [u8; 32] {
422 let mut bytes = [0u8; 32];
423 bytes[24..].copy_from_slice(&value.to_be_bytes());
424 bytes
425}
426
427fn address_word(address: Address) -> [u8; 32] {
428 let mut bytes = [0u8; 32];
429 bytes[12..].copy_from_slice(address.bytes());
430 bytes
431}
432
433#[cfg(test)]
434#[path = "tests/eip712.rs"]
435mod tests;