nautilus_hyperliquid/common/
credential.rs1#![allow(unused_assignments)] use std::{
19 env,
20 fmt::{Debug, Display},
21 fs,
22 path::Path,
23};
24
25use serde::Deserialize;
26use zeroize::{Zeroize, ZeroizeOnDrop};
27
28use crate::http::error::{Error, Result};
29
30#[derive(Clone, ZeroizeOnDrop)]
32pub struct EvmPrivateKey {
33 #[zeroize(skip)]
34 formatted_key: String, #[zeroize(skip)] raw_bytes: Vec<u8>, }
38
39impl EvmPrivateKey {
40 pub fn new(key: String) -> Result<Self> {
42 let key = key.trim().to_string();
43 let hex_key = key.strip_prefix("0x").unwrap_or(&key);
44
45 if hex_key.len() != 64 {
47 return Err(Error::bad_request(
48 "EVM private key must be 32 bytes (64 hex chars)",
49 ));
50 }
51
52 if !hex_key.chars().all(|c| c.is_ascii_hexdigit()) {
53 return Err(Error::bad_request("EVM private key must be valid hex"));
54 }
55
56 let normalized = hex_key.to_lowercase();
58 let formatted = format!("0x{}", normalized);
59
60 let raw_bytes = hex::decode(&normalized)
62 .map_err(|_| Error::bad_request("Invalid hex in private key"))?;
63
64 if raw_bytes.len() != 32 {
65 return Err(Error::bad_request(
66 "EVM private key must be exactly 32 bytes",
67 ));
68 }
69
70 Ok(Self {
71 formatted_key: formatted,
72 raw_bytes,
73 })
74 }
75
76 pub fn as_hex(&self) -> &str {
78 &self.formatted_key
79 }
80
81 pub fn as_bytes(&self) -> &[u8] {
83 &self.raw_bytes
84 }
85}
86
87impl Debug for EvmPrivateKey {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 f.write_str("EvmPrivateKey(***redacted***)")
90 }
91}
92
93impl Display for EvmPrivateKey {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 f.write_str("EvmPrivateKey(***redacted***)")
96 }
97}
98
99#[derive(Clone, Copy)]
101pub struct VaultAddress {
102 bytes: [u8; 20],
103}
104
105impl VaultAddress {
106 pub fn parse(s: &str) -> Result<Self> {
108 let s = s.trim();
109 let hex_part = s.strip_prefix("0x").unwrap_or(s);
110
111 if hex_part.len() != 40 {
112 return Err(Error::bad_request(
113 "Vault address must be 20 bytes (40 hex chars)",
114 ));
115 }
116
117 let bytes = hex::decode(hex_part)
118 .map_err(|_| Error::bad_request("Invalid hex in vault address"))?;
119
120 if bytes.len() != 20 {
121 return Err(Error::bad_request("Vault address must be exactly 20 bytes"));
122 }
123
124 let mut addr_bytes = [0u8; 20];
125 addr_bytes.copy_from_slice(&bytes);
126
127 Ok(Self { bytes: addr_bytes })
128 }
129
130 pub fn to_hex(&self) -> String {
132 format!("0x{}", hex::encode(self.bytes))
133 }
134
135 pub fn as_bytes(&self) -> &[u8; 20] {
137 &self.bytes
138 }
139}
140
141impl Debug for VaultAddress {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 let hex = self.to_hex();
144 write!(f, "VaultAddress({}...{})", &hex[..6], &hex[hex.len() - 4..])
145 }
146}
147
148impl Display for VaultAddress {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 write!(f, "{}", self.to_hex())
151 }
152}
153
154#[derive(Clone)]
156pub struct Secrets {
157 pub private_key: EvmPrivateKey,
158 pub vault_address: Option<VaultAddress>,
159 pub is_testnet: bool,
160}
161
162impl Debug for Secrets {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 f.debug_struct(stringify!(Secrets))
165 .field("private_key", &self.private_key)
166 .field("vault_address", &self.vault_address)
167 .field("is_testnet", &self.is_testnet)
168 .finish()
169 }
170}
171
172impl Secrets {
173 pub fn from_env() -> Result<Self> {
183 let (private_key_str, vault_env_var, is_testnet) =
185 if let Ok(testnet_pk) = env::var("HYPERLIQUID_TESTNET_PK") {
186 (testnet_pk, "HYPERLIQUID_TESTNET_VAULT", true)
187 } else if let Ok(mainnet_pk) = env::var("HYPERLIQUID_PK") {
188 (mainnet_pk, "HYPERLIQUID_VAULT", false)
189 } else {
190 return Err(Error::bad_request(
191 "Neither HYPERLIQUID_PK nor HYPERLIQUID_TESTNET_PK environment variable is set",
192 ));
193 };
194
195 let private_key = EvmPrivateKey::new(private_key_str)?;
196
197 let vault_address = match env::var(vault_env_var) {
198 Ok(addr_str) if !addr_str.trim().is_empty() => Some(VaultAddress::parse(&addr_str)?),
199 _ => None,
200 };
201
202 Ok(Self {
203 private_key,
204 vault_address,
205 is_testnet,
206 })
207 }
208
209 pub fn from_private_key(
215 private_key_str: &str,
216 vault_address_str: Option<&str>,
217 is_testnet: bool,
218 ) -> Result<Self> {
219 let private_key = EvmPrivateKey::new(private_key_str.to_string())?;
220
221 let vault_address = match vault_address_str {
222 Some(addr_str) if !addr_str.trim().is_empty() => Some(VaultAddress::parse(addr_str)?),
223 _ => None,
224 };
225
226 Ok(Self {
227 private_key,
228 vault_address,
229 is_testnet,
230 })
231 }
232
233 pub fn from_file(path: &Path) -> Result<Self> {
244 let mut content = fs::read_to_string(path).map_err(Error::Io)?;
245
246 let result = Self::from_json(&content);
247
248 content.zeroize();
250
251 result
252 }
253
254 pub fn from_json(json: &str) -> Result<Self> {
256 #[derive(Deserialize)]
257 #[serde(rename_all = "camelCase")]
258 struct RawSecrets {
259 private_key: String,
260 #[serde(default)]
261 vault_address: Option<String>,
262 #[serde(default)]
263 network: Option<String>,
264 }
265
266 let raw: RawSecrets = serde_json::from_str(json)
267 .map_err(|e| Error::bad_request(format!("Invalid JSON: {e}")))?;
268
269 let private_key = EvmPrivateKey::new(raw.private_key)?;
270
271 let vault_address = match raw.vault_address {
272 Some(addr) => Some(VaultAddress::parse(&addr)?),
273 None => None,
274 };
275
276 let is_testnet = matches!(raw.network.as_deref(), Some("testnet" | "test"));
277
278 Ok(Self {
279 private_key,
280 vault_address,
281 is_testnet,
282 })
283 }
284}
285
286pub fn normalize_address(addr: &str) -> Result<String> {
288 let addr = addr.trim();
289 let hex_part = addr
290 .strip_prefix("0x")
291 .or_else(|| addr.strip_prefix("0X"))
292 .unwrap_or(addr);
293
294 if hex_part.len() != 40 {
295 return Err(Error::bad_request(
296 "Address must be 20 bytes (40 hex chars)",
297 ));
298 }
299
300 if !hex_part.chars().all(|c| c.is_ascii_hexdigit()) {
301 return Err(Error::bad_request("Address must be valid hex"));
302 }
303
304 Ok(format!("0x{}", hex_part.to_lowercase()))
305}
306
307#[cfg(test)]
312mod tests {
313 use rstest::rstest;
314
315 use super::*;
316
317 const TEST_PRIVATE_KEY: &str =
318 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
319 const TEST_VAULT_ADDRESS: &str = "0x1234567890123456789012345678901234567890";
320
321 #[rstest]
322 fn test_evm_private_key_creation() {
323 let key = EvmPrivateKey::new(TEST_PRIVATE_KEY.to_string()).unwrap();
324 assert_eq!(key.as_hex(), TEST_PRIVATE_KEY);
325 assert_eq!(key.as_bytes().len(), 32);
326 }
327
328 #[rstest]
329 fn test_evm_private_key_without_0x_prefix() {
330 let key_without_prefix = &TEST_PRIVATE_KEY[2..]; let key = EvmPrivateKey::new(key_without_prefix.to_string()).unwrap();
332 assert_eq!(key.as_hex(), TEST_PRIVATE_KEY);
333 }
334
335 #[rstest]
336 fn test_evm_private_key_invalid_length() {
337 let result = EvmPrivateKey::new("0x123".to_string());
338 assert!(result.is_err());
339 }
340
341 #[rstest]
342 fn test_evm_private_key_invalid_hex() {
343 let result = EvmPrivateKey::new(
344 "0x123g567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(),
345 );
346 assert!(result.is_err());
347 }
348
349 #[rstest]
350 fn test_evm_private_key_debug_redacts() {
351 let key = EvmPrivateKey::new(TEST_PRIVATE_KEY.to_string()).unwrap();
352 let debug_str = format!("{:?}", key);
353 assert_eq!(debug_str, "EvmPrivateKey(***redacted***)");
354 assert!(!debug_str.contains("1234"));
355 }
356
357 #[rstest]
358 fn test_vault_address_creation() {
359 let addr = VaultAddress::parse(TEST_VAULT_ADDRESS).unwrap();
360 assert_eq!(addr.to_hex(), TEST_VAULT_ADDRESS);
361 assert_eq!(addr.as_bytes().len(), 20);
362 }
363
364 #[rstest]
365 fn test_vault_address_without_0x_prefix() {
366 let addr_without_prefix = &TEST_VAULT_ADDRESS[2..]; let addr = VaultAddress::parse(addr_without_prefix).unwrap();
368 assert_eq!(addr.to_hex(), TEST_VAULT_ADDRESS);
369 }
370
371 #[rstest]
372 fn test_vault_address_debug_redacts_middle() {
373 let addr = VaultAddress::parse(TEST_VAULT_ADDRESS).unwrap();
374 let debug_str = format!("{:?}", addr);
375 assert!(debug_str.starts_with("VaultAddress(0x1234"));
376 assert!(debug_str.ends_with("7890)"));
377 assert!(debug_str.contains("..."));
378 }
379
380 #[rstest]
381 fn test_secrets_from_json() {
382 let json = format!(
383 r#"{{
384 "privateKey": "{}",
385 "vaultAddress": "{}",
386 "network": "testnet"
387 }}"#,
388 TEST_PRIVATE_KEY, TEST_VAULT_ADDRESS
389 );
390
391 let secrets = Secrets::from_json(&json).unwrap();
392 assert_eq!(secrets.private_key.as_hex(), TEST_PRIVATE_KEY);
393 assert!(secrets.vault_address.is_some());
394 assert_eq!(secrets.vault_address.unwrap().to_hex(), TEST_VAULT_ADDRESS);
395 assert!(secrets.is_testnet);
396 }
397
398 #[rstest]
399 fn test_secrets_from_json_minimal() {
400 let json = format!(
401 r#"{{
402 "privateKey": "{}"
403 }}"#,
404 TEST_PRIVATE_KEY
405 );
406
407 let secrets = Secrets::from_json(&json).unwrap();
408 assert_eq!(secrets.private_key.as_hex(), TEST_PRIVATE_KEY);
409 assert!(secrets.vault_address.is_none());
410 assert!(!secrets.is_testnet);
411 }
412
413 #[rstest]
414 fn test_normalize_address() {
415 let test_cases = [
416 (
417 TEST_VAULT_ADDRESS,
418 "0x1234567890123456789012345678901234567890",
419 ),
420 (
421 "1234567890123456789012345678901234567890",
422 "0x1234567890123456789012345678901234567890",
423 ),
424 (
425 "0X1234567890123456789012345678901234567890",
426 "0x1234567890123456789012345678901234567890",
427 ),
428 ];
429
430 for (input, expected) in test_cases {
431 assert_eq!(normalize_address(input).unwrap(), expected);
432 }
433 }
434}