use crate::config::CredentialsSection;
pub struct CredentialResolver {
config: CredentialsSection,
}
impl CredentialResolver {
pub fn new(config: CredentialsSection) -> Self {
Self { config }
}
pub fn anthropic_key(&self) -> Option<String> {
std::env::var("ANTHROPIC_OAUTH_TOKEN")
.ok()
.or_else(|| std::env::var("ANTHROPIC_API_KEY").ok())
.or_else(|| self.config.anthropic_api_key.clone())
.filter(|s| !s.is_empty())
}
pub fn hyperliquid_key(&self) -> Option<String> {
std::env::var("HYPERLIQUID_PRIVATE_KEY")
.ok()
.or_else(|| self.config.hyperliquid_private_key.clone())
.filter(|s| !s.is_empty())
}
pub fn hyperliquid_address(&self) -> Option<Result<String, String>> {
self.hyperliquid_key().map(|key_hex| {
let stripped = key_hex
.strip_prefix("0x")
.or_else(|| key_hex.strip_prefix("0X"))
.unwrap_or(&key_hex);
let key_bytes =
hex::decode(stripped).map_err(|e| format!("invalid hex in private key: {e}"))?;
let signing_key = k256::ecdsa::SigningKey::from_bytes(key_bytes.as_slice().into())
.map_err(|e| format!("invalid secp256k1 key: {e}"))?;
let verifying_key = signing_key.verifying_key();
let point = verifying_key.to_encoded_point(false);
let pubkey_bytes = &point.as_bytes()[1..];
use sha3::{Digest, Keccak256};
let hash = Keccak256::digest(pubkey_bytes);
Ok(format!("0x{}", hex::encode(&hash[12..])))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn clear_cred_env_vars() {
env::remove_var("ANTHROPIC_OAUTH_TOKEN");
env::remove_var("ANTHROPIC_API_KEY");
env::remove_var("HYPERLIQUID_PRIVATE_KEY");
}
fn empty_config() -> CredentialsSection {
CredentialsSection {
anthropic_api_key: None,
hyperliquid_private_key: None,
}
}
#[test]
fn test_anthropic_key_from_oauth_token_env() {
let _lock = ENV_LOCK.lock().unwrap();
clear_cred_env_vars();
env::set_var("ANTHROPIC_OAUTH_TOKEN", "oauth-tok");
env::set_var("ANTHROPIC_API_KEY", "api-key");
let resolver = CredentialResolver::new(CredentialsSection {
anthropic_api_key: Some("toml-key".to_string()),
hyperliquid_private_key: None,
});
assert_eq!(resolver.anthropic_key().as_deref(), Some("oauth-tok"));
clear_cred_env_vars();
}
#[test]
fn test_anthropic_key_from_api_key_env() {
let _lock = ENV_LOCK.lock().unwrap();
clear_cred_env_vars();
env::set_var("ANTHROPIC_API_KEY", "api-key");
let resolver = CredentialResolver::new(CredentialsSection {
anthropic_api_key: Some("toml-key".to_string()),
hyperliquid_private_key: None,
});
assert_eq!(resolver.anthropic_key().as_deref(), Some("api-key"));
clear_cred_env_vars();
}
#[test]
fn test_anthropic_key_from_toml() {
let _lock = ENV_LOCK.lock().unwrap();
clear_cred_env_vars();
let resolver = CredentialResolver::new(CredentialsSection {
anthropic_api_key: Some("toml-key".to_string()),
hyperliquid_private_key: None,
});
assert_eq!(resolver.anthropic_key().as_deref(), Some("toml-key"));
clear_cred_env_vars();
}
#[test]
fn test_anthropic_key_none_when_all_absent() {
let _lock = ENV_LOCK.lock().unwrap();
clear_cred_env_vars();
let resolver = CredentialResolver::new(empty_config());
assert!(resolver.anthropic_key().is_none());
clear_cred_env_vars();
}
#[test]
fn test_anthropic_key_empty_string_treated_as_absent() {
let _lock = ENV_LOCK.lock().unwrap();
clear_cred_env_vars();
env::set_var("ANTHROPIC_API_KEY", "");
let resolver = CredentialResolver::new(CredentialsSection {
anthropic_api_key: Some(String::new()),
hyperliquid_private_key: None,
});
assert!(resolver.anthropic_key().is_none());
clear_cred_env_vars();
}
#[test]
fn test_hyperliquid_key_from_env() {
let _lock = ENV_LOCK.lock().unwrap();
clear_cred_env_vars();
env::set_var("HYPERLIQUID_PRIVATE_KEY", "0xENV");
let resolver = CredentialResolver::new(CredentialsSection {
anthropic_api_key: None,
hyperliquid_private_key: Some("0xTOML".to_string()),
});
assert_eq!(resolver.hyperliquid_key().as_deref(), Some("0xENV"));
clear_cred_env_vars();
}
#[test]
fn test_hyperliquid_key_from_toml() {
let _lock = ENV_LOCK.lock().unwrap();
clear_cred_env_vars();
let resolver = CredentialResolver::new(CredentialsSection {
anthropic_api_key: None,
hyperliquid_private_key: Some("0xTOML".to_string()),
});
assert_eq!(resolver.hyperliquid_key().as_deref(), Some("0xTOML"));
clear_cred_env_vars();
}
#[test]
fn test_hyperliquid_key_none_when_absent() {
let _lock = ENV_LOCK.lock().unwrap();
clear_cred_env_vars();
let resolver = CredentialResolver::new(empty_config());
assert!(resolver.hyperliquid_key().is_none());
clear_cred_env_vars();
}
}