Skip to main content

rustrade_execution/client/hyperliquid/
config.rs

1//! Configuration for the Hyperliquid execution client.
2
3use ethers::signers::{LocalWallet, Signer};
4use serde::{Deserialize, Serialize};
5
6/// Configuration for the Hyperliquid execution client.
7///
8/// # Example
9///
10/// ```ignore
11/// use rustrade_execution::client::hyperliquid::config::HyperliquidConfig;
12/// use std::env;
13///
14/// let config = HyperliquidConfig::from_env().expect("HYPERLIQUID_PRIVATE_KEY must be set");
15/// ```
16#[derive(Debug, Clone)]
17pub struct HyperliquidConfig {
18    /// The wallet containing the private key for signing (ethers LocalWallet).
19    pub wallet: LocalWallet,
20    /// Whether to use testnet (true) or mainnet (false).
21    pub testnet: bool,
22}
23
24impl HyperliquidConfig {
25    /// Create a new config with the given wallet and network selection.
26    pub fn new(wallet: LocalWallet, testnet: bool) -> Self {
27        Self { wallet, testnet }
28    }
29
30    /// Create a config from environment variables.
31    ///
32    /// Reads:
33    /// - `HYPERLIQUID_PRIVATE_KEY`: Hex-encoded private key (with or without 0x prefix)
34    /// - `HYPERLIQUID_TESTNET`: Optional, set to "true" for testnet (default: mainnet)
35    ///
36    /// # Errors
37    ///
38    /// Returns an error if `HYPERLIQUID_PRIVATE_KEY` is not set or invalid.
39    pub fn from_env() -> Result<Self, ConfigError> {
40        let private_key =
41            std::env::var("HYPERLIQUID_PRIVATE_KEY").map_err(|_| ConfigError::MissingPrivateKey)?;
42
43        let testnet = std::env::var("HYPERLIQUID_TESTNET")
44            .map(|v| v.eq_ignore_ascii_case("true") || v == "1")
45            .unwrap_or(false);
46
47        Self::from_private_key(&private_key, testnet)
48    }
49
50    /// Create a config from a hex-encoded private key string.
51    ///
52    /// The private key can have an optional "0x" prefix.
53    pub fn from_private_key(private_key: &str, testnet: bool) -> Result<Self, ConfigError> {
54        let key = private_key.strip_prefix("0x").unwrap_or(private_key);
55
56        let wallet: LocalWallet = key
57            .parse()
58            .map_err(|e| ConfigError::InvalidPrivateKey(format!("{e}")))?;
59
60        Ok(Self { wallet, testnet })
61    }
62
63    /// Returns the wallet address as a hex string (0x-prefixed).
64    pub fn wallet_address_hex(&self) -> String {
65        format!("{:#x}", self.wallet.address())
66    }
67}
68
69/// Serializable version of HyperliquidConfig for config files.
70///
71/// Does NOT include the private key for security reasons.
72/// Use `HyperliquidConfig::from_env()` to load credentials.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct HyperliquidConfigFile {
75    /// Whether to use testnet (true) or mainnet (false).
76    #[serde(default)]
77    pub testnet: bool,
78}
79
80/// Errors that can occur when creating a HyperliquidConfig.
81#[derive(Debug, thiserror::Error)]
82pub enum ConfigError {
83    #[error("HYPERLIQUID_PRIVATE_KEY environment variable not set")]
84    MissingPrivateKey,
85
86    #[error("Invalid private key: {0}")]
87    InvalidPrivateKey(String),
88}
89
90#[cfg(test)]
91#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
92mod tests {
93    use super::*;
94
95    #[test]
96    fn test_from_private_key_with_prefix() {
97        let key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
98        let config = HyperliquidConfig::from_private_key(key, false).unwrap();
99        assert!(!config.testnet);
100        assert!(config.wallet_address_hex().starts_with("0x"));
101    }
102
103    #[test]
104    fn test_from_private_key_without_prefix() {
105        let key = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
106        let config = HyperliquidConfig::from_private_key(key, true).unwrap();
107        assert!(config.testnet);
108    }
109
110    #[test]
111    fn test_invalid_private_key() {
112        let result = HyperliquidConfig::from_private_key("invalid", false);
113        assert!(result.is_err());
114    }
115}