1use ccxt_core::credentials::SecretString;
11use hmac::{Hmac, Mac};
12use reqwest::header::{HeaderMap, HeaderValue};
13use sha2::Sha256;
14
15#[derive(Debug, Clone)]
22pub struct BybitAuth {
23 api_key: SecretString,
25 secret: SecretString,
27}
28
29impl BybitAuth {
30 pub fn new(api_key: String, secret: String) -> Self {
52 Self {
53 api_key: SecretString::new(api_key),
54 secret: SecretString::new(secret),
55 }
56 }
57
58 pub fn api_key(&self) -> &str {
60 self.api_key.expose_secret()
61 }
62
63 pub fn build_sign_string(&self, timestamp: &str, recv_window: u64, params: &str) -> String {
77 format!(
78 "{}{}{}{}",
79 timestamp,
80 self.api_key.expose_secret(),
81 recv_window,
82 params
83 )
84 }
85
86 pub fn sign(&self, timestamp: &str, recv_window: u64, params: &str) -> String {
112 let sign_string = self.build_sign_string(timestamp, recv_window, params);
113 self.hmac_sha256_hex(&sign_string)
114 }
115
116 fn hmac_sha256_hex(&self, message: &str) -> String {
118 type HmacSha256 = Hmac<Sha256>;
119
120 let mut mac = HmacSha256::new_from_slice(self.secret.expose_secret_bytes())
122 .expect("HMAC-SHA256 accepts keys of any length; this is an infallible operation");
123 mac.update(message.as_bytes());
124 let result = mac.finalize().into_bytes();
125
126 hex::encode(result)
128 }
129
130 pub fn add_auth_headers(
168 &self,
169 headers: &mut HeaderMap,
170 timestamp: &str,
171 sign: &str,
172 recv_window: u64,
173 ) {
174 headers.insert(
175 "X-BAPI-API-KEY",
176 HeaderValue::from_str(self.api_key.expose_secret())
177 .unwrap_or_else(|_| HeaderValue::from_static("")),
178 );
179 headers.insert(
180 "X-BAPI-SIGN",
181 HeaderValue::from_str(sign).unwrap_or_else(|_| HeaderValue::from_static("")),
182 );
183 headers.insert(
184 "X-BAPI-TIMESTAMP",
185 HeaderValue::from_str(timestamp).unwrap_or_else(|_| HeaderValue::from_static("")),
186 );
187 headers.insert(
188 "X-BAPI-RECV-WINDOW",
189 HeaderValue::from_str(&recv_window.to_string())
190 .unwrap_or_else(|_| HeaderValue::from_static("")),
191 );
192 }
193
194 pub fn create_auth_headers(
208 &self,
209 timestamp: &str,
210 recv_window: u64,
211 params: &str,
212 ) -> HeaderMap {
213 let sign = self.sign(timestamp, recv_window, params);
214 let mut headers = HeaderMap::new();
215 self.add_auth_headers(&mut headers, timestamp, &sign, recv_window);
216 headers
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_auth_new() {
226 let auth = BybitAuth::new("test-api-key".to_string(), "test-secret".to_string());
227
228 assert_eq!(auth.api_key(), "test-api-key");
229 }
230
231 #[test]
232 fn test_build_sign_string() {
233 let auth = BybitAuth::new("api-key".to_string(), "secret".to_string());
234
235 let sign_string = auth.build_sign_string("1234567890000", 5000, "symbol=BTCUSDT");
236 assert_eq!(sign_string, "1234567890000api-key5000symbol=BTCUSDT");
237
238 let sign_string_empty = auth.build_sign_string("1234567890000", 5000, "");
239 assert_eq!(sign_string_empty, "1234567890000api-key5000");
240 }
241
242 #[test]
243 fn test_sign_deterministic() {
244 let auth = BybitAuth::new("api-key".to_string(), "test-secret-key".to_string());
245
246 let sig1 = auth.sign("1234567890000", 5000, "symbol=BTCUSDT");
247 let sig2 = auth.sign("1234567890000", 5000, "symbol=BTCUSDT");
248
249 assert_eq!(sig1, sig2);
250 assert!(!sig1.is_empty());
251 }
252
253 #[test]
254 fn test_sign_different_inputs() {
255 let auth = BybitAuth::new("api-key".to_string(), "test-secret-key".to_string());
256
257 let sig1 = auth.sign("1234567890000", 5000, "symbol=BTCUSDT");
258 let sig2 = auth.sign("1234567890001", 5000, "symbol=BTCUSDT");
259 let sig3 = auth.sign("1234567890000", 10000, "symbol=BTCUSDT");
260 let sig4 = auth.sign("1234567890000", 5000, "symbol=ETHUSDT");
261
262 assert_ne!(sig1, sig2);
263 assert_ne!(sig1, sig3);
264 assert_ne!(sig1, sig4);
265 }
266
267 #[test]
268 fn test_add_auth_headers() {
269 let auth = BybitAuth::new("test-api-key".to_string(), "test-secret".to_string());
270
271 let mut headers = HeaderMap::new();
272 let timestamp = "1234567890000";
273 let recv_window = 5000u64;
274 let signature = auth.sign(timestamp, recv_window, "symbol=BTCUSDT");
275 auth.add_auth_headers(&mut headers, timestamp, &signature, recv_window);
276
277 assert_eq!(headers.get("X-BAPI-API-KEY").unwrap(), "test-api-key");
278 assert_eq!(headers.get("X-BAPI-SIGN").unwrap(), &signature);
279 assert_eq!(headers.get("X-BAPI-TIMESTAMP").unwrap(), "1234567890000");
280 assert_eq!(headers.get("X-BAPI-RECV-WINDOW").unwrap(), "5000");
281 }
282
283 #[test]
284 fn test_create_auth_headers() {
285 let auth = BybitAuth::new("test-api-key".to_string(), "test-secret".to_string());
286
287 let headers = auth.create_auth_headers("1234567890000", 5000, "symbol=BTCUSDT");
288
289 assert!(headers.contains_key("X-BAPI-API-KEY"));
290 assert!(headers.contains_key("X-BAPI-SIGN"));
291 assert!(headers.contains_key("X-BAPI-TIMESTAMP"));
292 assert!(headers.contains_key("X-BAPI-RECV-WINDOW"));
293 }
294
295 #[test]
296 fn test_signature_is_hex() {
297 let auth = BybitAuth::new("api-key".to_string(), "test-secret".to_string());
298
299 let signature = auth.sign("1234567890000", 5000, "symbol=BTCUSDT");
300
301 assert!(signature.chars().all(|c| c.is_ascii_hexdigit()));
303
304 assert_eq!(signature.len(), 64);
306
307 let decoded = hex::decode(&signature);
309 assert!(decoded.is_ok());
310 assert_eq!(decoded.unwrap().len(), 32);
311 }
312
313 #[test]
316 fn test_signature_with_known_inputs() {
317 let auth = BybitAuth::new("bybit_api_key".to_string(), "bybit-secret-key".to_string());
318
319 let timestamp = "1609459200000"; let recv_window = 5000u64;
322 let params = "category=spot&symbol=BTCUSDT";
323
324 let signature = auth.sign(timestamp, recv_window, params);
326
327 let expected_sign_string = "1609459200000bybit_api_key5000category=spot&symbol=BTCUSDT";
329 assert_eq!(
330 auth.build_sign_string(timestamp, recv_window, params),
331 expected_sign_string
332 );
333
334 assert!(!signature.is_empty());
336 assert_eq!(signature.len(), 64);
337 let decoded = hex::decode(&signature);
338 assert!(decoded.is_ok());
339 assert_eq!(decoded.unwrap().len(), 32);
340
341 let signature2 = auth.sign(timestamp, recv_window, params);
343 assert_eq!(signature, signature2);
344 }
345
346 #[test]
348 fn test_signature_with_post_body() {
349 let auth = BybitAuth::new("bybit_api_key".to_string(), "bybit-secret-key".to_string());
350
351 let timestamp = "1609459200000";
352 let recv_window = 5000u64;
353 let body = r#"{"category":"spot","symbol":"BTCUSDT","side":"Buy","orderType":"Limit","qty":"0.001","price":"50000"}"#;
354
355 let signature = auth.sign(timestamp, recv_window, body);
356
357 let expected_sign_string =
359 format!("{}{}{}{}", timestamp, auth.api_key(), recv_window, body);
360 assert_eq!(
361 auth.build_sign_string(timestamp, recv_window, body),
362 expected_sign_string
363 );
364
365 assert!(!signature.is_empty());
367 assert_eq!(signature.len(), 64);
368 let decoded = hex::decode(&signature);
369 assert!(decoded.is_ok());
370 assert_eq!(decoded.unwrap().len(), 32);
371 }
372
373 #[test]
375 fn test_header_values_correctness() {
376 let api_key = "my-api-key-12345";
377 let secret = "my-secret-key-67890";
378
379 let auth = BybitAuth::new(api_key.to_string(), secret.to_string());
380
381 let timestamp = "1609459200000";
382 let recv_window = 5000u64;
383 let params = "category=spot&symbol=BTCUSDT";
384
385 let headers = auth.create_auth_headers(timestamp, recv_window, params);
386
387 assert_eq!(
389 headers.get("X-BAPI-API-KEY").unwrap().to_str().unwrap(),
390 api_key
391 );
392
393 assert_eq!(
395 headers.get("X-BAPI-TIMESTAMP").unwrap().to_str().unwrap(),
396 timestamp
397 );
398
399 assert_eq!(
401 headers.get("X-BAPI-RECV-WINDOW").unwrap().to_str().unwrap(),
402 "5000"
403 );
404
405 let sign_header = headers.get("X-BAPI-SIGN").unwrap().to_str().unwrap();
407 assert!(!sign_header.is_empty());
408 assert_eq!(sign_header.len(), 64);
409 let decoded = hex::decode(sign_header);
410 assert!(decoded.is_ok());
411 }
412
413 #[test]
415 fn test_signature_with_empty_params() {
416 let auth = BybitAuth::new("api-key".to_string(), "secret-key".to_string());
417
418 let timestamp = "1609459200000";
419 let recv_window = 5000u64;
420 let params = "";
421
422 let signature = auth.sign(timestamp, recv_window, params);
423
424 let sign_string = auth.build_sign_string(timestamp, recv_window, params);
426 assert_eq!(sign_string, "1609459200000api-key5000");
427
428 assert!(!signature.is_empty());
430 assert_eq!(signature.len(), 64);
431 let decoded = hex::decode(&signature);
432 assert!(decoded.is_ok());
433 }
434
435 #[test]
437 fn test_different_recv_window() {
438 let auth = BybitAuth::new("api-key".to_string(), "secret-key".to_string());
439
440 let timestamp = "1609459200000";
441 let params = "symbol=BTCUSDT";
442
443 let sig_5000 = auth.sign(timestamp, 5000, params);
444 let sig_10000 = auth.sign(timestamp, 10000, params);
445 let sig_20000 = auth.sign(timestamp, 20000, params);
446
447 assert_ne!(sig_5000, sig_10000);
449 assert_ne!(sig_5000, sig_20000);
450 assert_ne!(sig_10000, sig_20000);
451 }
452}