ccxt_exchanges/hyperliquid/
auth.rs1use ccxt_core::error::{Error, Result};
8
9#[derive(Debug, Clone)]
14pub struct HyperLiquidAuth {
15 private_key: [u8; 32],
17 wallet_address: String,
19}
20
21impl HyperLiquidAuth {
22 pub fn from_private_key(private_key_hex: &str) -> Result<Self> {
50 let hex_str = private_key_hex
52 .strip_prefix("0x")
53 .or_else(|| private_key_hex.strip_prefix("0X"))
54 .unwrap_or(private_key_hex);
55
56 let bytes = hex::decode(hex_str)
58 .map_err(|e| Error::invalid_argument(format!("Invalid private key hex: {}", e)))?;
59
60 if bytes.len() != 32 {
62 return Err(Error::invalid_argument(format!(
63 "Private key must be 32 bytes, got {}",
64 bytes.len()
65 )));
66 }
67
68 let mut private_key = [0u8; 32];
70 private_key.copy_from_slice(&bytes);
71
72 let wallet_address = derive_address(&private_key)?;
74
75 Ok(Self {
76 private_key,
77 wallet_address,
78 })
79 }
80
81 pub fn wallet_address(&self) -> &str {
83 &self.wallet_address
84 }
85
86 pub fn private_key_bytes(&self) -> &[u8; 32] {
92 &self.private_key
93 }
94
95 pub fn sign_l1_action(
107 &self,
108 action: &serde_json::Value,
109 nonce: u64,
110 is_mainnet: bool,
111 ) -> Result<Eip712Signature> {
112 let typed_data_hash = build_typed_data_hash(action, nonce, is_mainnet)?;
114
115 sign_hash(&self.private_key, &typed_data_hash)
117 }
118
119 pub fn sign_agent(&self, agent_address: &str) -> Result<Eip712Signature> {
129 let message = format!("I authorize {} to trade on my behalf.", agent_address);
130
131 sign_personal_message(&self.private_key, &message)
133 }
134}
135
136#[derive(Debug, Clone)]
138pub struct Eip712Signature {
139 pub r: String,
141 pub s: String,
143 pub v: u8,
145}
146
147impl Eip712Signature {
148 pub fn to_hex(&self) -> String {
150 format!("0x{}{}{:02x}", self.r, self.s, self.v)
151 }
152}
153
154fn derive_address(private_key: &[u8; 32]) -> Result<String> {
156 use k256::ecdsa::SigningKey;
157 use sha3::{Digest, Keccak256};
158
159 let signing_key = SigningKey::from_bytes(private_key.into())
161 .map_err(|e| Error::invalid_argument(format!("Invalid private key: {}", e)))?;
162
163 let public_key = signing_key.verifying_key();
165 let public_key_bytes = public_key.to_encoded_point(false);
166
167 let public_key_data = &public_key_bytes.as_bytes()[1..];
169
170 let mut hasher = Keccak256::new();
172 hasher.update(public_key_data);
173 let hash = hasher.finalize();
174
175 let address_bytes = &hash[12..];
177 let address = format!("0x{}", hex::encode(address_bytes));
178
179 Ok(checksum_address(&address))
181}
182
183fn checksum_address(address: &str) -> String {
185 use sha3::{Digest, Keccak256};
186
187 let addr = address.strip_prefix("0x").unwrap_or(address).to_lowercase();
188
189 let mut hasher = Keccak256::new();
190 hasher.update(addr.as_bytes());
191 let hash = hasher.finalize();
192 let hash_hex = hex::encode(hash);
193
194 let mut checksummed = String::with_capacity(42);
195 checksummed.push_str("0x");
196
197 for (i, c) in addr.chars().enumerate() {
198 if c.is_ascii_digit() {
199 checksummed.push(c);
200 } else {
201 let hash_char = hash_hex.chars().nth(i).unwrap_or('0');
202 let hash_val = hash_char.to_digit(16).unwrap_or(0);
203 if hash_val >= 8 {
204 checksummed.push(c.to_ascii_uppercase());
205 } else {
206 checksummed.push(c);
207 }
208 }
209 }
210
211 checksummed
212}
213
214fn build_typed_data_hash(
216 action: &serde_json::Value,
217 nonce: u64,
218 is_mainnet: bool,
219) -> Result<[u8; 32]> {
220 #![allow(unused_imports)]
221 use sha3::{Digest, Keccak256};
222
223 let chain_id: u64 = if is_mainnet { 42161 } else { 421614 };
225
226 let domain_type_hash = keccak256(
228 b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)",
229 );
230
231 let name_hash = keccak256(b"HyperliquidSignTransaction");
232 let version_hash = keccak256(b"1");
233 let verifying_contract = [0u8; 20]; let mut domain_data = Vec::new();
236 domain_data.extend_from_slice(&domain_type_hash);
237 domain_data.extend_from_slice(&name_hash);
238 domain_data.extend_from_slice(&version_hash);
239 domain_data.extend_from_slice(&pad_u256(chain_id));
240 domain_data.extend_from_slice(&[0u8; 12]); domain_data.extend_from_slice(&verifying_contract);
242
243 let domain_separator = keccak256(&domain_data);
244
245 let action_str = serde_json::to_string(action)
247 .map_err(|e| Error::invalid_argument(format!("Failed to serialize action: {}", e)))?;
248
249 let mut message_data = Vec::new();
251 message_data.extend_from_slice(&keccak256(action_str.as_bytes()));
252 message_data.extend_from_slice(&pad_u256(nonce));
253
254 let message_hash = keccak256(&message_data);
255
256 let mut final_data = Vec::new();
258 final_data.push(0x19);
259 final_data.push(0x01);
260 final_data.extend_from_slice(&domain_separator);
261 final_data.extend_from_slice(&message_hash);
262
263 Ok(keccak256(&final_data))
264}
265
266fn sign_hash(private_key: &[u8; 32], hash: &[u8; 32]) -> Result<Eip712Signature> {
268 use k256::ecdsa::{Signature, SigningKey, signature::Signer};
269
270 let signing_key = SigningKey::from_bytes(private_key.into())
271 .map_err(|e| Error::invalid_argument(format!("Invalid private key: {}", e)))?;
272
273 let signature: Signature = signing_key.sign(hash);
274 let sig_bytes = signature.to_bytes();
275
276 let r = hex::encode(&sig_bytes[..32]);
278 let s = hex::encode(&sig_bytes[32..]);
279
280 let v = 27u8;
282
283 Ok(Eip712Signature { r, s, v })
284}
285
286fn sign_personal_message(private_key: &[u8; 32], message: &str) -> Result<Eip712Signature> {
288 let prefix = format!("\x19Ethereum Signed Message:\n{}", message.len());
289 let mut data = prefix.into_bytes();
290 data.extend_from_slice(message.as_bytes());
291
292 let hash = keccak256(&data);
293 sign_hash(private_key, &hash)
294}
295
296fn keccak256(data: &[u8]) -> [u8; 32] {
298 use sha3::{Digest, Keccak256};
299 let mut hasher = Keccak256::new();
300 hasher.update(data);
301 hasher.finalize().into()
302}
303
304fn pad_u256(value: u64) -> [u8; 32] {
306 let mut result = [0u8; 32];
307 result[24..].copy_from_slice(&value.to_be_bytes());
308 result
309}
310
311#[allow(dead_code)]
313fn keccak256_hash(data: &[u8]) -> [u8; 32] {
314 use sha3::{Digest, Keccak256};
315 let mut hasher = Keccak256::new();
316 hasher.update(data);
317 hasher.finalize().into()
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 const TEST_PRIVATE_KEY: &str =
326 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
327
328 #[test]
329 fn test_from_private_key_with_prefix() {
330 let auth = HyperLiquidAuth::from_private_key(TEST_PRIVATE_KEY);
331 assert!(auth.is_ok());
332 let auth = auth.unwrap();
333 assert!(auth.wallet_address().starts_with("0x"));
334 assert_eq!(auth.wallet_address().len(), 42);
335 }
336
337 #[test]
338 fn test_from_private_key_without_prefix() {
339 let key = TEST_PRIVATE_KEY.strip_prefix("0x").unwrap();
340 let auth = HyperLiquidAuth::from_private_key(key);
341 assert!(auth.is_ok());
342 }
343
344 #[test]
345 fn test_invalid_private_key_length() {
346 let result = HyperLiquidAuth::from_private_key("0x1234");
347 assert!(result.is_err());
348 }
349
350 #[test]
351 fn test_invalid_private_key_hex() {
352 let result = HyperLiquidAuth::from_private_key("0xGGGG");
353 assert!(result.is_err());
354 }
355
356 #[test]
357 fn test_address_derivation_deterministic() {
358 let auth1 = HyperLiquidAuth::from_private_key(TEST_PRIVATE_KEY).unwrap();
359 let auth2 = HyperLiquidAuth::from_private_key(TEST_PRIVATE_KEY).unwrap();
360 assert_eq!(auth1.wallet_address(), auth2.wallet_address());
361 }
362
363 #[test]
364 fn test_checksum_address() {
365 let addr = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266";
367 let checksummed = checksum_address(addr);
368 assert!(checksummed.chars().any(|c| c.is_uppercase()));
370 assert!(
371 checksummed
372 .chars()
373 .any(|c| c.is_lowercase() && c.is_alphabetic())
374 );
375 }
376
377 #[test]
378 fn test_signature_to_hex() {
379 let sig = Eip712Signature {
380 r: "a".repeat(64),
381 s: "b".repeat(64),
382 v: 27,
383 };
384 let hex = sig.to_hex();
385 assert!(hex.starts_with("0x"));
386 assert_eq!(hex.len(), 132); }
388
389 #[test]
390 fn test_sign_l1_action() {
391 let auth = HyperLiquidAuth::from_private_key(TEST_PRIVATE_KEY).unwrap();
392 let action = serde_json::json!({"type": "order", "data": {}});
393 let result = auth.sign_l1_action(&action, 1234567890, false);
394 assert!(result.is_ok());
395
396 let sig = result.unwrap();
397 assert_eq!(sig.r.len(), 64);
398 assert_eq!(sig.s.len(), 64);
399 }
400
401 #[test]
402 fn test_sign_deterministic() {
403 let auth = HyperLiquidAuth::from_private_key(TEST_PRIVATE_KEY).unwrap();
404 let action = serde_json::json!({"type": "test"});
405
406 let sig1 = auth.sign_l1_action(&action, 1000, false).unwrap();
407 let sig2 = auth.sign_l1_action(&action, 1000, false).unwrap();
408
409 assert_eq!(sig1.r, sig2.r);
410 assert_eq!(sig1.s, sig2.s);
411 }
412}