rusty_feeder/exchange/bybit/
auth.rs1use anyhow::{Result, anyhow};
7use hmac::{Hmac, Mac};
8use sha2::Sha256;
9use smartstring::alias::String;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12type HmacSha256 = Hmac<Sha256>;
14
15#[derive(Debug, Clone)]
17pub enum AuthMethod {
18 HmacSha256,
20 RsaSha256,
22}
23
24#[derive(Debug, Clone)]
26pub struct BybitAuthConfig {
27 pub api_key: String,
29
30 pub api_secret: String,
32
33 pub auth_method: AuthMethod,
35
36 pub recv_window: u64,
38}
39
40impl BybitAuthConfig {
41 #[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 #[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#[derive(Debug)]
66pub struct BybitAuth {
67 config: BybitAuthConfig,
69}
70
71impl BybitAuth {
72 #[must_use]
74 pub const fn new(config: BybitAuthConfig) -> Self {
75 Self { config }
76 }
77
78 #[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 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 fn generate_rsa_signature(&self, _param_string: &str) -> Result<String> {
112 Err(anyhow!("RSA SHA256 authentication is not yet implemented"))
115 }
116
117 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 let param_string = match (query_params, body_json) {
138 (Some(params), None) => {
139 format!("{timestamp}{api_key}{recv_window}{params}")
141 }
142 (None, Some(body)) => {
143 format!("{timestamp}{api_key}{recv_window}{body}")
145 }
146 (None, None) => {
147 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(¶m_string),
157 AuthMethod::RsaSha256 => self.generate_rsa_signature(¶m_string),
158 }
159 }
160
161 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 pub fn generate_websocket_signature(&self, expires: u64) -> Result<String> {
201 let param_string = format!("GET/realtime{expires}");
203
204 match self.config.auth_method {
205 AuthMethod::HmacSha256 => self.generate_hmac_signature(¶m_string),
206 AuthMethod::RsaSha256 => self.generate_rsa_signature(¶m_string),
207 }
208 }
209
210 pub fn generate_websocket_auth(&self) -> Result<(String, u64, String)> {
215 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
224pub 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 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(¶ms);
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}