1use std::env;
19
20use alloy_primitives::Address;
21
22use crate::{
23 common::{
24 credential::{EvmPrivateKey, VaultAddress, credential_env_vars},
25 enums::HyperliquidEnvironment,
26 },
27 http::error::{Error, Result},
28 signing::HyperliquidEip712Signer,
29};
30
31pub fn resolve_execution_account_address(
43 private_key: Option<&str>,
44 vault_address: Option<&str>,
45 account_address: Option<&str>,
46 environment: HyperliquidEnvironment,
47) -> Result<Option<String>> {
48 let (pk_env_var, vault_env_var) = credential_env_vars(environment);
49 let env_private_key = env::var(pk_env_var).ok();
50 let env_vault_address = env::var(vault_env_var).ok();
51 let env_account_address = env::var("HYPERLIQUID_ACCOUNT_ADDRESS").ok();
52
53 resolve_execution_account_address_from_values(
54 private_key,
55 vault_address,
56 account_address,
57 env_private_key.as_deref(),
58 env_vault_address.as_deref(),
59 env_account_address.as_deref(),
60 )
61}
62
63fn resolve_execution_account_address_from_values(
64 private_key: Option<&str>,
65 vault_address: Option<&str>,
66 account_address: Option<&str>,
67 env_private_key: Option<&str>,
68 env_vault_address: Option<&str>,
69 env_account_address: Option<&str>,
70) -> Result<Option<String>> {
71 if let Some(address) = account_address {
72 return normalize_account_address(address).map(Some);
73 }
74
75 if let Some(address) = trim_nonempty(vault_address) {
76 return VaultAddress::parse(&address).map(|addr| Some(addr.to_hex()));
77 }
78
79 if let Some(address) = trim_nonempty(env_account_address) {
80 return normalize_account_address(&address).map(Some);
81 }
82
83 if let Some(address) = trim_nonempty(env_vault_address) {
84 return VaultAddress::parse(&address).map(|addr| Some(addr.to_hex()));
85 }
86
87 let Some(private_key) = trim_nonempty(private_key).or_else(|| trim_nonempty(env_private_key))
88 else {
89 return Ok(None);
90 };
91
92 let private_key = EvmPrivateKey::new(&private_key)?;
93 HyperliquidEip712Signer::new(&private_key)?
94 .address()
95 .map(Some)
96}
97
98fn trim_nonempty(value: Option<&str>) -> Option<String> {
99 let value = value?.trim();
100 (!value.is_empty()).then(|| value.to_string())
101}
102
103pub(crate) fn normalize_account_address(value: &str) -> Result<String> {
104 let value = value.trim();
105 let address = value
106 .parse::<Address>()
107 .map_err(|_| Error::bad_request("Account address must be 20 bytes of valid hex"))?;
108 let normalized = format!("{address:#x}");
109 if value != normalized && value != address.to_checksum(None) {
110 return Err(Error::bad_request(
111 "Account address must be 0x-prefixed lowercase or EIP-55 checksummed hex",
112 ));
113 }
114
115 Ok(normalized)
116}
117
118#[cfg(test)]
119mod tests {
120 use rstest::rstest;
121
122 use super::resolve_execution_account_address_from_values;
123
124 const PRIVATE_KEY: &str = "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
125 const EXPLICIT_ACCOUNT: &str = " 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ";
126 const EXPLICIT_VAULT: &str = " 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ";
127 const ENV_ACCOUNT: &str = " 0xcccccccccccccccccccccccccccccccccccccccc ";
128 const ENV_VAULT: &str = " 0xdddddddddddddddddddddddddddddddddddddddd ";
129
130 #[rstest]
131 #[case(
132 Some(PRIVATE_KEY),
133 Some(EXPLICIT_VAULT),
134 Some(EXPLICIT_ACCOUNT),
135 Some(PRIVATE_KEY),
136 Some(ENV_VAULT),
137 Some(ENV_ACCOUNT),
138 Some("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
139 )]
140 #[case(
141 Some(PRIVATE_KEY),
142 Some(EXPLICIT_VAULT),
143 None,
144 Some(PRIVATE_KEY),
145 Some(ENV_VAULT),
146 Some(ENV_ACCOUNT),
147 Some("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
148 )]
149 #[case(
150 Some(PRIVATE_KEY),
151 None,
152 None,
153 Some(PRIVATE_KEY),
154 Some(ENV_VAULT),
155 Some(ENV_ACCOUNT),
156 Some("0xcccccccccccccccccccccccccccccccccccccccc")
157 )]
158 #[case(
159 Some(PRIVATE_KEY),
160 None,
161 None,
162 Some(PRIVATE_KEY),
163 Some(ENV_VAULT),
164 None,
165 Some("0xdddddddddddddddddddddddddddddddddddddddd")
166 )]
167 #[case(None, None, None, None, None, None, None)]
168 fn test_resolve_execution_account_address_precedence(
169 #[case] private_key: Option<&str>,
170 #[case] vault_address: Option<&str>,
171 #[case] account_address: Option<&str>,
172 #[case] env_private_key: Option<&str>,
173 #[case] env_vault_address: Option<&str>,
174 #[case] env_account_address: Option<&str>,
175 #[case] expected: Option<&str>,
176 ) {
177 let result = resolve_execution_account_address_from_values(
178 private_key,
179 vault_address,
180 account_address,
181 env_private_key,
182 env_vault_address,
183 env_account_address,
184 )
185 .unwrap();
186
187 assert_eq!(result.as_deref(), expected);
188 }
189
190 #[rstest]
191 fn test_resolve_execution_account_address_uses_signer_fallback() {
192 let result = resolve_execution_account_address_from_values(
193 Some(PRIVATE_KEY),
194 None,
195 None,
196 None,
197 None,
198 None,
199 )
200 .unwrap();
201
202 assert_eq!(
203 result.as_deref(),
204 Some("0xc96aaa54e2d44c299564da76e1cd3184a2386b8d"),
205 );
206 }
207
208 #[rstest]
209 fn test_resolve_execution_account_address_uses_env_private_key_fallback() {
210 let result = resolve_execution_account_address_from_values(
211 None,
212 None,
213 None,
214 Some(PRIVATE_KEY),
215 None,
216 None,
217 )
218 .unwrap();
219
220 assert_eq!(
221 result.as_deref(),
222 Some("0xc96aaa54e2d44c299564da76e1cd3184a2386b8d"),
223 );
224 }
225
226 #[rstest]
227 fn test_resolve_execution_account_address_rejects_invalid_selected_vault() {
228 let err = resolve_execution_account_address_from_values(
229 Some(PRIVATE_KEY),
230 Some("0xinvalid"),
231 None,
232 None,
233 None,
234 Some(ENV_ACCOUNT),
235 )
236 .unwrap_err();
237
238 assert!(
239 err.to_string()
240 .contains("Vault address must be 20 bytes of valid hex"),
241 "unexpected error: {err}",
242 );
243 }
244
245 #[rstest]
246 #[case("0xd8da6bf26964af9d7eed9e03e53415d37aa96045")]
247 #[case("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")]
248 #[case(" \t0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045\n")]
249 fn test_resolve_execution_account_address_normalizes(#[case] address: &str) {
250 let explicit = resolve_execution_account_address_from_values(
251 Some(PRIVATE_KEY),
252 None,
253 Some(address),
254 None,
255 None,
256 None,
257 )
258 .unwrap();
259 let from_env = resolve_execution_account_address_from_values(
260 Some(PRIVATE_KEY),
261 None,
262 None,
263 None,
264 None,
265 Some(address),
266 )
267 .unwrap();
268
269 assert_eq!(
270 explicit.as_deref(),
271 Some("0xd8da6bf26964af9d7eed9e03e53415d37aa96045")
272 );
273 assert_eq!(from_env, explicit);
274 }
275
276 #[rstest]
277 #[case("")]
278 #[case(" \t\n")]
279 #[case("0xd8da6bf26964af9d7eed9e03e53415d37aa9604")]
280 #[case("0xd8da6bf26964af9d7eed9e03e53415d37aa960450")]
281 #[case("0xd8da6bf26964af9d7eed9e03e53415d37aa9604g")]
282 #[case("0xd8da6bf26964af9d7eed9e03e53415d37aa9604é")]
283 fn test_resolve_execution_account_address_rejects_invalid_hex(#[case] address: &str) {
284 let e = resolve_execution_account_address_from_values(
285 Some(PRIVATE_KEY),
286 Some(EXPLICIT_VAULT),
287 Some(address),
288 None,
289 None,
290 Some(ENV_ACCOUNT),
291 )
292 .unwrap_err();
293
294 assert_eq!(
295 e.to_string(),
296 "bad request: Account address must be 20 bytes of valid hex"
297 );
298 }
299
300 #[rstest]
301 #[case("d8da6bf26964af9d7eed9e03e53415d37aa96045")]
302 #[case("0Xd8da6bf26964af9d7eed9e03e53415d37aa96045")]
303 #[case("0xd8Da6BF26964aF9D7eEd9e03E53415D37aA96045")]
304 #[case("0xD8DA6BF26964AF9D7EED9E03E53415D37AA96045")]
305 fn test_resolve_execution_account_address_rejects_invalid_format(#[case] address: &str) {
306 let e = resolve_execution_account_address_from_values(
307 Some(PRIVATE_KEY),
308 Some(EXPLICIT_VAULT),
309 Some(address),
310 None,
311 None,
312 Some(ENV_ACCOUNT),
313 )
314 .unwrap_err();
315
316 assert_eq!(
317 e.to_string(),
318 "bad request: Account address must be 0x-prefixed lowercase or EIP-55 checksummed hex"
319 );
320 }
321
322 #[rstest]
323 fn test_resolve_execution_account_address_rejects_invalid_env_account() {
324 let e = resolve_execution_account_address_from_values(
325 Some(PRIVATE_KEY),
326 None,
327 None,
328 None,
329 Some(ENV_VAULT),
330 Some("0xinvalid"),
331 )
332 .unwrap_err();
333
334 assert_eq!(
335 e.to_string(),
336 "bad request: Account address must be 20 bytes of valid hex"
337 );
338 }
339
340 #[rstest]
341 #[case(None, None, None)]
342 #[case(
343 Some(PRIVATE_KEY),
344 None,
345 Some("0xc96aaa54e2d44c299564da76e1cd3184a2386b8d")
346 )]
347 #[case(
348 Some(PRIVATE_KEY),
349 Some(ENV_VAULT),
350 Some("0xdddddddddddddddddddddddddddddddddddddddd")
351 )]
352 fn test_resolve_execution_account_address_ignores_empty_env_account(
353 #[case] private_key: Option<&str>,
354 #[case] vault_address: Option<&str>,
355 #[case] expected: Option<&str>,
356 ) {
357 let result = resolve_execution_account_address_from_values(
358 None,
359 None,
360 None,
361 private_key,
362 vault_address,
363 Some(" \t\n"),
364 )
365 .unwrap();
366
367 assert_eq!(result.as_deref(), expected);
368 }
369}