rustrade_execution/client/hyperliquid/
config.rs1use ethers::signers::{LocalWallet, Signer};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone)]
17pub struct HyperliquidConfig {
18 pub wallet: LocalWallet,
20 pub testnet: bool,
22}
23
24impl HyperliquidConfig {
25 pub fn new(wallet: LocalWallet, testnet: bool) -> Self {
28 Self { wallet, testnet }
29 }
30
31 pub fn from_env() -> Result<Self, HyperliquidConfigError> {
47 let private_key = match std::env::var("HYPERLIQUID_PRIVATE_KEY") {
48 Ok(value) => value,
49 Err(std::env::VarError::NotPresent) => {
50 return Err(HyperliquidConfigError::MissingPrivateKey);
51 }
52 Err(std::env::VarError::NotUnicode(_)) => {
53 return Err(HyperliquidConfigError::InvalidPrivateKeyVar);
54 }
55 };
56
57 let testnet = match std::env::var("HYPERLIQUID_TESTNET") {
58 Ok(value) => crate::parse_env_bool(&value)
59 .ok_or(HyperliquidConfigError::InvalidTestnet(value))?,
60 Err(std::env::VarError::NotPresent) => true,
61 Err(std::env::VarError::NotUnicode(value)) => {
64 return Err(HyperliquidConfigError::InvalidTestnet(
65 value.to_string_lossy().into_owned(),
66 ));
67 }
68 };
69
70 Self::from_private_key(&private_key, testnet)
71 }
72
73 pub fn from_private_key(
77 private_key: &str,
78 testnet: bool,
79 ) -> Result<Self, HyperliquidConfigError> {
80 let key = private_key.strip_prefix("0x").unwrap_or(private_key);
81
82 let wallet: LocalWallet = key
83 .parse()
84 .map_err(|e| HyperliquidConfigError::InvalidPrivateKey(format!("{e}")))?;
85
86 Ok(Self { wallet, testnet })
87 }
88
89 pub fn wallet_address_hex(&self) -> String {
91 format!("{:#x}", self.wallet.address())
92 }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct HyperliquidConfigFile {
101 #[serde(default = "default_testnet")]
106 pub testnet: bool,
107}
108
109fn default_testnet() -> bool {
115 true
116}
117
118#[derive(Debug, PartialEq, thiserror::Error)]
120pub enum HyperliquidConfigError {
121 #[error("HYPERLIQUID_PRIVATE_KEY environment variable not set")]
122 MissingPrivateKey,
123
124 #[error("HYPERLIQUID_PRIVATE_KEY environment variable is not valid UTF-8")]
128 InvalidPrivateKeyVar,
129
130 #[error("Invalid private key: {0}")]
131 InvalidPrivateKey(String),
132
133 #[error("HYPERLIQUID_TESTNET must be true or false, got {0}")]
134 InvalidTestnet(String),
135}
136
137#[cfg(test)]
138#[allow(clippy::unwrap_used)] mod tests {
140 use super::*;
141
142 #[test]
143 fn test_from_private_key_with_prefix() {
144 let key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
145 let config = HyperliquidConfig::from_private_key(key, false).unwrap();
146 assert!(!config.testnet);
147 assert!(config.wallet_address_hex().starts_with("0x"));
148 }
149
150 #[test]
151 fn test_from_private_key_without_prefix() {
152 let key = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
153 let config = HyperliquidConfig::from_private_key(key, true).unwrap();
154 assert!(config.testnet);
155 }
156
157 #[test]
158 fn test_invalid_private_key() {
159 let result = HyperliquidConfig::from_private_key("invalid", false);
160 assert!(result.is_err());
161 }
162
163 const TEST_KEY: &str = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
165
166 #[test]
167 #[serial_test::serial]
168 fn test_from_env_defaults_to_testnet() {
169 temp_env::with_vars(
170 [
171 ("HYPERLIQUID_PRIVATE_KEY", Some(TEST_KEY)),
172 ("HYPERLIQUID_TESTNET", None),
173 ],
174 || {
175 let cfg = HyperliquidConfig::from_env().unwrap();
176 assert!(
177 cfg.testnet,
178 "absent toggle must default to the safe testnet"
179 );
180 },
181 );
182 }
183
184 #[test]
185 #[serial_test::serial]
186 fn test_from_env_accepts_explicit_mainnet() {
187 temp_env::with_vars(
188 [
189 ("HYPERLIQUID_PRIVATE_KEY", Some(TEST_KEY)),
190 ("HYPERLIQUID_TESTNET", Some("false")),
191 ],
192 || {
193 let cfg = HyperliquidConfig::from_env().unwrap();
194 assert!(!cfg.testnet);
195 },
196 );
197 }
198
199 #[test]
200 #[serial_test::serial]
201 fn test_from_env_rejects_invalid_testnet() {
202 temp_env::with_vars(
203 [
204 ("HYPERLIQUID_PRIVATE_KEY", Some(TEST_KEY)),
205 ("HYPERLIQUID_TESTNET", Some("maybe")),
206 ],
207 || {
208 let err = HyperliquidConfig::from_env().unwrap_err();
209 assert!(
210 matches!(err, HyperliquidConfigError::InvalidTestnet(value) if value == "maybe")
211 );
212 },
213 );
214 }
215
216 #[test]
217 #[serial_test::serial]
218 fn test_from_env_drops_numeric_one_special_case() {
219 temp_env::with_vars(
222 [
223 ("HYPERLIQUID_PRIVATE_KEY", Some(TEST_KEY)),
224 ("HYPERLIQUID_TESTNET", Some("1")),
225 ],
226 || {
227 let err = HyperliquidConfig::from_env().unwrap_err();
228 assert!(
229 matches!(err, HyperliquidConfigError::InvalidTestnet(value) if value == "1")
230 );
231 },
232 );
233 }
234
235 #[test]
236 #[serial_test::serial]
237 fn test_from_env_requires_private_key() {
238 temp_env::with_vars(
239 [
240 ("HYPERLIQUID_PRIVATE_KEY", None),
241 ("HYPERLIQUID_TESTNET", Some("true")),
242 ],
243 || {
244 let err = HyperliquidConfig::from_env().unwrap_err();
245 assert!(matches!(err, HyperliquidConfigError::MissingPrivateKey));
246 },
247 );
248 }
249
250 #[test]
251 fn test_config_file_absent_testnet_defaults_to_testnet() {
252 let file: HyperliquidConfigFile = serde_json::from_str("{}").unwrap();
253 assert!(
254 file.testnet,
255 "absent `testnet` field must default to safe testnet"
256 );
257 }
258}