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(is_testnet: bool) -> Result<Self> {
181 let (pk_env_var, vault_env_var) = if is_testnet {
182 ("HYPERLIQUID_TESTNET_PK", "HYPERLIQUID_TESTNET_VAULT")
183 } else {
184 ("HYPERLIQUID_PK", "HYPERLIQUID_VAULT")
185 };
186
187 let private_key_str = env::var(pk_env_var).map_err(|_| {
188 Error::bad_request(format!("{pk_env_var} environment variable is not set"))
189 })?;
190
191 let private_key = EvmPrivateKey::new(private_key_str)?;
192
193 let vault_address = match env::var(vault_env_var) {
194 Ok(addr_str) if !addr_str.trim().is_empty() => Some(VaultAddress::parse(&addr_str)?),
195 _ => None,
196 };
197
198 Ok(Self {
199 private_key,
200 vault_address,
201 is_testnet,
202 })
203 }
204
205 pub fn from_private_key(
211 private_key_str: &str,
212 vault_address_str: Option<&str>,
213 is_testnet: bool,
214 ) -> Result<Self> {
215 let private_key = EvmPrivateKey::new(private_key_str.to_string())?;
216
217 let vault_address = match vault_address_str {
218 Some(addr_str) if !addr_str.trim().is_empty() => Some(VaultAddress::parse(addr_str)?),
219 _ => None,
220 };
221
222 Ok(Self {
223 private_key,
224 vault_address,
225 is_testnet,
226 })
227 }
228
229 pub fn from_file(path: &Path) -> Result<Self> {
240 let mut content = fs::read_to_string(path).map_err(Error::Io)?;
241
242 let result = Self::from_json(&content);
243
244 content.zeroize();
246
247 result
248 }
249
250 pub fn from_json(json: &str) -> Result<Self> {
252 #[derive(Deserialize)]
253 #[serde(rename_all = "camelCase")]
254 struct RawSecrets {
255 private_key: String,
256 #[serde(default)]
257 vault_address: Option<String>,
258 #[serde(default)]
259 network: Option<String>,
260 }
261
262 let raw: RawSecrets = serde_json::from_str(json)
263 .map_err(|e| Error::bad_request(format!("Invalid JSON: {e}")))?;
264
265 let private_key = EvmPrivateKey::new(raw.private_key)?;
266
267 let vault_address = match raw.vault_address {
268 Some(addr) => Some(VaultAddress::parse(&addr)?),
269 None => None,
270 };
271
272 let is_testnet = matches!(raw.network.as_deref(), Some("testnet" | "test"));
273
274 Ok(Self {
275 private_key,
276 vault_address,
277 is_testnet,
278 })
279 }
280}
281
282pub fn normalize_address(addr: &str) -> Result<String> {
284 let addr = addr.trim();
285 let hex_part = addr
286 .strip_prefix("0x")
287 .or_else(|| addr.strip_prefix("0X"))
288 .unwrap_or(addr);
289
290 if hex_part.len() != 40 {
291 return Err(Error::bad_request(
292 "Address must be 20 bytes (40 hex chars)",
293 ));
294 }
295
296 if !hex_part.chars().all(|c| c.is_ascii_hexdigit()) {
297 return Err(Error::bad_request("Address must be valid hex"));
298 }
299
300 Ok(format!("0x{}", hex_part.to_lowercase()))
301}
302
303#[cfg(test)]
304mod tests {
305 use rstest::rstest;
306
307 use super::*;
308
309 const TEST_PRIVATE_KEY: &str =
310 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
311 const TEST_VAULT_ADDRESS: &str = "0x1234567890123456789012345678901234567890";
312
313 #[rstest]
314 fn test_evm_private_key_creation() {
315 let key = EvmPrivateKey::new(TEST_PRIVATE_KEY.to_string()).unwrap();
316 assert_eq!(key.as_hex(), TEST_PRIVATE_KEY);
317 assert_eq!(key.as_bytes().len(), 32);
318 }
319
320 #[rstest]
321 fn test_evm_private_key_without_0x_prefix() {
322 let key_without_prefix = &TEST_PRIVATE_KEY[2..]; let key = EvmPrivateKey::new(key_without_prefix.to_string()).unwrap();
324 assert_eq!(key.as_hex(), TEST_PRIVATE_KEY);
325 }
326
327 #[rstest]
328 fn test_evm_private_key_invalid_length() {
329 let result = EvmPrivateKey::new("0x123".to_string());
330 assert!(result.is_err());
331 }
332
333 #[rstest]
334 fn test_evm_private_key_invalid_hex() {
335 let result = EvmPrivateKey::new(
336 "0x123g567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(),
337 );
338 assert!(result.is_err());
339 }
340
341 #[rstest]
342 fn test_evm_private_key_debug_redacts() {
343 let key = EvmPrivateKey::new(TEST_PRIVATE_KEY.to_string()).unwrap();
344 let debug_str = format!("{key:?}");
345 assert_eq!(debug_str, "EvmPrivateKey(***redacted***)");
346 assert!(!debug_str.contains("1234"));
347 }
348
349 #[rstest]
350 fn test_vault_address_creation() {
351 let addr = VaultAddress::parse(TEST_VAULT_ADDRESS).unwrap();
352 assert_eq!(addr.to_hex(), TEST_VAULT_ADDRESS);
353 assert_eq!(addr.as_bytes().len(), 20);
354 }
355
356 #[rstest]
357 fn test_vault_address_without_0x_prefix() {
358 let addr_without_prefix = &TEST_VAULT_ADDRESS[2..]; let addr = VaultAddress::parse(addr_without_prefix).unwrap();
360 assert_eq!(addr.to_hex(), TEST_VAULT_ADDRESS);
361 }
362
363 #[rstest]
364 fn test_vault_address_debug_redacts_middle() {
365 let addr = VaultAddress::parse(TEST_VAULT_ADDRESS).unwrap();
366 let debug_str = format!("{addr:?}");
367 assert!(debug_str.starts_with("VaultAddress(0x1234"));
368 assert!(debug_str.ends_with("7890)"));
369 assert!(debug_str.contains("..."));
370 }
371
372 #[rstest]
373 fn test_secrets_from_json() {
374 let json = format!(
375 r#"{{
376 "privateKey": "{TEST_PRIVATE_KEY}",
377 "vaultAddress": "{TEST_VAULT_ADDRESS}",
378 "network": "testnet"
379 }}"#
380 );
381
382 let secrets = Secrets::from_json(&json).unwrap();
383 assert_eq!(secrets.private_key.as_hex(), TEST_PRIVATE_KEY);
384 assert!(secrets.vault_address.is_some());
385 assert_eq!(secrets.vault_address.unwrap().to_hex(), TEST_VAULT_ADDRESS);
386 assert!(secrets.is_testnet);
387 }
388
389 #[rstest]
390 fn test_secrets_from_json_minimal() {
391 let json = format!(
392 r#"{{
393 "privateKey": "{TEST_PRIVATE_KEY}"
394 }}"#
395 );
396
397 let secrets = Secrets::from_json(&json).unwrap();
398 assert_eq!(secrets.private_key.as_hex(), TEST_PRIVATE_KEY);
399 assert!(secrets.vault_address.is_none());
400 assert!(!secrets.is_testnet);
401 }
402
403 #[rstest]
404 fn test_normalize_address() {
405 let test_cases = [
406 (
407 TEST_VAULT_ADDRESS,
408 "0x1234567890123456789012345678901234567890",
409 ),
410 (
411 "1234567890123456789012345678901234567890",
412 "0x1234567890123456789012345678901234567890",
413 ),
414 (
415 "0X1234567890123456789012345678901234567890",
416 "0x1234567890123456789012345678901234567890",
417 ),
418 ];
419
420 for (input, expected) in test_cases {
421 assert_eq!(normalize_address(input).unwrap(), expected);
422 }
423 }
424}