1use std::env;
19
20use crate::{
21 common::{
22 credential::{EvmPrivateKey, VaultAddress, credential_env_vars},
23 enums::HyperliquidEnvironment,
24 },
25 http::error::Result,
26 signing::HyperliquidEip712Signer,
27};
28
29pub fn resolve_execution_account_address(
38 private_key: Option<&str>,
39 vault_address: Option<&str>,
40 account_address: Option<&str>,
41 environment: HyperliquidEnvironment,
42) -> Result<Option<String>> {
43 let (pk_env_var, vault_env_var) = credential_env_vars(environment);
44 let env_private_key = env::var(pk_env_var).ok();
45 let env_vault_address = env::var(vault_env_var).ok();
46 let env_account_address = env::var("HYPERLIQUID_ACCOUNT_ADDRESS").ok();
47
48 resolve_execution_account_address_from_values(
49 private_key,
50 vault_address,
51 account_address,
52 env_private_key.as_deref(),
53 env_vault_address.as_deref(),
54 env_account_address.as_deref(),
55 )
56}
57
58fn resolve_execution_account_address_from_values(
59 private_key: Option<&str>,
60 vault_address: Option<&str>,
61 account_address: Option<&str>,
62 env_private_key: Option<&str>,
63 env_vault_address: Option<&str>,
64 env_account_address: Option<&str>,
65) -> Result<Option<String>> {
66 if let Some(address) = trim_nonempty(account_address) {
67 return Ok(Some(address));
68 }
69
70 if let Some(address) = trim_nonempty(vault_address) {
71 return VaultAddress::parse(&address).map(|addr| Some(addr.to_hex()));
72 }
73
74 if let Some(address) = trim_nonempty(env_account_address) {
75 return Ok(Some(address));
76 }
77
78 if let Some(address) = trim_nonempty(env_vault_address) {
79 return VaultAddress::parse(&address).map(|addr| Some(addr.to_hex()));
80 }
81
82 let Some(private_key) = trim_nonempty(private_key).or_else(|| trim_nonempty(env_private_key))
83 else {
84 return Ok(None);
85 };
86
87 let private_key = EvmPrivateKey::new(&private_key)?;
88 HyperliquidEip712Signer::new(&private_key)?
89 .address()
90 .map(Some)
91}
92
93fn trim_nonempty(value: Option<&str>) -> Option<String> {
94 let value = value?.trim();
95 (!value.is_empty()).then(|| value.to_string())
96}
97
98#[cfg(test)]
99mod tests {
100 use rstest::rstest;
101
102 use super::resolve_execution_account_address_from_values;
103
104 const PRIVATE_KEY: &str = "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
105 const EXPLICIT_ACCOUNT: &str = " 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ";
106 const EXPLICIT_VAULT: &str = " 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ";
107 const ENV_ACCOUNT: &str = " 0xcccccccccccccccccccccccccccccccccccccccc ";
108 const ENV_VAULT: &str = " 0xdddddddddddddddddddddddddddddddddddddddd ";
109
110 #[rstest]
111 #[case(
112 Some(PRIVATE_KEY),
113 Some(EXPLICIT_VAULT),
114 Some(EXPLICIT_ACCOUNT),
115 Some(PRIVATE_KEY),
116 Some(ENV_VAULT),
117 Some(ENV_ACCOUNT),
118 Some("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
119 )]
120 #[case(
121 Some(PRIVATE_KEY),
122 Some(EXPLICIT_VAULT),
123 None,
124 Some(PRIVATE_KEY),
125 Some(ENV_VAULT),
126 Some(ENV_ACCOUNT),
127 Some("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
128 )]
129 #[case(
130 Some(PRIVATE_KEY),
131 None,
132 None,
133 Some(PRIVATE_KEY),
134 Some(ENV_VAULT),
135 Some(ENV_ACCOUNT),
136 Some("0xcccccccccccccccccccccccccccccccccccccccc")
137 )]
138 #[case(
139 Some(PRIVATE_KEY),
140 None,
141 None,
142 Some(PRIVATE_KEY),
143 Some(ENV_VAULT),
144 None,
145 Some("0xdddddddddddddddddddddddddddddddddddddddd")
146 )]
147 #[case(None, None, None, None, None, None, None)]
148 fn test_resolve_execution_account_address_precedence(
149 #[case] private_key: Option<&str>,
150 #[case] vault_address: Option<&str>,
151 #[case] account_address: Option<&str>,
152 #[case] env_private_key: Option<&str>,
153 #[case] env_vault_address: Option<&str>,
154 #[case] env_account_address: Option<&str>,
155 #[case] expected: Option<&str>,
156 ) {
157 let result = resolve_execution_account_address_from_values(
158 private_key,
159 vault_address,
160 account_address,
161 env_private_key,
162 env_vault_address,
163 env_account_address,
164 )
165 .unwrap();
166
167 assert_eq!(result.as_deref(), expected);
168 }
169
170 #[rstest]
171 fn test_resolve_execution_account_address_uses_signer_fallback() {
172 let result = resolve_execution_account_address_from_values(
173 Some(PRIVATE_KEY),
174 None,
175 None,
176 None,
177 None,
178 None,
179 )
180 .unwrap();
181
182 assert_eq!(
183 result.as_deref(),
184 Some("0xc96aaa54e2d44c299564da76e1cd3184a2386b8d"),
185 );
186 }
187
188 #[rstest]
189 fn test_resolve_execution_account_address_uses_env_private_key_fallback() {
190 let result = resolve_execution_account_address_from_values(
191 None,
192 None,
193 None,
194 Some(PRIVATE_KEY),
195 None,
196 None,
197 )
198 .unwrap();
199
200 assert_eq!(
201 result.as_deref(),
202 Some("0xc96aaa54e2d44c299564da76e1cd3184a2386b8d"),
203 );
204 }
205
206 #[rstest]
207 fn test_resolve_execution_account_address_rejects_invalid_selected_vault() {
208 let err = resolve_execution_account_address_from_values(
209 Some(PRIVATE_KEY),
210 Some("0xinvalid"),
211 None,
212 None,
213 None,
214 Some(ENV_ACCOUNT),
215 )
216 .unwrap_err();
217
218 assert!(
219 err.to_string()
220 .contains("Vault address must be 20 bytes of valid hex"),
221 "unexpected error: {err}",
222 );
223 }
224}