ccxt_exchanges/hyperliquid/
auth.rs1use ccxt_core::credentials::SecretBytes;
8use ccxt_core::error::{Error, Result};
9
10#[derive(Debug, Clone)]
17pub struct HyperLiquidAuth {
18 private_key: SecretBytes,
20 wallet_address: String,
22}
23
24impl HyperLiquidAuth {
25 pub fn from_private_key(private_key_hex: &str) -> Result<Self> {
57 let hex_str = private_key_hex
59 .strip_prefix("0x")
60 .or_else(|| private_key_hex.strip_prefix("0X"))
61 .unwrap_or(private_key_hex);
62
63 let bytes = hex::decode(hex_str)
65 .map_err(|e| Error::invalid_argument(format!("Invalid private key hex: {}", e)))?;
66
67 if bytes.len() != 32 {
69 return Err(Error::invalid_argument(format!(
70 "Private key must be 32 bytes, got {}",
71 bytes.len()
72 )));
73 }
74
75 let mut private_key_array = [0u8; 32];
77 private_key_array.copy_from_slice(&bytes);
78
79 let wallet_address = derive_address(&private_key_array)?;
81
82 let private_key = SecretBytes::from_array(private_key_array);
84
85 private_key_array.fill(0);
87
88 Ok(Self {
89 private_key,
90 wallet_address,
91 })
92 }
93
94 pub fn wallet_address(&self) -> &str {
96 &self.wallet_address
97 }
98
99 pub fn private_key_bytes(&self) -> &[u8] {
106 self.private_key.expose_secret()
107 }
108
109 pub fn sign_l1_action(
121 &self,
122 action: &serde_json::Value,
123 nonce: u64,
124 is_mainnet: bool,
125 ) -> Result<Eip712Signature> {
126 let typed_data_hash = build_typed_data_hash(action, nonce, is_mainnet)?;
128
129 let key_bytes = self.private_key.expose_secret();
131 let mut key_array = [0u8; 32];
132 key_array.copy_from_slice(key_bytes);
133
134 let result = sign_hash(&key_array, &typed_data_hash);
136
137 key_array.fill(0);
139
140 result
141 }
142
143 pub fn sign_agent(&self, agent_address: &str) -> Result<Eip712Signature> {
153 let message = format!("I authorize {} to trade on my behalf.", agent_address);
154
155 let key_bytes = self.private_key.expose_secret();
157 let mut key_array = [0u8; 32];
158 key_array.copy_from_slice(key_bytes);
159
160 let result = sign_personal_message(&key_array, &message);
162
163 key_array.fill(0);
165
166 result
167 }
168}
169
170#[derive(Debug, Clone)]
172pub struct Eip712Signature {
173 pub r: String,
175 pub s: String,
177 pub v: u8,
179}
180
181impl Eip712Signature {
182 pub fn to_hex(&self) -> String {
184 format!("0x{}{}{:02x}", self.r, self.s, self.v)
185 }
186}
187
188fn derive_address(private_key: &[u8; 32]) -> Result<String> {
190 use k256::ecdsa::SigningKey;
191 use sha3::{Digest, Keccak256};
192
193 let signing_key = SigningKey::from_bytes(private_key.into())
195 .map_err(|e| Error::invalid_argument(format!("Invalid private key: {}", e)))?;
196
197 let public_key = signing_key.verifying_key();
199 let public_key_bytes = public_key.to_encoded_point(false);
200
201 let public_key_data = &public_key_bytes.as_bytes()[1..];
203
204 let mut hasher = Keccak256::new();
206 hasher.update(public_key_data);
207 let hash = hasher.finalize();
208
209 let address_bytes = &hash[12..];
211 let address = format!("0x{}", hex::encode(address_bytes));
212
213 Ok(checksum_address(&address))
215}
216
217fn checksum_address(address: &str) -> String {
219 use sha3::{Digest, Keccak256};
220
221 let addr = address.strip_prefix("0x").unwrap_or(address).to_lowercase();
222
223 let mut hasher = Keccak256::new();
224 hasher.update(addr.as_bytes());
225 let hash = hasher.finalize();
226 let hash_hex = hex::encode(hash);
227
228 let mut checksummed = String::with_capacity(42);
229 checksummed.push_str("0x");
230
231 for (i, c) in addr.chars().enumerate() {
232 if c.is_ascii_digit() {
233 checksummed.push(c);
234 } else {
235 let hash_char = hash_hex.chars().nth(i).unwrap_or('0');
236 let hash_val = hash_char.to_digit(16).unwrap_or(0);
237 if hash_val >= 8 {
238 checksummed.push(c.to_ascii_uppercase());
239 } else {
240 checksummed.push(c);
241 }
242 }
243 }
244
245 checksummed
246}
247
248fn build_typed_data_hash(
250 action: &serde_json::Value,
251 nonce: u64,
252 is_mainnet: bool,
253) -> Result<[u8; 32]> {
254 #![allow(unused_imports)]
255 use sha3::{Digest, Keccak256};
256
257 let chain_id: u64 = if is_mainnet { 42161 } else { 421614 };
259
260 let domain_type_hash = keccak256(
262 b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)",
263 );
264
265 let name_hash = keccak256(b"HyperliquidSignTransaction");
266 let version_hash = keccak256(b"1");
267 let verifying_contract = [0u8; 20]; let mut domain_data = Vec::new();
270 domain_data.extend_from_slice(&domain_type_hash);
271 domain_data.extend_from_slice(&name_hash);
272 domain_data.extend_from_slice(&version_hash);
273 domain_data.extend_from_slice(&pad_u256(chain_id));
274 domain_data.extend_from_slice(&[0u8; 12]); domain_data.extend_from_slice(&verifying_contract);
276
277 let domain_separator = keccak256(&domain_data);
278
279 let action_str = serde_json::to_string(action)
281 .map_err(|e| Error::invalid_argument(format!("Failed to serialize action: {}", e)))?;
282
283 let mut message_data = Vec::new();
285 message_data.extend_from_slice(&keccak256(action_str.as_bytes()));
286 message_data.extend_from_slice(&pad_u256(nonce));
287
288 let message_hash = keccak256(&message_data);
289
290 let mut final_data = Vec::new();
292 final_data.push(0x19);
293 final_data.push(0x01);
294 final_data.extend_from_slice(&domain_separator);
295 final_data.extend_from_slice(&message_hash);
296
297 Ok(keccak256(&final_data))
298}
299
300fn sign_hash(private_key: &[u8; 32], hash: &[u8; 32]) -> Result<Eip712Signature> {
302 use k256::ecdsa::{Signature, SigningKey, signature::Signer};
303
304 let signing_key = SigningKey::from_bytes(private_key.into())
305 .map_err(|e| Error::invalid_argument(format!("Invalid private key: {}", e)))?;
306
307 let signature: Signature = signing_key.sign(hash);
308 let sig_bytes = signature.to_bytes();
309
310 let r = hex::encode(&sig_bytes[..32]);
312 let s = hex::encode(&sig_bytes[32..]);
313
314 let v = 27u8;
316
317 Ok(Eip712Signature { r, s, v })
318}
319
320fn sign_personal_message(private_key: &[u8; 32], message: &str) -> Result<Eip712Signature> {
322 let prefix = format!("\x19Ethereum Signed Message:\n{}", message.len());
323 let mut data = prefix.into_bytes();
324 data.extend_from_slice(message.as_bytes());
325
326 let hash = keccak256(&data);
327 sign_hash(private_key, &hash)
328}
329
330fn keccak256(data: &[u8]) -> [u8; 32] {
332 use sha3::{Digest, Keccak256};
333 let mut hasher = Keccak256::new();
334 hasher.update(data);
335 hasher.finalize().into()
336}
337
338fn pad_u256(value: u64) -> [u8; 32] {
340 let mut result = [0u8; 32];
341 result[24..].copy_from_slice(&value.to_be_bytes());
342 result
343}
344
345#[allow(dead_code)]
347fn keccak256_hash(data: &[u8]) -> [u8; 32] {
348 use sha3::{Digest, Keccak256};
349 let mut hasher = Keccak256::new();
350 hasher.update(data);
351 hasher.finalize().into()
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 const TEST_PRIVATE_KEY: &str =
360 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
361
362 #[test]
363 fn test_from_private_key_with_prefix() {
364 let auth = HyperLiquidAuth::from_private_key(TEST_PRIVATE_KEY);
365 assert!(auth.is_ok());
366 let auth = auth.unwrap();
367 assert!(auth.wallet_address().starts_with("0x"));
368 assert_eq!(auth.wallet_address().len(), 42);
369 }
370
371 #[test]
372 fn test_from_private_key_without_prefix() {
373 let key = TEST_PRIVATE_KEY.strip_prefix("0x").unwrap();
374 let auth = HyperLiquidAuth::from_private_key(key);
375 assert!(auth.is_ok());
376 }
377
378 #[test]
379 fn test_invalid_private_key_length() {
380 let result = HyperLiquidAuth::from_private_key("0x1234");
381 assert!(result.is_err());
382 }
383
384 #[test]
385 fn test_invalid_private_key_hex() {
386 let result = HyperLiquidAuth::from_private_key("0xGGGG");
387 assert!(result.is_err());
388 }
389
390 #[test]
391 fn test_address_derivation_deterministic() {
392 let auth1 = HyperLiquidAuth::from_private_key(TEST_PRIVATE_KEY).unwrap();
393 let auth2 = HyperLiquidAuth::from_private_key(TEST_PRIVATE_KEY).unwrap();
394 assert_eq!(auth1.wallet_address(), auth2.wallet_address());
395 }
396
397 #[test]
398 fn test_checksum_address() {
399 let addr = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266";
401 let checksummed = checksum_address(addr);
402 assert!(checksummed.chars().any(|c| c.is_uppercase()));
404 assert!(
405 checksummed
406 .chars()
407 .any(|c| c.is_lowercase() && c.is_alphabetic())
408 );
409 }
410
411 #[test]
412 fn test_signature_to_hex() {
413 let sig = Eip712Signature {
414 r: "a".repeat(64),
415 s: "b".repeat(64),
416 v: 27,
417 };
418 let hex = sig.to_hex();
419 assert!(hex.starts_with("0x"));
420 assert_eq!(hex.len(), 132); }
422
423 #[test]
424 fn test_sign_l1_action() {
425 let auth = HyperLiquidAuth::from_private_key(TEST_PRIVATE_KEY).unwrap();
426 let action = serde_json::json!({"type": "order", "data": {}});
427 let result = auth.sign_l1_action(&action, 1234567890, false);
428 assert!(result.is_ok());
429
430 let sig = result.unwrap();
431 assert_eq!(sig.r.len(), 64);
432 assert_eq!(sig.s.len(), 64);
433 }
434
435 #[test]
436 fn test_sign_deterministic() {
437 let auth = HyperLiquidAuth::from_private_key(TEST_PRIVATE_KEY).unwrap();
438 let action = serde_json::json!({"type": "test"});
439
440 let sig1 = auth.sign_l1_action(&action, 1000, false).unwrap();
441 let sig2 = auth.sign_l1_action(&action, 1000, false).unwrap();
442
443 assert_eq!(sig1.r, sig2.r);
444 assert_eq!(sig1.s, sig2.s);
445 }
446}