rusty_ems/auth/
adapters.rs

1//! Authentication adapters for different exchanges
2
3use super::traits::{
4    AuthenticationAdapter, AuthenticationContext, AuthenticationManager, AuthenticationResult,
5};
6use crate::error::{EMSError, Result};
7use async_trait::async_trait;
8use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
9use rusty_common::SmartString;
10use rusty_common::auth::ExchangeAuth;
11use rusty_common::auth::exchanges::{
12    binance::BinanceAuth, bithumb::BithumbAuth, bybit::BybitAuth, coinbase::CoinbaseAuth,
13    upbit::UpbitAuth,
14};
15use std::any::Any;
16use std::time::Duration;
17
18/// Binance authentication adapter
19pub struct BinanceAuthAdapter {
20    auth: BinanceAuth,
21    exchange_name: SmartString,
22}
23
24impl BinanceAuthAdapter {
25    /// Create a new Binance authentication adapter
26    #[must_use]
27    pub fn new(auth: BinanceAuth) -> Self {
28        Self {
29            auth,
30            exchange_name: "Binance".into(),
31        }
32    }
33}
34
35#[async_trait]
36impl AuthenticationManager for BinanceAuthAdapter {
37    async fn authenticate_rest_request(
38        &self,
39        context: &AuthenticationContext,
40    ) -> Result<AuthenticationResult> {
41        // Convert query params to the format expected by Binance auth
42        let params = context.query_params.as_ref().map(|p| {
43            p.iter()
44                .map(|(k, v)| (k.as_str(), v.as_str()))
45                .collect::<Vec<_>>()
46        });
47
48        let headers = self
49            .auth
50            .generate_headers(
51                &context.method,
52                &context.path,
53                params.as_deref(),
54                context.body.as_deref(),
55            )
56            .map_err(|e| EMSError::auth_exchange_failed("Binance", &e))?;
57
58        // Convert to reqwest HeaderMap
59        let mut header_map = HeaderMap::new();
60        for (key, value) in headers {
61            let header_name = HeaderName::from_bytes(key.as_bytes())
62                .map_err(|e| EMSError::auth_invalid_header_name(&e))?;
63            let header_value = HeaderValue::from_str(value.as_str())
64                .map_err(|e| EMSError::auth_invalid_header_value(&e))?;
65            header_map.insert(header_name, header_value);
66        }
67
68        let mut result = AuthenticationResult::new(header_map);
69
70        // Add signed query if applicable
71        if let Some(params) = params
72            && let Ok(signed_query) = self.auth.generate_signed_query_string(Some(&params))
73        {
74            result = result.with_signed_query(signed_query);
75        }
76
77        Ok(result)
78    }
79
80    async fn authenticate_websocket(&self) -> Result<AuthenticationResult> {
81        // Binance WebSocket typically doesn't require authentication for market data
82        // But user data streams require listen keys, which are handled separately
83        Ok(AuthenticationResult::new(HeaderMap::new()))
84    }
85
86    async fn authenticate_websocket_trading(
87        &self,
88        timestamp: Option<u64>,
89    ) -> Result<AuthenticationResult> {
90        // Only Ed25519 auth supports WebSocket trading for Binance
91        // For now, return error as we can't determine auth type
92        Err(EMSError::not_supported(
93            self.exchange_name.clone(),
94            "WebSocket trading authentication (requires Ed25519)",
95        ))
96    }
97
98    fn is_authentication_valid(&self) -> bool {
99        true // HMAC/Ed25519 doesn't expire
100    }
101
102    fn time_until_expiration(&self) -> Option<Duration> {
103        None // HMAC/Ed25519 doesn't expire
104    }
105
106    fn exchange_name(&self) -> &str {
107        &self.exchange_name
108    }
109
110    fn api_key(&self) -> &str {
111        self.auth.api_key()
112    }
113
114    fn supports_websocket_trading(&self) -> bool {
115        // This would need to check if it's Ed25519 auth
116        // For now, return false
117        false
118    }
119}
120
121impl AuthenticationAdapter for BinanceAuthAdapter {
122    fn underlying_auth(&self) -> &dyn Any {
123        &self.auth
124    }
125}
126
127/// Bybit authentication adapter
128pub struct BybitAuthAdapter {
129    auth: BybitAuth,
130    exchange_name: SmartString,
131}
132
133impl BybitAuthAdapter {
134    /// Create a new Bybit authentication adapter
135    #[must_use]
136    pub fn new(auth: BybitAuth) -> Self {
137        Self {
138            auth,
139            exchange_name: "Bybit".into(),
140        }
141    }
142}
143
144#[async_trait]
145impl AuthenticationManager for BybitAuthAdapter {
146    async fn authenticate_rest_request(
147        &self,
148        context: &AuthenticationContext,
149    ) -> Result<AuthenticationResult> {
150        let headers = self
151            .auth
152            .generate_headers(&context.method, &context.path, context.body.as_deref())
153            .map_err(|e| EMSError::auth_exchange_failed("Bybit", &e))?;
154
155        // Convert to reqwest HeaderMap
156        let mut header_map = HeaderMap::new();
157        for (key, value) in headers {
158            let header_name = HeaderName::from_bytes(key.as_bytes())
159                .map_err(|e| EMSError::auth_invalid_header_name(&e))?;
160            let header_value = HeaderValue::from_str(value.as_str())
161                .map_err(|e| EMSError::auth_invalid_header_value(&e))?;
162            header_map.insert(header_name, header_value);
163        }
164
165        Ok(AuthenticationResult::new(header_map))
166    }
167
168    async fn authenticate_websocket(&self) -> Result<AuthenticationResult> {
169        Ok(AuthenticationResult::new(HeaderMap::new()))
170    }
171
172    async fn authenticate_websocket_trading(
173        &self,
174        timestamp: Option<u64>,
175    ) -> Result<AuthenticationResult> {
176        let timestamp = timestamp.unwrap_or_else(BybitAuth::get_timestamp);
177        let header = self.auth.create_ws_trading_header(timestamp);
178
179        let ws_message = simd_json::json!({
180            "req_id": "auth",
181            "op": "auth",
182            "args": [header]
183        })
184        .to_string();
185
186        let result = AuthenticationResult::new(HeaderMap::new()).with_ws_auth_message(ws_message);
187
188        Ok(result)
189    }
190
191    fn is_authentication_valid(&self) -> bool {
192        true // HMAC doesn't expire
193    }
194
195    fn time_until_expiration(&self) -> Option<Duration> {
196        None
197    }
198
199    fn exchange_name(&self) -> &str {
200        &self.exchange_name
201    }
202
203    fn api_key(&self) -> &str {
204        self.auth.api_key()
205    }
206
207    fn supports_websocket_trading(&self) -> bool {
208        true
209    }
210}
211
212impl AuthenticationAdapter for BybitAuthAdapter {
213    fn underlying_auth(&self) -> &dyn Any {
214        &self.auth
215    }
216}
217
218/// Coinbase authentication adapter
219pub struct CoinbaseAuthAdapter {
220    auth: CoinbaseAuth,
221    exchange_name: SmartString,
222}
223
224impl CoinbaseAuthAdapter {
225    /// Create a new Coinbase authentication adapter
226    #[must_use]
227    pub fn new(auth: CoinbaseAuth) -> Self {
228        Self {
229            auth,
230            exchange_name: "Coinbase".into(),
231        }
232    }
233}
234
235#[async_trait]
236impl AuthenticationManager for CoinbaseAuthAdapter {
237    async fn authenticate_rest_request(
238        &self,
239        context: &AuthenticationContext,
240    ) -> Result<AuthenticationResult> {
241        let headers = self
242            .auth
243            .generate_headers(&context.method, &context.path, context.body.as_deref())
244            .map_err(|e| EMSError::auth(format!("Coinbase auth failed: {e}")))?;
245
246        // Convert to reqwest HeaderMap
247        let mut header_map = HeaderMap::new();
248        for (key, value) in headers {
249            let header_name = HeaderName::from_bytes(key.as_bytes())
250                .map_err(|e| EMSError::auth_invalid_header_name(&e))?;
251            let header_value = HeaderValue::from_str(value.as_str())
252                .map_err(|e| EMSError::auth_invalid_header_value(&e))?;
253            header_map.insert(header_name, header_value);
254        }
255
256        Ok(AuthenticationResult::new(header_map))
257    }
258
259    async fn authenticate_websocket(&self) -> Result<AuthenticationResult> {
260        let jwt_token = self
261            .auth
262            .generate_jwt()
263            .map_err(|e| EMSError::auth(format!("Coinbase JWT generation failed: {e}")))?;
264
265        let result = AuthenticationResult::new(HeaderMap::new())
266            .with_ws_auth_message(jwt_token)
267            .with_validity_duration(Duration::from_secs(3600)); // JWT typically valid for 1 hour
268
269        Ok(result)
270    }
271
272    fn is_authentication_valid(&self) -> bool {
273        true // Implementation should check JWT expiration
274    }
275
276    fn time_until_expiration(&self) -> Option<Duration> {
277        Some(Duration::from_secs(3600)) // JWT default expiration
278    }
279
280    fn exchange_name(&self) -> &str {
281        &self.exchange_name
282    }
283
284    fn api_key(&self) -> &str {
285        self.auth.api_key()
286    }
287}
288
289impl AuthenticationAdapter for CoinbaseAuthAdapter {
290    fn underlying_auth(&self) -> &dyn Any {
291        &self.auth
292    }
293}
294
295/// Upbit authentication adapter
296pub struct UpbitAuthAdapter {
297    auth: UpbitAuth,
298    exchange_name: SmartString,
299}
300
301impl UpbitAuthAdapter {
302    /// Create a new Upbit authentication adapter
303    #[must_use]
304    pub fn new(auth: UpbitAuth) -> Self {
305        Self {
306            auth,
307            exchange_name: "Upbit".into(),
308        }
309    }
310}
311
312#[async_trait]
313impl AuthenticationManager for UpbitAuthAdapter {
314    async fn authenticate_rest_request(
315        &self,
316        context: &AuthenticationContext,
317    ) -> Result<AuthenticationResult> {
318        // Convert query params for Upbit
319        let params = context.query_params.as_ref().and_then(|p| {
320            if p.is_empty() {
321                None
322            } else {
323                Some((p[0].0.as_str(), p[0].1.as_str()))
324            }
325        });
326
327        let headers = self
328            .auth
329            .generate_authentication_headers(&context.method, &context.path, params)
330            .map_err(|e| EMSError::auth(format!("Upbit auth failed: {e}")))?;
331
332        // Convert to reqwest HeaderMap
333        let mut header_map = HeaderMap::new();
334        for (key, value) in headers {
335            let header_name = HeaderName::from_bytes(key.as_bytes())
336                .map_err(|e| EMSError::auth_invalid_header_name(&e))?;
337            let header_value = HeaderValue::from_str(value.as_str())
338                .map_err(|e| EMSError::auth_invalid_header_value(&e))?;
339            header_map.insert(header_name, header_value);
340        }
341
342        Ok(AuthenticationResult::new(header_map))
343    }
344
345    async fn authenticate_websocket(&self) -> Result<AuthenticationResult> {
346        let ws_auth = self
347            .auth
348            .generate_websocket_authentication()
349            .map_err(|e| EMSError::auth(format!("Upbit WS auth failed: {e}")))?;
350
351        let result = AuthenticationResult::new(HeaderMap::new())
352            .with_ws_auth_message(ws_auth)
353            .with_validity_duration(Duration::from_secs(3600));
354
355        Ok(result)
356    }
357
358    fn is_authentication_valid(&self) -> bool {
359        true // Implementation should check JWT expiration
360    }
361
362    fn time_until_expiration(&self) -> Option<Duration> {
363        Some(Duration::from_secs(3600))
364    }
365
366    fn exchange_name(&self) -> &str {
367        &self.exchange_name
368    }
369
370    fn api_key(&self) -> &str {
371        self.auth.api_key()
372    }
373
374    fn requires_refresh(&self) -> bool {
375        true
376    }
377
378    fn refresh_interval(&self) -> Option<Duration> {
379        Some(Duration::from_secs(3000)) // Refresh every 50 minutes
380    }
381}
382
383impl AuthenticationAdapter for UpbitAuthAdapter {
384    fn underlying_auth(&self) -> &dyn Any {
385        &self.auth
386    }
387}
388
389/// Bithumb authentication adapter
390pub struct BithumbAuthAdapter {
391    auth: BithumbAuth,
392    exchange_name: SmartString,
393}
394
395impl BithumbAuthAdapter {
396    /// Create a new Bithumb authentication adapter
397    #[must_use]
398    pub fn new(auth: BithumbAuth) -> Self {
399        Self {
400            auth,
401            exchange_name: "Bithumb".into(),
402        }
403    }
404}
405
406#[async_trait]
407impl AuthenticationManager for BithumbAuthAdapter {
408    async fn authenticate_rest_request(
409        &self,
410        context: &AuthenticationContext,
411    ) -> Result<AuthenticationResult> {
412        // Convert query params to the format expected by Bithumb
413        let params = context.query_params.as_ref().map(|p| {
414            p.iter()
415                .map(|(k, v)| (k.as_str(), v.as_str()))
416                .collect::<Vec<_>>()
417        });
418
419        let headers = self
420            .auth
421            .generate_headers(&context.method, &context.path, params.as_deref())
422            .map_err(|e| EMSError::auth(format!("Bithumb auth failed: {e}")))?;
423
424        // Convert to reqwest HeaderMap
425        let mut header_map = HeaderMap::new();
426        for (key, value) in headers {
427            let header_name = HeaderName::from_bytes(key.as_bytes())
428                .map_err(|e| EMSError::auth_invalid_header_name(&e))?;
429            let header_value = HeaderValue::from_str(value.as_str())
430                .map_err(|e| EMSError::auth_invalid_header_value(&e))?;
431            header_map.insert(header_name, header_value);
432        }
433
434        Ok(AuthenticationResult::new(header_map))
435    }
436
437    async fn authenticate_websocket(&self) -> Result<AuthenticationResult> {
438        // Bithumb uses JWT for WebSocket authentication
439        let ws_auth = self
440            .auth
441            .generate_ws_auth()
442            .map_err(|e| EMSError::auth(format!("Bithumb WS auth failed: {e}")))?;
443
444        let result = AuthenticationResult::new(HeaderMap::new())
445            .with_ws_auth_message(ws_auth)
446            .with_validity_duration(Duration::from_secs(3600));
447
448        Ok(result)
449    }
450
451    fn is_authentication_valid(&self) -> bool {
452        true // Implementation should check JWT expiration
453    }
454
455    fn time_until_expiration(&self) -> Option<Duration> {
456        Some(Duration::from_secs(3600))
457    }
458
459    fn exchange_name(&self) -> &str {
460        &self.exchange_name
461    }
462
463    fn api_key(&self) -> &str {
464        self.auth.api_key()
465    }
466
467    fn requires_refresh(&self) -> bool {
468        true
469    }
470
471    fn refresh_interval(&self) -> Option<Duration> {
472        Some(Duration::from_secs(3000)) // Refresh every 50 minutes
473    }
474}
475
476impl AuthenticationAdapter for BithumbAuthAdapter {
477    fn underlying_auth(&self) -> &dyn Any {
478        &self.auth
479    }
480}