nautilus_hyperliquid/common/
credential.rs1#![allow(unused_assignments)] use std::fmt::{Debug, Display};
19
20use nautilus_core::{
21 env::{get_or_env_var, get_or_env_var_opt},
22 hex,
23 string::secret::REDACTED,
24};
25use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
26
27use crate::{
28 common::enums::HyperliquidEnvironment,
29 http::error::{Error, Result},
30};
31
32#[must_use]
37pub fn credential_env_vars(environment: HyperliquidEnvironment) -> (&'static str, &'static str) {
38 match environment {
39 HyperliquidEnvironment::Testnet => ("HYPERLIQUID_TESTNET_PK", "HYPERLIQUID_TESTNET_VAULT"),
40 HyperliquidEnvironment::Mainnet => ("HYPERLIQUID_PK", "HYPERLIQUID_VAULT"),
41 }
42}
43
44#[derive(Clone, Zeroize, ZeroizeOnDrop)]
46pub struct EvmPrivateKey {
47 formatted_key: String,
48 raw_bytes: Vec<u8>,
49}
50
51impl EvmPrivateKey {
52 pub fn new(key: &str) -> Result<Self> {
54 let key = Zeroizing::new(key.trim().to_string());
55 let hex_key = key.strip_prefix("0x").unwrap_or(&key);
56
57 if hex_key.len() != 64 {
59 return Err(Error::bad_request(
60 "EVM private key must be 32 bytes (64 hex chars)",
61 ));
62 }
63
64 if !hex_key.chars().all(|c| c.is_ascii_hexdigit()) {
65 return Err(Error::bad_request("EVM private key must be valid hex"));
66 }
67
68 let normalized = Zeroizing::new(hex_key.to_lowercase());
70 let formatted = format!("0x{}", normalized.as_str());
71
72 let raw_bytes = hex::decode(&normalized)
74 .map_err(|_| Error::bad_request("Invalid hex in private key"))?;
75
76 if raw_bytes.len() != 32 {
77 return Err(Error::bad_request(
78 "EVM private key must be exactly 32 bytes",
79 ));
80 }
81
82 Ok(Self {
83 formatted_key: formatted,
84 raw_bytes,
85 })
86 }
87
88 pub fn as_hex(&self) -> &str {
90 &self.formatted_key
91 }
92
93 pub fn as_bytes(&self) -> &[u8] {
95 &self.raw_bytes
96 }
97}
98
99impl Debug for EvmPrivateKey {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 write!(f, "EvmPrivateKey({REDACTED})")
102 }
103}
104
105impl Display for EvmPrivateKey {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 write!(f, "EvmPrivateKey({REDACTED})")
108 }
109}
110
111#[derive(Clone, Copy)]
113pub struct VaultAddress {
114 bytes: [u8; 20],
115}
116
117impl VaultAddress {
118 pub fn parse(s: &str) -> Result<Self> {
120 let s = s.trim();
121 let hex_part = s.strip_prefix("0x").unwrap_or(s);
122
123 let bytes: [u8; 20] = hex::decode_array(hex_part)
124 .map_err(|_| Error::bad_request("Vault address must be 20 bytes of valid hex"))?;
125
126 Ok(Self { bytes })
127 }
128
129 pub fn to_hex(&self) -> String {
131 hex::encode_prefixed(self.bytes)
132 }
133
134 pub fn as_bytes(&self) -> &[u8; 20] {
136 &self.bytes
137 }
138}
139
140impl Debug for VaultAddress {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 let hex = self.to_hex();
143 write!(f, "VaultAddress({}...{})", &hex[..6], &hex[hex.len() - 4..])
144 }
145}
146
147impl Display for VaultAddress {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 write!(f, "{}", self.to_hex())
150 }
151}
152
153#[derive(Clone)]
155pub struct Secrets {
156 pub private_key: EvmPrivateKey,
157 pub vault_address: Option<VaultAddress>,
158 pub environment: HyperliquidEnvironment,
159}
160
161impl Secrets {
162 #[must_use]
164 pub fn is_testnet(&self) -> bool {
165 self.environment == HyperliquidEnvironment::Testnet
166 }
167}
168
169impl Debug for Secrets {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 f.debug_struct(stringify!(Secrets))
172 .field("private_key", &self.private_key)
173 .field("vault_address", &self.vault_address)
174 .field("environment", &self.environment)
175 .finish()
176 }
177}
178
179impl Secrets {
180 #[must_use]
182 pub fn env_vars(environment: HyperliquidEnvironment) -> (&'static str, &'static str) {
183 credential_env_vars(environment)
184 }
185
186 pub fn resolve(
191 private_key: Option<&str>,
192 vault_address: Option<&str>,
193 environment: HyperliquidEnvironment,
194 ) -> Result<Self> {
195 let (pk_env_var, vault_env_var) = credential_env_vars(environment);
196
197 let pk_str = Zeroizing::new(
198 get_or_env_var(
199 private_key
200 .filter(|s| !s.trim().is_empty())
201 .map(String::from),
202 pk_env_var,
203 )
204 .map_err(|_| {
205 Error::bad_request(format!("{pk_env_var} environment variable is not set"))
206 })?,
207 );
208
209 let vault_str = get_or_env_var_opt(
210 vault_address
211 .filter(|s| !s.trim().is_empty())
212 .map(String::from),
213 vault_env_var,
214 )
215 .filter(|s| !s.trim().is_empty());
216
217 let private_key = EvmPrivateKey::new(&pk_str)?;
218 let vault_address = match vault_str {
219 Some(addr) => Some(VaultAddress::parse(&addr)?),
220 None => None,
221 };
222
223 Ok(Self {
224 private_key,
225 vault_address,
226 environment,
227 })
228 }
229
230 pub fn from_env(environment: HyperliquidEnvironment) -> Result<Self> {
238 Self::resolve(None, None, environment)
239 }
240
241 pub fn from_private_key(
247 private_key_str: &str,
248 vault_address_str: Option<&str>,
249 environment: HyperliquidEnvironment,
250 ) -> Result<Self> {
251 let private_key = EvmPrivateKey::new(private_key_str)?;
252
253 let vault_address = match vault_address_str {
254 Some(addr_str) if !addr_str.trim().is_empty() => Some(VaultAddress::parse(addr_str)?),
255 _ => None,
256 };
257
258 Ok(Self {
259 private_key,
260 vault_address,
261 environment,
262 })
263 }
264}
265
266pub fn normalize_address(addr: &str) -> Result<String> {
268 let addr = addr.trim();
269 let hex_part = addr
270 .strip_prefix("0x")
271 .or_else(|| addr.strip_prefix("0X"))
272 .unwrap_or(addr);
273
274 if hex_part.len() != 40 {
275 return Err(Error::bad_request(
276 "Address must be 20 bytes (40 hex chars)",
277 ));
278 }
279
280 if !hex_part.chars().all(|c| c.is_ascii_hexdigit()) {
281 return Err(Error::bad_request("Address must be valid hex"));
282 }
283
284 Ok(format!("0x{}", hex_part.to_lowercase()))
285}
286
287#[cfg(test)]
288mod tests {
289 use rstest::rstest;
290
291 use super::*;
292
293 const TEST_PRIVATE_KEY: &str =
294 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
295 const TEST_VAULT_ADDRESS: &str = "0x1234567890123456789012345678901234567890";
296
297 #[rstest]
298 fn test_evm_private_key_creation() {
299 let key = EvmPrivateKey::new(TEST_PRIVATE_KEY).unwrap();
300 assert_eq!(key.as_hex(), TEST_PRIVATE_KEY);
301 assert_eq!(key.as_bytes().len(), 32);
302 }
303
304 #[rstest]
305 fn test_evm_private_key_without_0x_prefix() {
306 let key_without_prefix = &TEST_PRIVATE_KEY[2..]; let key = EvmPrivateKey::new(key_without_prefix).unwrap();
308 assert_eq!(key.as_hex(), TEST_PRIVATE_KEY);
309 }
310
311 #[rstest]
312 fn test_evm_private_key_invalid_length() {
313 let result = EvmPrivateKey::new("0x123");
314 assert!(result.is_err());
315 }
316
317 #[rstest]
318 fn test_evm_private_key_invalid_hex() {
319 let result = EvmPrivateKey::new(
320 "0x123g567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
321 );
322 assert!(result.is_err());
323 }
324
325 #[rstest]
326 fn test_evm_private_key_debug_redacts() {
327 let key = EvmPrivateKey::new(TEST_PRIVATE_KEY).unwrap();
328 let debug_str = format!("{key:?}");
329 assert_eq!(debug_str, format!("EvmPrivateKey({REDACTED})"));
330 assert!(!debug_str.contains("1234"));
331 }
332
333 #[rstest]
334 fn test_vault_address_creation() {
335 let addr = VaultAddress::parse(TEST_VAULT_ADDRESS).unwrap();
336 assert_eq!(addr.to_hex(), TEST_VAULT_ADDRESS);
337 assert_eq!(addr.as_bytes().len(), 20);
338 }
339
340 #[rstest]
341 fn test_vault_address_without_0x_prefix() {
342 let addr_without_prefix = &TEST_VAULT_ADDRESS[2..]; let addr = VaultAddress::parse(addr_without_prefix).unwrap();
344 assert_eq!(addr.to_hex(), TEST_VAULT_ADDRESS);
345 }
346
347 #[rstest]
348 fn test_vault_address_debug_redacts_middle() {
349 let addr = VaultAddress::parse(TEST_VAULT_ADDRESS).unwrap();
350 let debug_str = format!("{addr:?}");
351 assert!(debug_str.starts_with("VaultAddress(0x1234"));
352 assert!(debug_str.ends_with("7890)"));
353 assert!(debug_str.contains("..."));
354 }
355
356 #[rstest]
357 fn test_normalize_address() {
358 let test_cases = [
359 (
360 TEST_VAULT_ADDRESS,
361 "0x1234567890123456789012345678901234567890",
362 ),
363 (
364 "1234567890123456789012345678901234567890",
365 "0x1234567890123456789012345678901234567890",
366 ),
367 (
368 "0X1234567890123456789012345678901234567890",
369 "0x1234567890123456789012345678901234567890",
370 ),
371 ];
372
373 for (input, expected) in test_cases {
374 assert_eq!(normalize_address(input).unwrap(), expected);
375 }
376 }
377}