rusty_feeder/exchange/bybit/
auth.rs

1/*
2 * Bybit authentication implementation
3 * Supports HMAC SHA256 and RSA SHA256 signature generation for API v5
4 */
5
6use anyhow::{Result, anyhow};
7use hmac::{Hmac, Mac};
8use sha2::Sha256;
9use smartstring::alias::String;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12/// Type alias for HMAC-SHA256
13type HmacSha256 = Hmac<Sha256>;
14
15/// Bybit authentication method
16#[derive(Debug, Clone)]
17pub enum AuthMethod {
18    /// HMAC SHA256 (default)
19    HmacSha256,
20    /// RSA SHA256 (requires RSA private key)
21    RsaSha256,
22}
23
24/// Bybit authentication configuration
25#[derive(Debug, Clone)]
26pub struct BybitAuthConfig {
27    /// API key
28    pub api_key: String,
29
30    /// API secret key (for HMAC) or RSA private key (for RSA)
31    pub api_secret: String,
32
33    /// Authentication method
34    pub auth_method: AuthMethod,
35
36    /// Receive window (milliseconds, default 5000)
37    pub recv_window: u64,
38}
39
40impl BybitAuthConfig {
41    /// Create a new authentication configuration with HMAC SHA256
42    #[must_use]
43    pub const fn new_hmac(api_key: String, api_secret: String) -> Self {
44        Self {
45            api_key,
46            api_secret,
47            auth_method: AuthMethod::HmacSha256,
48            recv_window: 5000,
49        }
50    }
51
52    /// Create a new authentication configuration with RSA SHA256
53    #[must_use]
54    pub const fn new_rsa(api_key: String, rsa_private_key: String) -> Self {
55        Self {
56            api_key,
57            api_secret: rsa_private_key,
58            auth_method: AuthMethod::RsaSha256,
59            recv_window: 5000,
60        }
61    }
62}
63
64/// Bybit authentication handler
65#[derive(Debug)]
66pub struct BybitAuth {
67    /// Authentication configuration
68    config: BybitAuthConfig,
69}
70
71impl BybitAuth {
72    /// Create a new authentication handler
73    #[must_use]
74    pub const fn new(config: BybitAuthConfig) -> Self {
75        Self { config }
76    }
77
78    /// Generate timestamp in milliseconds
79    #[inline]
80    pub fn generate_timestamp() -> u64 {
81        SystemTime::now()
82            .duration_since(UNIX_EPOCH)
83            .unwrap_or_default()
84            .as_millis() as u64
85    }
86
87    /// Generate HMAC SHA256 signature
88    ///
89    /// # Parameters
90    /// - `param_SmolStr`: The String to sign
91    ///
92    /// # Returns
93    /// Hexadecimal representation of the HMAC SHA256 signature
94    fn generate_hmac_signature(&self, param_string: &str) -> Result<String> {
95        let mut mac = HmacSha256::new_from_slice(self.config.api_secret.as_bytes())
96            .map_err(|e| anyhow!("Failed to create HMAC: {}", e))?;
97
98        mac.update(param_string.as_bytes());
99
100        let result = mac.finalize();
101        Ok(hex::encode(result.into_bytes().as_slice()).into())
102    }
103
104    /// Generate RSA SHA256 signature
105    ///
106    /// # Parameters
107    /// - `param_SmolStr`: The String to sign
108    ///
109    /// # Returns
110    /// Base64-encoded RSA SHA256 signature
111    fn generate_rsa_signature(&self, _param_string: &str) -> Result<String> {
112        // RSA implementation would require additional dependencies like `rsa` crate
113        // For now, return an error indicating RSA is not yet implemented
114        Err(anyhow!("RSA SHA256 authentication is not yet implemented"))
115    }
116
117    /// Generate signature for REST API requests
118    ///
119    /// # Parameters
120    /// - `timestamp`: Request timestamp in milliseconds
121    /// - `api_key`: API key
122    /// - `recv_window`: Receive window in milliseconds
123    /// - `query_params`: Sorted query parameters (for GET/DELETE)
124    /// - `body_json`: Request body JSON String (for POST/PUT)
125    ///
126    /// # Returns
127    /// Signature String
128    pub fn generate_signature(
129        &self,
130        timestamp: u64,
131        api_key: &str,
132        recv_window: u64,
133        query_params: Option<&str>,
134        body_json: Option<&str>,
135    ) -> Result<String> {
136        // Build the param String to sign
137        let param_string = match (query_params, body_json) {
138            (Some(params), None) => {
139                // GET/DELETE: timestamp + api_key + recv_window + query_params
140                format!("{timestamp}{api_key}{recv_window}{params}")
141            }
142            (None, Some(body)) => {
143                // POST/PUT: timestamp + api_key + recv_window + body_json
144                format!("{timestamp}{api_key}{recv_window}{body}")
145            }
146            (None, None) => {
147                // No params: timestamp + api_key + recv_window
148                format!("{timestamp}{api_key}{recv_window}")
149            }
150            (Some(_), Some(_)) => {
151                return Err(anyhow!("Cannot have both query params and body"));
152            }
153        };
154
155        match self.config.auth_method {
156            AuthMethod::HmacSha256 => self.generate_hmac_signature(&param_string),
157            AuthMethod::RsaSha256 => self.generate_rsa_signature(&param_string),
158        }
159    }
160
161    /// Generate authentication headers for REST API requests
162    ///
163    /// # Parameters
164    /// - `query_params`: Sorted query parameters (for GET/DELETE)
165    /// - `body_json`: Request body JSON String (for POST/PUT)
166    ///
167    /// # Returns
168    /// Tuple of (api_key, timestamp, sign, recv_window)
169    pub fn generate_auth_headers(
170        &self,
171        query_params: Option<&str>,
172        body_json: Option<&str>,
173    ) -> Result<(String, u64, String, u64)> {
174        let timestamp = Self::generate_timestamp();
175        let recv_window = self.config.recv_window;
176
177        let signature = self.generate_signature(
178            timestamp,
179            &self.config.api_key,
180            recv_window,
181            query_params,
182            body_json,
183        )?;
184
185        Ok((
186            self.config.api_key.clone(),
187            timestamp,
188            signature,
189            recv_window,
190        ))
191    }
192
193    /// Generate signature for WebSocket authentication
194    ///
195    /// # Parameters
196    /// - `expires`: Expiration timestamp in milliseconds
197    ///
198    /// # Returns
199    /// Signature for WebSocket auth
200    pub fn generate_websocket_signature(&self, expires: u64) -> Result<String> {
201        // For WebSocket: "GET/realtime" + expires
202        let param_string = format!("GET/realtime{expires}");
203
204        match self.config.auth_method {
205            AuthMethod::HmacSha256 => self.generate_hmac_signature(&param_string),
206            AuthMethod::RsaSha256 => self.generate_rsa_signature(&param_string),
207        }
208    }
209
210    /// Generate WebSocket authentication message
211    ///
212    /// # Returns
213    /// WebSocket auth arguments [api_key, expires, signature]
214    pub fn generate_websocket_auth(&self) -> Result<(String, u64, String)> {
215        // Generate expires timestamp (current + 5000ms)
216        let expires = Self::generate_timestamp() + 5000;
217
218        let signature = self.generate_websocket_signature(expires)?;
219
220        Ok((self.config.api_key.clone(), expires, signature))
221    }
222}
223
224/// Helper function to sort query parameters alphabetically
225pub fn sort_query_params(params: &[(&str, &str)]) -> String {
226    let mut sorted_params = params.to_vec();
227    sorted_params.sort_by(|a, b| a.0.cmp(b.0));
228
229    let params_vec: Vec<std::string::String> = sorted_params
230        .iter()
231        .map(|(k, v)| format!("{k}={v}"))
232        .collect();
233    params_vec.join("&").into()
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn test_timestamp_generation() {
242        let ts1 = BybitAuth::generate_timestamp();
243        std::thread::sleep(std::time::Duration::from_millis(1));
244        let ts2 = BybitAuth::generate_timestamp();
245
246        assert!(ts2 > ts1);
247    }
248
249    #[test]
250    fn test_hmac_signature_generation() {
251        let config = BybitAuthConfig::new_hmac("test_api_key".into(), "test_api_secret".into());
252
253        let auth = BybitAuth::new(config);
254
255        let timestamp = 1234567890123u64;
256        let api_key = "test_api_key";
257        let recv_window = 5000u64;
258        let query_params = Some("category=spot&symbol=BTCUSDT");
259
260        let signature = auth
261            .generate_signature(timestamp, api_key, recv_window, query_params, None)
262            .unwrap();
263
264        assert!(!signature.is_empty());
265        // Signature should be hex encoded
266        assert!(signature.chars().all(|c| c.is_ascii_hexdigit()));
267    }
268
269    #[test]
270    fn test_query_param_sorting() {
271        let params = vec![
272            ("symbol", "BTCUSDT"),
273            ("category", "spot"),
274            ("limit", "100"),
275        ];
276
277        let sorted = sort_query_params(&params);
278        assert_eq!(sorted, "category=spot&limit=100&symbol=BTCUSDT");
279    }
280
281    #[test]
282    fn test_websocket_signature() {
283        let config = BybitAuthConfig::new_hmac("test_api_key".into(), "test_api_secret".into());
284
285        let auth = BybitAuth::new(config);
286
287        let expires = 1234567890123u64;
288        let signature = auth.generate_websocket_signature(expires).unwrap();
289
290        assert!(!signature.is_empty());
291        assert!(signature.chars().all(|c| c.is_ascii_hexdigit()));
292    }
293}