use crate::core::types::ExchangeId;
use std::collections::HashMap;
#[derive(Clone, Debug)]
pub struct ExchangeCredentials {
pub exchange_id: ExchangeId,
pub api_key: String,
pub api_secret: String,
pub passphrase: Option<String>,
pub testnet: bool,
}
impl ExchangeCredentials {
pub fn new(exchange_id: ExchangeId, api_key: String, api_secret: String) -> Self {
assert!(!api_key.is_empty(), "API key cannot be empty");
assert!(!api_secret.is_empty(), "API secret cannot be empty");
Self {
exchange_id,
api_key,
api_secret,
passphrase: None,
testnet: false,
}
}
pub fn with_passphrase(mut self, passphrase: String) -> Self {
self.passphrase = Some(passphrase);
self
}
pub fn with_testnet(mut self, testnet: bool) -> Self {
self.testnet = testnet;
self
}
pub fn is_complete(&self) -> bool {
!self.api_key.is_empty() && !self.api_secret.is_empty()
}
}
#[derive(Clone, Debug)]
pub struct ConnectorConfig {
pub exchange_id: ExchangeId,
pub credentials: Option<ExchangeCredentials>,
pub testnet: bool,
pub enabled: bool,
}
impl ConnectorConfig {
pub fn new(exchange_id: ExchangeId) -> Self {
Self {
exchange_id,
credentials: None,
testnet: false,
enabled: true,
}
}
pub fn with_credentials(mut self, credentials: ExchangeCredentials) -> Self {
self.credentials = Some(credentials);
self
}
pub fn with_testnet(mut self, testnet: bool) -> Self {
self.testnet = testnet;
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn is_public(&self) -> bool {
self.credentials.is_none()
}
pub fn is_authenticated(&self) -> bool {
self.credentials
.as_ref()
.map(|c| c.is_complete())
.unwrap_or(false)
}
}
pub struct ConnectorConfigManager {
configs: HashMap<ExchangeId, ConnectorConfig>,
}
impl ConnectorConfigManager {
pub fn new() -> Self {
Self {
configs: HashMap::new(),
}
}
pub fn add_config(&mut self, config: ConnectorConfig) {
self.configs.insert(config.exchange_id, config);
}
pub fn get_config(&self, id: &ExchangeId) -> Option<&ConnectorConfig> {
self.configs.get(id)
}
pub fn remove_config(&mut self, id: &ExchangeId) -> Option<ConnectorConfig> {
self.configs.remove(id)
}
pub fn all_configs(&self) -> Vec<&ConnectorConfig> {
self.configs.values().collect()
}
pub fn enabled_configs(&self) -> Vec<&ConnectorConfig> {
self.configs
.values()
.filter(|c| c.enabled)
.collect()
}
pub fn authenticated_configs(&self) -> Vec<&ConnectorConfig> {
self.configs
.values()
.filter(|c| c.is_authenticated())
.collect()
}
pub fn load_from_env() -> Self {
let mut manager = Self::new();
let exchanges = [
ExchangeId::Binance,
ExchangeId::Bybit,
ExchangeId::OKX,
ExchangeId::KuCoin,
ExchangeId::Kraken,
ExchangeId::Coinbase,
ExchangeId::GateIO,
ExchangeId::Bitfinex,
ExchangeId::Bitstamp,
ExchangeId::Gemini,
ExchangeId::MEXC,
ExchangeId::HTX,
ExchangeId::Bitget,
ExchangeId::BingX,
ExchangeId::CryptoCom,
ExchangeId::Upbit,
ExchangeId::Deribit,
ExchangeId::HyperLiquid,
ExchangeId::Lighter,
ExchangeId::Paradex,
ExchangeId::Dydx,
ExchangeId::Polygon,
ExchangeId::Finnhub,
ExchangeId::Tiingo,
ExchangeId::Twelvedata,
ExchangeId::Coinglass,
ExchangeId::CryptoCompare,
ExchangeId::WhaleAlert,
ExchangeId::DefiLlama,
ExchangeId::Oanda,
ExchangeId::AlphaVantage,
ExchangeId::Dukascopy,
ExchangeId::AngelOne,
ExchangeId::Zerodha,
ExchangeId::Fyers,
ExchangeId::Dhan,
ExchangeId::Upstox,
ExchangeId::Alpaca,
ExchangeId::JQuants,
ExchangeId::Tinkoff,
ExchangeId::Moex,
ExchangeId::Krx,
ExchangeId::Fred,
ExchangeId::Futu,
ExchangeId::Bitquery,
ExchangeId::YahooFinance,
];
for exchange_id in exchanges {
let prefix = exchange_id.as_str().to_uppercase();
let api_key_var = format!("{}_API_KEY", prefix);
let api_secret_var = format!("{}_API_SECRET", prefix);
if let (Ok(api_key), Ok(api_secret)) = (
std::env::var(&api_key_var),
std::env::var(&api_secret_var),
) {
if api_key.is_empty() || api_secret.is_empty() {
continue;
}
let mut creds = ExchangeCredentials::new(exchange_id, api_key, api_secret);
let passphrase_var = format!("{}_PASSPHRASE", prefix);
if let Ok(passphrase) = std::env::var(&passphrase_var) {
if !passphrase.is_empty() {
creds = creds.with_passphrase(passphrase);
}
}
let testnet_var = format!("{}_TESTNET", prefix);
if let Ok(testnet_str) = std::env::var(&testnet_var) {
if testnet_str.to_lowercase() == "true" {
creds = creds.with_testnet(true);
}
}
let config = ConnectorConfig::new(exchange_id)
.with_credentials(creds)
.enabled(true);
manager.add_config(config);
}
}
manager
}
pub fn len(&self) -> usize {
self.configs.len()
}
pub fn is_empty(&self) -> bool {
self.configs.is_empty()
}
}
impl Default for ConnectorConfigManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_credentials_new() {
let creds = ExchangeCredentials::new(
ExchangeId::Binance,
"key123".to_string(),
"secret456".to_string(),
);
assert_eq!(creds.exchange_id, ExchangeId::Binance);
assert_eq!(creds.api_key, "key123");
assert_eq!(creds.api_secret, "secret456");
assert_eq!(creds.passphrase, None);
assert!(!creds.testnet);
}
#[test]
#[should_panic(expected = "API key cannot be empty")]
fn test_credentials_empty_key_panics() {
ExchangeCredentials::new(
ExchangeId::Binance,
"".to_string(),
"secret".to_string(),
);
}
#[test]
#[should_panic(expected = "API secret cannot be empty")]
fn test_credentials_empty_secret_panics() {
ExchangeCredentials::new(
ExchangeId::Binance,
"key".to_string(),
"".to_string(),
);
}
#[test]
fn test_credentials_with_passphrase() {
let creds = ExchangeCredentials::new(
ExchangeId::OKX,
"key".to_string(),
"secret".to_string(),
)
.with_passphrase("pass123".to_string());
assert_eq!(creds.passphrase, Some("pass123".to_string()));
}
#[test]
fn test_credentials_with_testnet() {
let creds = ExchangeCredentials::new(
ExchangeId::Binance,
"key".to_string(),
"secret".to_string(),
)
.with_testnet(true);
assert!(creds.testnet);
}
#[test]
fn test_credentials_builder_chain() {
let creds = ExchangeCredentials::new(
ExchangeId::KuCoin,
"key".to_string(),
"secret".to_string(),
)
.with_passphrase("pass".to_string())
.with_testnet(true);
assert_eq!(creds.passphrase, Some("pass".to_string()));
assert!(creds.testnet);
}
#[test]
fn test_credentials_is_complete() {
let creds = ExchangeCredentials::new(
ExchangeId::Binance,
"key".to_string(),
"secret".to_string(),
);
assert!(creds.is_complete());
}
#[test]
fn test_config_new() {
let config = ConnectorConfig::new(ExchangeId::Binance);
assert_eq!(config.exchange_id, ExchangeId::Binance);
assert!(config.credentials.is_none());
assert!(!config.testnet);
assert!(config.enabled);
}
#[test]
fn test_config_with_credentials() {
let creds = ExchangeCredentials::new(
ExchangeId::Binance,
"key".to_string(),
"secret".to_string(),
);
let config = ConnectorConfig::new(ExchangeId::Binance)
.with_credentials(creds.clone());
assert!(config.credentials.is_some());
assert_eq!(config.credentials.unwrap().api_key, "key");
}
#[test]
fn test_config_with_testnet() {
let config = ConnectorConfig::new(ExchangeId::Binance)
.with_testnet(true);
assert!(config.testnet);
}
#[test]
fn test_config_enabled() {
let config = ConnectorConfig::new(ExchangeId::Binance)
.enabled(false);
assert!(!config.enabled);
}
#[test]
fn test_config_is_public() {
let config = ConnectorConfig::new(ExchangeId::Binance);
assert!(config.is_public());
let creds = ExchangeCredentials::new(
ExchangeId::Binance,
"key".to_string(),
"secret".to_string(),
);
let config_with_creds = ConnectorConfig::new(ExchangeId::Binance)
.with_credentials(creds);
assert!(!config_with_creds.is_public());
}
#[test]
fn test_config_is_authenticated() {
let config = ConnectorConfig::new(ExchangeId::Binance);
assert!(!config.is_authenticated());
let creds = ExchangeCredentials::new(
ExchangeId::Binance,
"key".to_string(),
"secret".to_string(),
);
let config_with_creds = ConnectorConfig::new(ExchangeId::Binance)
.with_credentials(creds);
assert!(config_with_creds.is_authenticated());
}
#[test]
fn test_config_builder_chain() {
let creds = ExchangeCredentials::new(
ExchangeId::Binance,
"key".to_string(),
"secret".to_string(),
);
let config = ConnectorConfig::new(ExchangeId::Binance)
.with_credentials(creds)
.with_testnet(true)
.enabled(false);
assert!(config.is_authenticated());
assert!(config.testnet);
assert!(!config.enabled);
}
#[test]
fn test_manager_new() {
let manager = ConnectorConfigManager::new();
assert!(manager.is_empty());
assert_eq!(manager.len(), 0);
}
#[test]
fn test_manager_add_config() {
let mut manager = ConnectorConfigManager::new();
let config = ConnectorConfig::new(ExchangeId::Binance);
manager.add_config(config);
assert_eq!(manager.len(), 1);
assert!(!manager.is_empty());
}
#[test]
fn test_manager_get_config() {
let mut manager = ConnectorConfigManager::new();
let config = ConnectorConfig::new(ExchangeId::Binance);
manager.add_config(config);
let retrieved = manager.get_config(&ExchangeId::Binance);
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().exchange_id, ExchangeId::Binance);
let not_found = manager.get_config(&ExchangeId::OKX);
assert!(not_found.is_none());
}
#[test]
fn test_manager_remove_config() {
let mut manager = ConnectorConfigManager::new();
let config = ConnectorConfig::new(ExchangeId::Binance);
manager.add_config(config);
assert_eq!(manager.len(), 1);
let removed = manager.remove_config(&ExchangeId::Binance);
assert!(removed.is_some());
assert_eq!(manager.len(), 0);
let not_found = manager.remove_config(&ExchangeId::Binance);
assert!(not_found.is_none());
}
#[test]
fn test_manager_all_configs() {
let mut manager = ConnectorConfigManager::new();
manager.add_config(ConnectorConfig::new(ExchangeId::Binance));
manager.add_config(ConnectorConfig::new(ExchangeId::OKX));
manager.add_config(ConnectorConfig::new(ExchangeId::Bybit));
let all = manager.all_configs();
assert_eq!(all.len(), 3);
}
#[test]
fn test_manager_enabled_configs() {
let mut manager = ConnectorConfigManager::new();
manager.add_config(ConnectorConfig::new(ExchangeId::Binance).enabled(true));
manager.add_config(ConnectorConfig::new(ExchangeId::OKX).enabled(false));
manager.add_config(ConnectorConfig::new(ExchangeId::Bybit).enabled(true));
let enabled = manager.enabled_configs();
assert_eq!(enabled.len(), 2);
}
#[test]
fn test_manager_authenticated_configs() {
let mut manager = ConnectorConfigManager::new();
let creds1 = ExchangeCredentials::new(
ExchangeId::Binance,
"key1".to_string(),
"secret1".to_string(),
);
let creds2 = ExchangeCredentials::new(
ExchangeId::Bybit,
"key2".to_string(),
"secret2".to_string(),
);
manager.add_config(ConnectorConfig::new(ExchangeId::Binance).with_credentials(creds1));
manager.add_config(ConnectorConfig::new(ExchangeId::OKX)); manager.add_config(ConnectorConfig::new(ExchangeId::Bybit).with_credentials(creds2));
let authenticated = manager.authenticated_configs();
assert_eq!(authenticated.len(), 2);
}
#[test]
fn test_manager_default() {
let manager = ConnectorConfigManager::default();
assert!(manager.is_empty());
}
#[test]
fn test_manager_replace_existing_config() {
let mut manager = ConnectorConfigManager::new();
let config1 = ConnectorConfig::new(ExchangeId::Binance).enabled(true);
let config2 = ConnectorConfig::new(ExchangeId::Binance).enabled(false);
manager.add_config(config1);
assert_eq!(manager.len(), 1);
assert!(manager.get_config(&ExchangeId::Binance).unwrap().enabled);
manager.add_config(config2);
assert_eq!(manager.len(), 1); assert!(!manager.get_config(&ExchangeId::Binance).unwrap().enabled);
}
#[test]
fn test_load_from_env_basic() {
std::env::set_var("BINANCE_API_KEY", "test_key");
std::env::set_var("BINANCE_API_SECRET", "test_secret");
let manager = ConnectorConfigManager::load_from_env();
let config = manager.get_config(&ExchangeId::Binance);
assert!(config.is_some());
let config = config.unwrap();
assert!(config.is_authenticated());
assert_eq!(config.credentials.as_ref().unwrap().api_key, "test_key");
assert_eq!(config.credentials.as_ref().unwrap().api_secret, "test_secret");
std::env::remove_var("BINANCE_API_KEY");
std::env::remove_var("BINANCE_API_SECRET");
}
#[test]
fn test_load_from_env_with_passphrase() {
std::env::set_var("OKX_API_KEY", "okx_key");
std::env::set_var("OKX_API_SECRET", "okx_secret");
std::env::set_var("OKX_PASSPHRASE", "okx_pass");
let manager = ConnectorConfigManager::load_from_env();
let config = manager.get_config(&ExchangeId::OKX);
assert!(config.is_some());
let creds = config.unwrap().credentials.as_ref().unwrap();
assert_eq!(creds.passphrase, Some("okx_pass".to_string()));
std::env::remove_var("OKX_API_KEY");
std::env::remove_var("OKX_API_SECRET");
std::env::remove_var("OKX_PASSPHRASE");
}
#[test]
fn test_load_from_env_with_testnet() {
std::env::set_var("BYBIT_API_KEY", "bybit_key");
std::env::set_var("BYBIT_API_SECRET", "bybit_secret");
std::env::set_var("BYBIT_TESTNET", "true");
let manager = ConnectorConfigManager::load_from_env();
let config = manager.get_config(&ExchangeId::Bybit);
assert!(config.is_some());
let creds = config.unwrap().credentials.as_ref().unwrap();
assert!(creds.testnet);
std::env::remove_var("BYBIT_API_KEY");
std::env::remove_var("BYBIT_API_SECRET");
std::env::remove_var("BYBIT_TESTNET");
}
#[test]
fn test_load_from_env_testnet_false() {
std::env::set_var("KRAKEN_API_KEY", "kraken_key");
std::env::set_var("KRAKEN_API_SECRET", "kraken_secret");
std::env::set_var("KRAKEN_TESTNET", "false");
let manager = ConnectorConfigManager::load_from_env();
let config = manager.get_config(&ExchangeId::Kraken);
assert!(config.is_some());
let creds = config.unwrap().credentials.as_ref().unwrap();
assert!(!creds.testnet);
std::env::remove_var("KRAKEN_API_KEY");
std::env::remove_var("KRAKEN_API_SECRET");
std::env::remove_var("KRAKEN_TESTNET");
}
#[test]
fn test_load_from_env_multiple_exchanges() {
std::env::set_var("BINANCE_API_KEY", "binance_key");
std::env::set_var("BINANCE_API_SECRET", "binance_secret");
std::env::set_var("OKX_API_KEY", "okx_key");
std::env::set_var("OKX_API_SECRET", "okx_secret");
std::env::set_var("KUCOIN_API_KEY", "kucoin_key");
std::env::set_var("KUCOIN_API_SECRET", "kucoin_secret");
let manager = ConnectorConfigManager::load_from_env();
assert_eq!(manager.authenticated_configs().len(), 3);
assert!(manager.get_config(&ExchangeId::Binance).is_some());
assert!(manager.get_config(&ExchangeId::OKX).is_some());
assert!(manager.get_config(&ExchangeId::KuCoin).is_some());
std::env::remove_var("BINANCE_API_KEY");
std::env::remove_var("BINANCE_API_SECRET");
std::env::remove_var("OKX_API_KEY");
std::env::remove_var("OKX_API_SECRET");
std::env::remove_var("KUCOIN_API_KEY");
std::env::remove_var("KUCOIN_API_SECRET");
}
#[test]
fn test_load_from_env_empty_values_ignored() {
std::env::set_var("GEMINI_API_KEY", "");
std::env::set_var("GEMINI_API_SECRET", "secret");
let manager = ConnectorConfigManager::load_from_env();
assert!(manager.get_config(&ExchangeId::Gemini).is_none());
std::env::remove_var("GEMINI_API_KEY");
std::env::remove_var("GEMINI_API_SECRET");
}
#[test]
fn test_load_from_env_missing_secret_ignored() {
std::env::set_var("COINBASE_API_KEY", "coinbase_key");
let manager = ConnectorConfigManager::load_from_env();
assert!(manager.get_config(&ExchangeId::Coinbase).is_none());
std::env::remove_var("COINBASE_API_KEY");
}
#[test]
fn test_load_from_env_case_insensitive_testnet() {
std::env::set_var("MEXC_API_KEY", "mexc_key");
std::env::set_var("MEXC_API_SECRET", "mexc_secret");
std::env::set_var("MEXC_TESTNET", "TRUE");
let manager = ConnectorConfigManager::load_from_env();
let config = manager.get_config(&ExchangeId::MEXC);
assert!(config.is_some());
assert!(config.unwrap().credentials.as_ref().unwrap().testnet);
std::env::remove_var("MEXC_API_KEY");
std::env::remove_var("MEXC_API_SECRET");
std::env::remove_var("MEXC_TESTNET");
}
#[test]
fn test_load_from_env_empty_passphrase_ignored() {
std::env::set_var("BITGET_API_KEY", "bitget_key");
std::env::set_var("BITGET_API_SECRET", "bitget_secret");
std::env::set_var("BITGET_PASSPHRASE", "");
let manager = ConnectorConfigManager::load_from_env();
let config = manager.get_config(&ExchangeId::Bitget);
assert!(config.is_some());
assert_eq!(config.unwrap().credentials.as_ref().unwrap().passphrase, None);
std::env::remove_var("BITGET_API_KEY");
std::env::remove_var("BITGET_API_SECRET");
std::env::remove_var("BITGET_PASSPHRASE");
}
#[test]
fn test_load_from_env_all_enabled() {
std::env::set_var("HTX_API_KEY", "htx_key");
std::env::set_var("HTX_API_SECRET", "htx_secret");
let manager = ConnectorConfigManager::load_from_env();
let config = manager.get_config(&ExchangeId::HTX);
assert!(config.is_some());
assert!(config.unwrap().enabled);
std::env::remove_var("HTX_API_KEY");
std::env::remove_var("HTX_API_SECRET");
}
}