Skip to main content

nautilus_hyperliquid/
account.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Account-address resolution for Hyperliquid execution queries and subscriptions.
17
18use 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
29/// Resolves the execution account address used for REST queries and WebSocket subscriptions.
30///
31/// Precedence is explicit account address, explicit vault address, environment account address,
32/// environment vault address, then the signer address derived from the private key.
33///
34/// # Errors
35///
36/// Returns an error if a selected vault address or private key is invalid.
37pub 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}