hyper_agent_core/
credentials.rs1use crate::config::CredentialsSection;
14
15pub struct CredentialResolver {
20 config: CredentialsSection,
21}
22
23impl CredentialResolver {
24 pub fn new(config: CredentialsSection) -> Self {
26 Self { config }
27 }
28
29 pub fn anthropic_key(&self) -> Option<String> {
33 std::env::var("ANTHROPIC_OAUTH_TOKEN")
34 .ok()
35 .or_else(|| std::env::var("ANTHROPIC_API_KEY").ok())
36 .or_else(|| self.config.anthropic_api_key.clone())
37 .filter(|s| !s.is_empty())
38 }
39
40 pub fn hyperliquid_key(&self) -> Option<String> {
44 std::env::var("HYPERLIQUID_PRIVATE_KEY")
45 .ok()
46 .or_else(|| self.config.hyperliquid_private_key.clone())
47 .filter(|s| !s.is_empty())
48 }
49
50 pub fn hyperliquid_address(&self) -> Option<Result<String, String>> {
55 self.hyperliquid_key().map(|key_hex| {
56 let stripped = key_hex
57 .strip_prefix("0x")
58 .or_else(|| key_hex.strip_prefix("0X"))
59 .unwrap_or(&key_hex);
60 let key_bytes =
61 hex::decode(stripped).map_err(|e| format!("invalid hex in private key: {e}"))?;
62 let signing_key = k256::ecdsa::SigningKey::from_bytes(key_bytes.as_slice().into())
63 .map_err(|e| format!("invalid secp256k1 key: {e}"))?;
64 let verifying_key = signing_key.verifying_key();
65 let point = verifying_key.to_encoded_point(false);
66 let pubkey_bytes = &point.as_bytes()[1..];
67 use sha3::{Digest, Keccak256};
68 let hash = Keccak256::digest(pubkey_bytes);
69 Ok(format!("0x{}", hex::encode(&hash[12..])))
70 })
71 }
72}
73
74#[cfg(test)]
79mod tests {
80 use super::*;
81 use std::env;
82 use std::sync::Mutex;
83
84 static ENV_LOCK: Mutex<()> = Mutex::new(());
86
87 fn clear_cred_env_vars() {
88 env::remove_var("ANTHROPIC_OAUTH_TOKEN");
89 env::remove_var("ANTHROPIC_API_KEY");
90 env::remove_var("HYPERLIQUID_PRIVATE_KEY");
91 }
92
93 fn empty_config() -> CredentialsSection {
94 CredentialsSection {
95 anthropic_api_key: None,
96 hyperliquid_private_key: None,
97 }
98 }
99
100 #[test]
101 fn test_anthropic_key_from_oauth_token_env() {
102 let _lock = ENV_LOCK.lock().unwrap();
103 clear_cred_env_vars();
104 env::set_var("ANTHROPIC_OAUTH_TOKEN", "oauth-tok");
105 env::set_var("ANTHROPIC_API_KEY", "api-key");
106
107 let resolver = CredentialResolver::new(CredentialsSection {
108 anthropic_api_key: Some("toml-key".to_string()),
109 hyperliquid_private_key: None,
110 });
111
112 assert_eq!(resolver.anthropic_key().as_deref(), Some("oauth-tok"));
114 clear_cred_env_vars();
115 }
116
117 #[test]
118 fn test_anthropic_key_from_api_key_env() {
119 let _lock = ENV_LOCK.lock().unwrap();
120 clear_cred_env_vars();
121 env::set_var("ANTHROPIC_API_KEY", "api-key");
122
123 let resolver = CredentialResolver::new(CredentialsSection {
124 anthropic_api_key: Some("toml-key".to_string()),
125 hyperliquid_private_key: None,
126 });
127
128 assert_eq!(resolver.anthropic_key().as_deref(), Some("api-key"));
129 clear_cred_env_vars();
130 }
131
132 #[test]
133 fn test_anthropic_key_from_toml() {
134 let _lock = ENV_LOCK.lock().unwrap();
135 clear_cred_env_vars();
136
137 let resolver = CredentialResolver::new(CredentialsSection {
138 anthropic_api_key: Some("toml-key".to_string()),
139 hyperliquid_private_key: None,
140 });
141
142 assert_eq!(resolver.anthropic_key().as_deref(), Some("toml-key"));
143 clear_cred_env_vars();
144 }
145
146 #[test]
147 fn test_anthropic_key_none_when_all_absent() {
148 let _lock = ENV_LOCK.lock().unwrap();
149 clear_cred_env_vars();
150
151 let resolver = CredentialResolver::new(empty_config());
152 assert!(resolver.anthropic_key().is_none());
153 clear_cred_env_vars();
154 }
155
156 #[test]
157 fn test_anthropic_key_empty_string_treated_as_absent() {
158 let _lock = ENV_LOCK.lock().unwrap();
159 clear_cred_env_vars();
160 env::set_var("ANTHROPIC_API_KEY", "");
161
162 let resolver = CredentialResolver::new(CredentialsSection {
163 anthropic_api_key: Some(String::new()),
164 hyperliquid_private_key: None,
165 });
166
167 assert!(resolver.anthropic_key().is_none());
168 clear_cred_env_vars();
169 }
170
171 #[test]
172 fn test_hyperliquid_key_from_env() {
173 let _lock = ENV_LOCK.lock().unwrap();
174 clear_cred_env_vars();
175 env::set_var("HYPERLIQUID_PRIVATE_KEY", "0xENV");
176
177 let resolver = CredentialResolver::new(CredentialsSection {
178 anthropic_api_key: None,
179 hyperliquid_private_key: Some("0xTOML".to_string()),
180 });
181
182 assert_eq!(resolver.hyperliquid_key().as_deref(), Some("0xENV"));
183 clear_cred_env_vars();
184 }
185
186 #[test]
187 fn test_hyperliquid_key_from_toml() {
188 let _lock = ENV_LOCK.lock().unwrap();
189 clear_cred_env_vars();
190
191 let resolver = CredentialResolver::new(CredentialsSection {
192 anthropic_api_key: None,
193 hyperliquid_private_key: Some("0xTOML".to_string()),
194 });
195
196 assert_eq!(resolver.hyperliquid_key().as_deref(), Some("0xTOML"));
197 clear_cred_env_vars();
198 }
199
200 #[test]
201 fn test_hyperliquid_key_none_when_absent() {
202 let _lock = ENV_LOCK.lock().unwrap();
203 clear_cred_env_vars();
204
205 let resolver = CredentialResolver::new(empty_config());
206 assert!(resolver.hyperliquid_key().is_none());
207 clear_cred_env_vars();
208 }
209}