#![allow(unused_assignments)]
use std::{
fmt::{Debug, Display},
fs,
path::Path,
};
use nautilus_core::{
env::{get_or_env_var, get_or_env_var_opt},
hex,
};
use serde::Deserialize;
use zeroize::{Zeroize, ZeroizeOnDrop};
use crate::http::error::{Error, Result};
#[must_use]
pub fn credential_env_vars(is_testnet: bool) -> (&'static str, &'static str) {
if is_testnet {
("HYPERLIQUID_TESTNET_PK", "HYPERLIQUID_TESTNET_VAULT")
} else {
("HYPERLIQUID_PK", "HYPERLIQUID_VAULT")
}
}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct EvmPrivateKey {
formatted_key: String,
raw_bytes: Vec<u8>,
}
impl EvmPrivateKey {
pub fn new(key: &str) -> Result<Self> {
let key = key.trim().to_string();
let hex_key = key.strip_prefix("0x").unwrap_or(&key);
if hex_key.len() != 64 {
return Err(Error::bad_request(
"EVM private key must be 32 bytes (64 hex chars)",
));
}
if !hex_key.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(Error::bad_request("EVM private key must be valid hex"));
}
let normalized = hex_key.to_lowercase();
let formatted = format!("0x{normalized}");
let raw_bytes = hex::decode(&normalized)
.map_err(|_| Error::bad_request("Invalid hex in private key"))?;
if raw_bytes.len() != 32 {
return Err(Error::bad_request(
"EVM private key must be exactly 32 bytes",
));
}
Ok(Self {
formatted_key: formatted,
raw_bytes,
})
}
pub fn as_hex(&self) -> &str {
&self.formatted_key
}
pub fn as_bytes(&self) -> &[u8] {
&self.raw_bytes
}
}
impl Debug for EvmPrivateKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("EvmPrivateKey(***redacted***)")
}
}
impl Display for EvmPrivateKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("EvmPrivateKey(***redacted***)")
}
}
#[derive(Clone, Copy)]
pub struct VaultAddress {
bytes: [u8; 20],
}
impl VaultAddress {
pub fn parse(s: &str) -> Result<Self> {
let s = s.trim();
let hex_part = s.strip_prefix("0x").unwrap_or(s);
let bytes: [u8; 20] = hex::decode_array(hex_part)
.map_err(|_| Error::bad_request("Vault address must be 20 bytes of valid hex"))?;
Ok(Self { bytes })
}
pub fn to_hex(&self) -> String {
hex::encode_prefixed(self.bytes)
}
pub fn as_bytes(&self) -> &[u8; 20] {
&self.bytes
}
}
impl Debug for VaultAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let hex = self.to_hex();
write!(f, "VaultAddress({}...{})", &hex[..6], &hex[hex.len() - 4..])
}
}
impl Display for VaultAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_hex())
}
}
#[derive(Clone)]
pub struct Secrets {
pub private_key: EvmPrivateKey,
pub vault_address: Option<VaultAddress>,
pub is_testnet: bool,
}
impl Debug for Secrets {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(stringify!(Secrets))
.field("private_key", &self.private_key)
.field("vault_address", &self.vault_address)
.field("is_testnet", &self.is_testnet)
.finish()
}
}
impl Secrets {
#[must_use]
pub fn env_vars(is_testnet: bool) -> (&'static str, &'static str) {
credential_env_vars(is_testnet)
}
pub fn resolve(
private_key: Option<&str>,
vault_address: Option<&str>,
is_testnet: bool,
) -> Result<Self> {
let (pk_env_var, vault_env_var) = credential_env_vars(is_testnet);
let pk_str = get_or_env_var(
private_key
.filter(|s| !s.trim().is_empty())
.map(String::from),
pk_env_var,
)
.map_err(|_| Error::bad_request(format!("{pk_env_var} environment variable is not set")))?;
let vault_str = get_or_env_var_opt(
vault_address
.filter(|s| !s.trim().is_empty())
.map(String::from),
vault_env_var,
)
.filter(|s| !s.trim().is_empty());
let private_key = EvmPrivateKey::new(&pk_str)?;
let vault_address = match vault_str {
Some(addr) => Some(VaultAddress::parse(&addr)?),
None => None,
};
Ok(Self {
private_key,
vault_address,
is_testnet,
})
}
pub fn from_env(is_testnet: bool) -> Result<Self> {
Self::resolve(None, None, is_testnet)
}
pub fn from_private_key(
private_key_str: &str,
vault_address_str: Option<&str>,
is_testnet: bool,
) -> Result<Self> {
let private_key = EvmPrivateKey::new(private_key_str)?;
let vault_address = match vault_address_str {
Some(addr_str) if !addr_str.trim().is_empty() => Some(VaultAddress::parse(addr_str)?),
_ => None,
};
Ok(Self {
private_key,
vault_address,
is_testnet,
})
}
pub fn from_file(path: &Path) -> Result<Self> {
let mut content = fs::read_to_string(path).map_err(Error::Io)?;
let result = Self::from_json(&content);
content.zeroize();
result
}
pub fn from_json(json: &str) -> Result<Self> {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct RawSecrets {
private_key: String,
#[serde(default)]
vault_address: Option<String>,
#[serde(default)]
network: Option<String>,
}
let raw: RawSecrets = serde_json::from_str(json)
.map_err(|e| Error::bad_request(format!("Invalid JSON: {e}")))?;
let private_key = EvmPrivateKey::new(&raw.private_key)?;
let vault_address = match raw.vault_address {
Some(addr) => Some(VaultAddress::parse(&addr)?),
None => None,
};
let is_testnet = matches!(raw.network.as_deref(), Some("testnet" | "test"));
Ok(Self {
private_key,
vault_address,
is_testnet,
})
}
}
pub fn normalize_address(addr: &str) -> Result<String> {
let addr = addr.trim();
let hex_part = addr
.strip_prefix("0x")
.or_else(|| addr.strip_prefix("0X"))
.unwrap_or(addr);
if hex_part.len() != 40 {
return Err(Error::bad_request(
"Address must be 20 bytes (40 hex chars)",
));
}
if !hex_part.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(Error::bad_request("Address must be valid hex"));
}
Ok(format!("0x{}", hex_part.to_lowercase()))
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
const TEST_PRIVATE_KEY: &str =
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
const TEST_VAULT_ADDRESS: &str = "0x1234567890123456789012345678901234567890";
#[rstest]
fn test_evm_private_key_creation() {
let key = EvmPrivateKey::new(TEST_PRIVATE_KEY).unwrap();
assert_eq!(key.as_hex(), TEST_PRIVATE_KEY);
assert_eq!(key.as_bytes().len(), 32);
}
#[rstest]
fn test_evm_private_key_without_0x_prefix() {
let key_without_prefix = &TEST_PRIVATE_KEY[2..]; let key = EvmPrivateKey::new(key_without_prefix).unwrap();
assert_eq!(key.as_hex(), TEST_PRIVATE_KEY);
}
#[rstest]
fn test_evm_private_key_invalid_length() {
let result = EvmPrivateKey::new("0x123");
assert!(result.is_err());
}
#[rstest]
fn test_evm_private_key_invalid_hex() {
let result = EvmPrivateKey::new(
"0x123g567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
);
assert!(result.is_err());
}
#[rstest]
fn test_evm_private_key_debug_redacts() {
let key = EvmPrivateKey::new(TEST_PRIVATE_KEY).unwrap();
let debug_str = format!("{key:?}");
assert_eq!(debug_str, "EvmPrivateKey(***redacted***)");
assert!(!debug_str.contains("1234"));
}
#[rstest]
fn test_vault_address_creation() {
let addr = VaultAddress::parse(TEST_VAULT_ADDRESS).unwrap();
assert_eq!(addr.to_hex(), TEST_VAULT_ADDRESS);
assert_eq!(addr.as_bytes().len(), 20);
}
#[rstest]
fn test_vault_address_without_0x_prefix() {
let addr_without_prefix = &TEST_VAULT_ADDRESS[2..]; let addr = VaultAddress::parse(addr_without_prefix).unwrap();
assert_eq!(addr.to_hex(), TEST_VAULT_ADDRESS);
}
#[rstest]
fn test_vault_address_debug_redacts_middle() {
let addr = VaultAddress::parse(TEST_VAULT_ADDRESS).unwrap();
let debug_str = format!("{addr:?}");
assert!(debug_str.starts_with("VaultAddress(0x1234"));
assert!(debug_str.ends_with("7890)"));
assert!(debug_str.contains("..."));
}
#[rstest]
fn test_secrets_from_json() {
let json = format!(
r#"{{
"privateKey": "{TEST_PRIVATE_KEY}",
"vaultAddress": "{TEST_VAULT_ADDRESS}",
"network": "testnet"
}}"#
);
let secrets = Secrets::from_json(&json).unwrap();
assert_eq!(secrets.private_key.as_hex(), TEST_PRIVATE_KEY);
assert!(secrets.vault_address.is_some());
assert_eq!(secrets.vault_address.unwrap().to_hex(), TEST_VAULT_ADDRESS);
assert!(secrets.is_testnet);
}
#[rstest]
fn test_secrets_from_json_minimal() {
let json = format!(
r#"{{
"privateKey": "{TEST_PRIVATE_KEY}"
}}"#
);
let secrets = Secrets::from_json(&json).unwrap();
assert_eq!(secrets.private_key.as_hex(), TEST_PRIVATE_KEY);
assert!(secrets.vault_address.is_none());
assert!(!secrets.is_testnet);
}
#[rstest]
fn test_normalize_address() {
let test_cases = [
(
TEST_VAULT_ADDRESS,
"0x1234567890123456789012345678901234567890",
),
(
"1234567890123456789012345678901234567890",
"0x1234567890123456789012345678901234567890",
),
(
"0X1234567890123456789012345678901234567890",
"0x1234567890123456789012345678901234567890",
),
];
for (input, expected) in test_cases {
assert_eq!(normalize_address(input).unwrap(), expected);
}
}
}