rusty_common/auth/exchanges/
bybit.rs1use crate::collections::FxHashMap;
7use crate::{Result, SmartString};
8use hmac::{Hmac, Mac};
9use serde::Serialize;
10use sha2::Sha256;
11
12type HmacSha256 = Hmac<Sha256>;
13
14pub mod header_keys {
16 use crate::SmartString;
17
18 pub const X_BAPI_API_KEY: &str = "X-BAPI-API-KEY";
20 pub const X_BAPI_TIMESTAMP: &str = "X-BAPI-TIMESTAMP";
22 pub const X_BAPI_SIGN: &str = "X-BAPI-SIGN";
24 pub const X_BAPI_RECV_WINDOW: &str = "X-BAPI-RECV-WINDOW";
26 pub const CONTENT_TYPE: &str = "Content-Type";
28 pub const APPLICATION_JSON: &str = "application/json";
30 pub const APPLICATION_FORM: &str = "application/x-www-form-urlencoded";
32
33 #[must_use]
35 pub fn x_bapi_api_key() -> SmartString {
36 X_BAPI_API_KEY.into()
37 }
38
39 #[must_use]
41 pub fn x_bapi_timestamp() -> SmartString {
42 X_BAPI_TIMESTAMP.into()
43 }
44
45 #[must_use]
47 pub fn x_bapi_sign() -> SmartString {
48 X_BAPI_SIGN.into()
49 }
50
51 #[must_use]
53 pub fn x_bapi_recv_window() -> SmartString {
54 X_BAPI_RECV_WINDOW.into()
55 }
56
57 #[must_use]
59 pub fn content_type() -> SmartString {
60 CONTENT_TYPE.into()
61 }
62
63 #[must_use]
65 pub fn application_json() -> SmartString {
66 APPLICATION_JSON.into()
67 }
68
69 #[must_use]
71 pub fn application_form() -> SmartString {
72 APPLICATION_FORM.into()
73 }
74}
75
76#[derive(Debug, Clone, Serialize)]
78pub struct BybitWsAuthMessage {
79 pub req_id: Option<SmartString>,
81 pub op: SmartString,
83 pub args: [SmartString; 3],
85}
86
87#[derive(Debug, Clone, Serialize)]
89pub struct BybitWsTradingMessage {
90 #[serde(rename = "reqId")]
92 pub req_id: SmartString,
93 pub header: BybitWsMessageHeader,
95 pub op: SmartString,
97 pub args: Vec<simd_json::OwnedValue>,
99}
100
101#[derive(Debug, Clone, Serialize)]
103pub struct BybitWsMessageHeader {
104 #[serde(rename = "X-BAPI-TIMESTAMP")]
106 pub timestamp: SmartString,
107 #[serde(rename = "X-BAPI-RECV-WINDOW")]
109 pub recv_window: SmartString,
110}
111
112#[derive(Debug, Clone)]
114pub struct BybitAuth {
115 api_key: SmartString,
116 secret_key: SmartString,
117 recv_window: u64,
118}
119
120impl BybitAuth {
121 #[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, }
129 }
130
131 #[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 #[must_use]
147 pub fn get_timestamp() -> u64 {
148 crate::time::get_timestamp_ms()
150 }
151
152 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 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 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 pub fn create_ws_auth_message(
206 &self,
207 req_id: Option<SmartString>,
208 ) -> Result<BybitWsAuthMessage> {
209 let expires = Self::get_timestamp() + 10_000; 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 #[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 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 #[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 #[must_use]
256 pub const fn api_key(&self) -> &SmartString {
257 &self.api_key
258 }
259
260 #[must_use]
262 pub const fn recv_window(&self) -> u64 {
263 self.recv_window
264 }
265
266 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 if let Some(query_start) = path.find('?') {
283 &path[query_start + 1..]
284 } else {
285 ""
286 }
287 } else {
288 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); }
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); }
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()); assert!(!message.args[2].is_empty()); }
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 assert!(auth.is_timestamp_valid(now));
368 assert!(auth.is_timestamp_valid(now + 1000)); assert!(auth.is_timestamp_valid(now - 1000)); assert!(!auth.is_timestamp_valid(now + 10_000)); assert!(!auth.is_timestamp_valid(now - 10_000)); }
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)); assert!(!auth.is_timestamp_valid(now + 6000)); }
385}