rusty_common/auth/exchanges/
bybit.rs

1//! Bybit V5 authentication implementation - Performance optimized for HFT
2//!
3//! This module provides high-performance HMAC-SHA256 authentication for Bybit V5 API.
4//! Supports both REST API and WebSocket authentication with zero-copy operations.
5
6use crate::collections::FxHashMap;
7use crate::{Result, SmartString};
8use hmac::{Hmac, Mac};
9use serde::Serialize;
10use sha2::Sha256;
11
12type HmacSha256 = Hmac<Sha256>;
13
14/// Bybit V5 API header key constants for type safety and performance
15pub mod header_keys {
16    use crate::SmartString;
17
18    /// The header key for the API key.
19    pub const X_BAPI_API_KEY: &str = "X-BAPI-API-KEY";
20    /// The header key for the timestamp.
21    pub const X_BAPI_TIMESTAMP: &str = "X-BAPI-TIMESTAMP";
22    /// The header key for the signature.
23    pub const X_BAPI_SIGN: &str = "X-BAPI-SIGN";
24    /// The header key for the receive window.
25    pub const X_BAPI_RECV_WINDOW: &str = "X-BAPI-RECV-WINDOW";
26    /// The header key for the content type.
27    pub const CONTENT_TYPE: &str = "Content-Type";
28    /// The value for the application/json content type.
29    pub const APPLICATION_JSON: &str = "application/json";
30    /// The value for the application/x-www-form-urlencoded content type.
31    pub const APPLICATION_FORM: &str = "application/x-www-form-urlencoded";
32
33    /// Pre-allocated SmartString constants for zero-allocation header creation
34    #[must_use]
35    pub fn x_bapi_api_key() -> SmartString {
36        X_BAPI_API_KEY.into()
37    }
38
39    /// Returns the `X-BAPI-TIMESTAMP` header key as a `SmartString`.
40    #[must_use]
41    pub fn x_bapi_timestamp() -> SmartString {
42        X_BAPI_TIMESTAMP.into()
43    }
44
45    /// Returns the `X-BAPI-SIGN` header key as a `SmartString`.
46    #[must_use]
47    pub fn x_bapi_sign() -> SmartString {
48        X_BAPI_SIGN.into()
49    }
50
51    /// Returns the `X-BAPI-RECV-WINDOW` header key as a `SmartString`.
52    #[must_use]
53    pub fn x_bapi_recv_window() -> SmartString {
54        X_BAPI_RECV_WINDOW.into()
55    }
56
57    /// Returns the `Content-Type` header value as a `SmartString`.
58    #[must_use]
59    pub fn content_type() -> SmartString {
60        CONTENT_TYPE.into()
61    }
62
63    /// Returns the `application/json` content type as a `SmartString`.
64    #[must_use]
65    pub fn application_json() -> SmartString {
66        APPLICATION_JSON.into()
67    }
68
69    /// Returns the `application/x-www-form-urlencoded` content type as a `SmartString`.
70    #[must_use]
71    pub fn application_form() -> SmartString {
72        APPLICATION_FORM.into()
73    }
74}
75
76/// WebSocket authentication message for Bybit V5
77#[derive(Debug, Clone, Serialize)]
78pub struct BybitWsAuthMessage {
79    /// Optional request ID to correlate requests and responses.
80    pub req_id: Option<SmartString>,
81    /// The operation to be performed.
82    pub op: SmartString,
83    /// The arguments for the authentication request: API key, expires timestamp, and signature.
84    pub args: [SmartString; 3],
85}
86
87/// WebSocket trading message for Bybit V5
88#[derive(Debug, Clone, Serialize)]
89pub struct BybitWsTradingMessage {
90    /// The request ID to correlate requests and responses.
91    #[serde(rename = "reqId")]
92    pub req_id: SmartString,
93    /// The header for the WebSocket message.
94    pub header: BybitWsMessageHeader,
95    /// The operation to be performed.
96    pub op: SmartString,
97    /// The arguments for the trading operation.
98    pub args: Vec<simd_json::OwnedValue>,
99}
100
101/// WebSocket message header for trading operations
102#[derive(Debug, Clone, Serialize)]
103pub struct BybitWsMessageHeader {
104    /// The timestamp for the message.
105    #[serde(rename = "X-BAPI-TIMESTAMP")]
106    pub timestamp: SmartString,
107    /// The receive window for the message.
108    #[serde(rename = "X-BAPI-RECV-WINDOW")]
109    pub recv_window: SmartString,
110}
111
112/// Bybit V5 authentication implementation
113#[derive(Debug, Clone)]
114pub struct BybitAuth {
115    api_key: SmartString,
116    secret_key: SmartString,
117    recv_window: u64,
118}
119
120impl BybitAuth {
121    /// Create new Bybit V5 auth instance
122    #[must_use]
123    pub const fn new(api_key: SmartString, secret_key: SmartString) -> Self {
124        Self {
125            api_key,
126            secret_key,
127            recv_window: 8000, // Default 8-second receive window
128        }
129    }
130
131    /// Create new Bybit V5 auth instance with custom receive window
132    #[must_use]
133    pub const fn with_recv_window(
134        api_key: SmartString,
135        secret_key: SmartString,
136        recv_window: u64,
137    ) -> Self {
138        Self {
139            api_key,
140            secret_key,
141            recv_window,
142        }
143    }
144
145    /// Get current timestamp in milliseconds
146    #[must_use]
147    pub fn get_timestamp() -> u64 {
148        // Use the improved time utility that handles clock adjustments properly
149        crate::time::get_timestamp_ms()
150    }
151
152    /// Generate signature for REST API requests
153    /// Format: timestamp + api_key + recv_window + params
154    pub fn generate_rest_signature(&self, timestamp: u64, params: &str) -> Result<SmartString> {
155        let string_to_sign = crate::safe_format!(
156            "{}{}{}{}",
157            timestamp,
158            self.api_key,
159            self.recv_window,
160            params
161        );
162        let mut mac = HmacSha256::new_from_slice(self.secret_key.as_bytes()).map_err(|e| {
163            crate::error::CommonError::Auth(crate::safe_format!("HMAC initialization failed: {e}"))
164        })?;
165        mac.update(string_to_sign.as_bytes());
166        let result = mac.finalize();
167        Ok(hex::encode(result.into_bytes()).into())
168    }
169
170    /// Generate signature for WebSocket authentication
171    /// Format: GET/realtime{expires}
172    pub fn generate_ws_signature(&self, expires: u64) -> Result<SmartString> {
173        let string_to_sign = crate::safe_format!("GET/realtime{expires}");
174        let mut mac = HmacSha256::new_from_slice(self.secret_key.as_bytes()).map_err(|e| {
175            crate::error::CommonError::Auth(crate::safe_format!("HMAC initialization failed: {e}"))
176        })?;
177        mac.update(string_to_sign.as_bytes());
178        let result = mac.finalize();
179        Ok(hex::encode(result.into_bytes()).into())
180    }
181
182    /// Create REST API headers for authenticated requests
183    pub fn create_rest_headers(
184        &self,
185        timestamp: u64,
186        params: &str,
187    ) -> Result<FxHashMap<SmartString, SmartString>> {
188        let signature = self.generate_rest_signature(timestamp, params)?;
189        let mut headers = FxHashMap::default();
190        headers.insert(header_keys::x_bapi_api_key(), self.api_key.clone());
191        headers.insert(
192            header_keys::x_bapi_timestamp(),
193            timestamp.to_string().into(),
194        );
195        headers.insert(
196            header_keys::x_bapi_recv_window(),
197            self.recv_window.to_string().into(),
198        );
199        headers.insert(header_keys::x_bapi_sign(), signature);
200        headers.insert(header_keys::content_type(), header_keys::application_json());
201        Ok(headers)
202    }
203
204    /// Create WebSocket authentication message
205    pub fn create_ws_auth_message(
206        &self,
207        req_id: Option<SmartString>,
208    ) -> Result<BybitWsAuthMessage> {
209        let expires = Self::get_timestamp() + 10_000; // 10 seconds from now
210        let signature = self.generate_ws_signature(expires)?;
211
212        Ok(BybitWsAuthMessage {
213            req_id,
214            op: "auth".into(),
215            args: [self.api_key.clone(), expires.to_string().into(), signature],
216        })
217    }
218
219    /// Create WebSocket trading message header
220    #[must_use]
221    pub fn create_ws_trading_header(&self, timestamp: u64) -> BybitWsMessageHeader {
222        BybitWsMessageHeader {
223            timestamp: timestamp.to_string().into(),
224            recv_window: self.recv_window.to_string().into(),
225        }
226    }
227
228    /// Create WebSocket trading message
229    pub fn create_ws_trading_message(
230        &self,
231        req_id: SmartString,
232        operation: SmartString,
233        args: Vec<simd_json::OwnedValue>,
234    ) -> BybitWsTradingMessage {
235        let timestamp = Self::get_timestamp();
236        let header = self.create_ws_trading_header(timestamp);
237
238        BybitWsTradingMessage {
239            req_id,
240            header,
241            op: operation,
242            args,
243        }
244    }
245
246    /// Validate timestamp to prevent replay attacks
247    #[must_use]
248    pub fn is_timestamp_valid(&self, timestamp: u64) -> bool {
249        let now = Self::get_timestamp();
250        let diff = now.abs_diff(timestamp);
251        diff <= self.recv_window
252    }
253
254    /// Get API key for external use
255    #[must_use]
256    pub const fn api_key(&self) -> &SmartString {
257        &self.api_key
258    }
259
260    /// Get receive window for external use
261    #[must_use]
262    pub const fn recv_window(&self) -> u64 {
263        self.recv_window
264    }
265
266    /// Generate headers for REST API requests (compatibility method)
267    ///
268    /// This method provides compatibility with the expected signature for exchange implementations.
269    /// It generates a timestamp internally and creates the parameter string for signing.
270    ///
271    /// For GET requests, query parameters from the path are used for signing.
272    /// For POST/PUT/DELETE requests, the body is used for signing.
273    pub fn generate_headers(
274        &self,
275        method: &str,
276        path: &str,
277        body: Option<&str>,
278    ) -> Result<crate::collections::FxHashMap<SmartString, SmartString>> {
279        let timestamp = Self::get_timestamp();
280        let params = if method == "GET" {
281            // For GET requests, extract query parameters from path
282            if let Some(query_start) = path.find('?') {
283                &path[query_start + 1..]
284            } else {
285                ""
286            }
287        } else {
288            // For POST/PUT/DELETE requests, use body
289            body.unwrap_or("")
290        };
291        self.create_rest_headers(timestamp, params)
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn test_rest_signature_generation() {
301        let auth = BybitAuth::new("test_key".into(), "test_secret".into());
302        let timestamp = 1_672_916_271_123;
303        let params = r#"{"category":"spot","symbol":"BTCUSDT"}"#;
304
305        let signature = auth.generate_rest_signature(timestamp, params).unwrap();
306        assert!(!signature.is_empty());
307        assert_eq!(signature.len(), 64); // HMAC-SHA256 produces 64 hex chars
308    }
309
310    #[test]
311    fn test_ws_signature_generation() {
312        let auth = BybitAuth::new("test_key".into(), "test_secret".into());
313        let expires = 1_672_916_271_123;
314
315        let signature = auth.generate_ws_signature(expires).unwrap();
316        assert!(!signature.is_empty());
317        assert_eq!(signature.len(), 64); // HMAC-SHA256 produces 64 hex chars
318    }
319
320    #[test]
321    fn test_rest_headers_creation() {
322        let auth = BybitAuth::new("test_key".into(), "test_secret".into());
323        let timestamp = 1_672_916_271_123;
324        let params = "";
325
326        let headers = auth.create_rest_headers(timestamp, params).unwrap();
327        assert!(headers.contains_key(&header_keys::x_bapi_api_key()));
328        assert!(headers.contains_key(&header_keys::x_bapi_timestamp()));
329        assert!(headers.contains_key(&header_keys::x_bapi_sign()));
330        assert!(headers.contains_key(&header_keys::x_bapi_recv_window()));
331        assert!(headers.contains_key(&header_keys::content_type()));
332    }
333
334    #[test]
335    fn test_ws_auth_message_creation() {
336        let auth = BybitAuth::new("test_key".into(), "test_secret".into());
337        let req_id = Some("test_123".into());
338
339        let message = auth.create_ws_auth_message(req_id).unwrap();
340        assert_eq!(message.op, "auth");
341        assert_eq!(message.args[0], *auth.api_key());
342        assert!(!message.args[1].is_empty()); // expires
343        assert!(!message.args[2].is_empty()); // signature
344    }
345
346    #[test]
347    fn test_ws_trading_message_creation() {
348        let auth = BybitAuth::new("test_key".into(), "test_secret".into());
349        let req_id = "test_order_123".into();
350        let operation = "order.create".into();
351        let args = vec![simd_json::json!({"symbol": "BTCUSDT", "side": "Buy"})];
352
353        let message = auth.create_ws_trading_message(req_id, operation, args);
354        assert_eq!(message.req_id, "test_order_123");
355        assert_eq!(message.op, "order.create");
356        assert_eq!(message.args.len(), 1);
357        assert!(!message.header.timestamp.is_empty());
358        assert!(!message.header.recv_window.is_empty());
359    }
360
361    #[test]
362    fn test_timestamp_validation() {
363        let auth = BybitAuth::new("test_key".into(), "test_secret".into());
364        let now = BybitAuth::get_timestamp();
365
366        // Valid timestamps
367        assert!(auth.is_timestamp_valid(now));
368        assert!(auth.is_timestamp_valid(now + 1000)); // 1 second later
369        assert!(auth.is_timestamp_valid(now - 1000)); // 1 second earlier
370
371        // Invalid timestamps
372        assert!(!auth.is_timestamp_valid(now + 10_000)); // 10 seconds later (beyond recv_window)
373        assert!(!auth.is_timestamp_valid(now - 10_000)); // 10 seconds earlier (beyond recv_window)
374    }
375
376    #[test]
377    fn test_custom_recv_window() {
378        let auth = BybitAuth::with_recv_window("test_key".into(), "test_secret".into(), 5000);
379        assert_eq!(auth.recv_window(), 5000);
380
381        let now = BybitAuth::get_timestamp();
382        assert!(auth.is_timestamp_valid(now + 4000)); // Valid within 5s window
383        assert!(!auth.is_timestamp_valid(now + 6000)); // Invalid beyond 5s window
384    }
385}