1use super::adapters::{
4 BinanceAuthAdapter, BithumbAuthAdapter, BybitAuthAdapter, CoinbaseAuthAdapter, UpbitAuthAdapter,
5};
6use super::traits::AuthenticationManager;
7use crate::error::{EMSError, Result};
8use rusty_common::SmartString;
9use rusty_common::auth::exchanges::{
10 binance::BinanceAuth, bithumb::BithumbAuth, bybit::BybitAuth, coinbase::CoinbaseAuth,
11 upbit::UpbitAuth,
12};
13use rusty_common::auth::pem::validate_pem_format;
14use std::str::FromStr;
15use std::sync::Arc;
16use thiserror::Error;
17
18#[derive(Error, Debug)]
20pub enum AuthenticationError {
21 #[error("Invalid exchange: {0}")]
23 InvalidExchange(SmartString),
24
25 #[error("Authentication method not supported: {0}")]
27 UnsupportedMethod(SmartString),
28
29 #[error("Invalid credentials format: {0}")]
31 InvalidCredentials(SmartString),
32
33 #[error("Authentication expired")]
35 Expired,
36
37 #[error("Authentication refresh failed: {0}")]
39 RefreshFailed(SmartString),
40}
41
42impl From<AuthenticationError> for EMSError {
43 fn from(err: AuthenticationError) -> Self {
44 Self::auth(err.to_string())
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum ExchangeType {
51 Binance,
53 Bybit,
55 Coinbase,
57 Upbit,
59 Bithumb,
61}
62
63impl ExchangeType {
64 #[must_use]
66 pub const fn as_str(&self) -> &'static str {
67 match self {
68 Self::Binance => "binance",
69 Self::Bybit => "bybit",
70 Self::Coinbase => "coinbase",
71 Self::Upbit => "upbit",
72 Self::Bithumb => "bithumb",
73 }
74 }
75}
76
77impl FromStr for ExchangeType {
78 type Err = EMSError;
79
80 fn from_str(s: &str) -> Result<Self> {
81 match s.to_lowercase().as_str() {
82 "binance" => Ok(Self::Binance),
83 "bybit" => Ok(Self::Bybit),
84 "coinbase" => Ok(Self::Coinbase),
85 "upbit" => Ok(Self::Upbit),
86 "bithumb" => Ok(Self::Bithumb),
87 _ => Err(AuthenticationError::InvalidExchange(s.into()).into()),
88 }
89 }
90}
91
92pub fn create_auth_adapter(
94 exchange_type: ExchangeType,
95 api_key: impl Into<SmartString>,
96 secret_key: impl Into<SmartString>,
97 passphrase: Option<impl Into<SmartString>>,
98) -> Result<Arc<dyn AuthenticationManager>> {
99 let api_key = api_key.into();
100 let secret_key = secret_key.into();
101
102 match exchange_type {
103 ExchangeType::Binance => {
104 let auth = BinanceAuth::new_hmac(api_key, secret_key);
105 Ok(Arc::new(BinanceAuthAdapter::new(auth)))
106 }
107
108 ExchangeType::Bybit => {
109 let auth = BybitAuth::new(api_key, secret_key);
110 Ok(Arc::new(BybitAuthAdapter::new(auth)))
111 }
112
113 ExchangeType::Coinbase => {
114 if let Some(_passphrase) = passphrase {
115 let auth = CoinbaseAuth::new_hmac(api_key, secret_key);
117 Ok(Arc::new(CoinbaseAuthAdapter::new(auth)))
118 } else {
119 let auth = CoinbaseAuth::new_ecdsa(api_key, secret_key).map_err(|e| {
121 EMSError::auth(format!("Failed to create Coinbase ECDSA auth: {e}"))
122 })?;
123 Ok(Arc::new(CoinbaseAuthAdapter::new(auth)))
124 }
125 }
126
127 ExchangeType::Upbit => {
128 let config = rusty_common::auth::exchanges::upbit::UpbitAuthConfig {
129 access_key: api_key,
130 secret_key,
131 };
132 let auth = UpbitAuth::new(config);
133 Ok(Arc::new(UpbitAuthAdapter::new(auth)))
134 }
135
136 ExchangeType::Bithumb => {
137 let auth = BithumbAuth::new(api_key, secret_key);
138 Ok(Arc::new(BithumbAuthAdapter::new(auth)))
139 }
140 }
141}
142
143pub fn create_binance_ed25519_adapter(
145 api_key: impl Into<SmartString>,
146 private_key_base64: impl Into<SmartString>,
147) -> Result<Arc<dyn AuthenticationManager>> {
148 let auth = BinanceAuth::new_ed25519(api_key.into(), private_key_base64.into())
149 .map_err(|e| EMSError::auth(format!("Failed to create Binance Ed25519 auth: {e}")))?;
150 Ok(Arc::new(BinanceAuthAdapter::new(auth)))
151}
152
153pub struct AuthConfigBuilder {
155 exchange_type: Option<ExchangeType>,
156 api_key: Option<SmartString>,
157 secret_key: Option<SmartString>,
158 passphrase: Option<SmartString>,
159 use_ed25519: bool,
160}
161
162impl AuthConfigBuilder {
163 #[must_use]
165 pub const fn new() -> Self {
166 Self {
167 exchange_type: None,
168 api_key: None,
169 secret_key: None,
170 passphrase: None,
171 use_ed25519: false,
172 }
173 }
174
175 #[must_use]
177 pub const fn exchange(mut self, exchange_type: ExchangeType) -> Self {
178 self.exchange_type = Some(exchange_type);
179 self
180 }
181
182 pub fn exchange_str(mut self, exchange: &str) -> Result<Self> {
184 self.exchange_type = Some(ExchangeType::from_str(exchange)?);
185 Ok(self)
186 }
187
188 pub fn api_key(mut self, api_key: impl Into<SmartString>) -> Self {
190 self.api_key = Some(api_key.into());
191 self
192 }
193
194 pub fn secret_key(mut self, secret_key: impl Into<SmartString>) -> Self {
196 self.secret_key = Some(secret_key.into());
197 self
198 }
199
200 pub fn passphrase(mut self, passphrase: impl Into<SmartString>) -> Self {
202 self.passphrase = Some(passphrase.into());
203 self
204 }
205
206 #[must_use]
208 pub const fn use_ed25519(mut self) -> Self {
209 self.use_ed25519 = true;
210 self
211 }
212
213 pub fn build(self) -> Result<Arc<dyn AuthenticationManager>> {
215 let exchange_type = self
216 .exchange_type
217 .ok_or_else(|| EMSError::invalid_params("Exchange type not specified"))?;
218 let api_key = self
219 .api_key
220 .ok_or_else(|| EMSError::invalid_params("API key not specified"))?;
221 let secret_key = self
222 .secret_key
223 .ok_or_else(|| EMSError::invalid_params("Secret key not specified"))?;
224
225 if self.use_ed25519 && exchange_type == ExchangeType::Binance {
226 create_binance_ed25519_adapter(api_key, secret_key)
227 } else {
228 create_auth_adapter(exchange_type, api_key, secret_key, self.passphrase)
229 }
230 }
231}
232
233impl Default for AuthConfigBuilder {
234 fn default() -> Self {
235 Self::new()
236 }
237}
238
239pub fn get_auth_method_info(adapter: &dyn AuthenticationManager) -> AuthMethodInfo {
241 let exchange = adapter.exchange_name();
242 let supports_ws_trading = adapter.supports_websocket_trading();
243 let requires_refresh = adapter.requires_refresh();
244 let refresh_interval = adapter.refresh_interval();
245
246 let method_type = match exchange.to_lowercase().as_str() {
247 "binance" => {
248 if supports_ws_trading {
249 "Ed25519".into()
250 } else {
251 "HMAC-SHA256".into()
252 }
253 }
254 "bybit" => "HMAC-SHA256".into(),
255 "coinbase" => "ECDSA/JWT".into(),
256 "upbit" | "bithumb" => "JWT-HMAC".into(),
257 _ => "Unknown".into(),
258 };
259
260 AuthMethodInfo {
261 exchange_name: exchange.into(),
262 method_type,
263 supports_websocket_trading: supports_ws_trading,
264 requires_refresh,
265 refresh_interval,
266 }
267}
268
269#[derive(Debug, Clone)]
271pub struct AuthMethodInfo {
272 pub exchange_name: SmartString,
274 pub method_type: SmartString,
276 pub supports_websocket_trading: bool,
278 pub requires_refresh: bool,
280 pub refresh_interval: Option<std::time::Duration>,
282}
283
284pub fn validate_auth_config(
286 exchange_type: ExchangeType,
287 api_key: &str,
288 secret_key: &str,
289 passphrase: Option<&str>,
290) -> Result<()> {
291 if api_key.is_empty() {
293 return Err(EMSError::invalid_params("API key cannot be empty"));
294 }
295
296 if secret_key.is_empty() {
297 return Err(EMSError::invalid_params("Secret key cannot be empty"));
298 }
299
300 match exchange_type {
302 ExchangeType::Coinbase => {
303 if let Some(phrase) = passphrase {
305 if phrase.is_empty() {
307 return Err(EMSError::invalid_params(
308 "Coinbase passphrase cannot be empty",
309 ));
310 }
311 } else {
312 validate_pem_format(secret_key)
314 .map_err(|e| EMSError::invalid_params(format!("PEM validation failed: {e}")))?;
315 }
316 }
317
318 ExchangeType::Binance => {
319 if secret_key.len() < 32 {
321 return Err(EMSError::invalid_params(
322 "Binance secret key appears too short",
323 ));
324 }
325 }
326
327 ExchangeType::Upbit | ExchangeType::Bithumb => {
328 if api_key.len() < 10 {
330 return Err(EMSError::invalid_params(
331 "Korean exchange API key appears too short",
332 ));
333 }
334 }
335
336 ExchangeType::Bybit => {
337 if secret_key.len() < 20 {
339 return Err(EMSError::invalid_params(
340 "Bybit secret key appears too short",
341 ));
342 }
343 }
344 }
345
346 Ok(())
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[test]
354 fn test_exchange_type_parsing() {
355 assert_eq!(
356 ExchangeType::from_str("binance").unwrap(),
357 ExchangeType::Binance
358 );
359 assert_eq!(
360 ExchangeType::from_str("BYBIT").unwrap(),
361 ExchangeType::Bybit
362 );
363 assert_eq!(
364 ExchangeType::from_str("Coinbase").unwrap(),
365 ExchangeType::Coinbase
366 );
367
368 assert!(ExchangeType::from_str("invalid").is_err());
369 }
370
371 #[test]
372 fn test_auth_config_builder() {
373 let builder = AuthConfigBuilder::new()
375 .exchange(ExchangeType::Binance)
376 .api_key("test_api_key")
377 .secret_key("test_secret_key_longer_than_32_chars_here");
378
379 assert!(builder.build().is_ok());
381
382 let invalid_pem_builder = AuthConfigBuilder::new()
384 .exchange(ExchangeType::Coinbase)
385 .api_key("test_api_key")
386 .secret_key("invalid_pem_key");
387
388 assert!(invalid_pem_builder.build().is_err());
390
391 let valid_pem_builder = AuthConfigBuilder::new()
393 .exchange(ExchangeType::Coinbase)
394 .api_key("test_api_key")
395 .secret_key(
396 "-----BEGIN PRIVATE KEY-----\nVEVTVCBLRVkgREFUQQ==\n-----END PRIVATE KEY-----",
397 );
398
399 let result = valid_pem_builder.build();
401 if let Err(e) = &result {
402 eprintln!("PEM validation failed: {e:?}");
403 }
404 assert!(result.is_ok());
405 }
406
407 #[test]
408 fn test_auth_validation() {
409 assert!(
411 validate_auth_config(
412 ExchangeType::Binance,
413 "test_api_key",
414 "test_secret_key_longer_than_32_chars_here",
415 None
416 )
417 .is_ok()
418 );
419
420 assert!(validate_auth_config(ExchangeType::Binance, "", "test_secret", None).is_err());
422
423 assert!(
424 validate_auth_config(ExchangeType::Coinbase, "test_api", "short", Some("")).is_err()
425 );
426 }
427}