1use hmac::{Hmac, Mac};
11use reqwest::header::{HeaderMap, HeaderValue};
12use sha2::Sha256;
13
14#[derive(Debug, Clone)]
19pub struct BybitAuth {
20 api_key: String,
22 secret: String,
24}
25
26impl BybitAuth {
27 pub fn new(api_key: String, secret: String) -> Self {
45 Self { api_key, secret }
46 }
47
48 pub fn api_key(&self) -> &str {
50 &self.api_key
51 }
52
53 pub fn build_sign_string(&self, timestamp: &str, recv_window: u64, params: &str) -> String {
67 format!("{}{}{}{}", timestamp, self.api_key, recv_window, params)
68 }
69
70 pub fn sign(&self, timestamp: &str, recv_window: u64, params: &str) -> String {
96 let sign_string = self.build_sign_string(timestamp, recv_window, params);
97 self.hmac_sha256_hex(&sign_string)
98 }
99
100 fn hmac_sha256_hex(&self, message: &str) -> String {
102 type HmacSha256 = Hmac<Sha256>;
103
104 let mut mac = HmacSha256::new_from_slice(self.secret.as_bytes())
106 .expect("HMAC-SHA256 accepts keys of any length; this is an infallible operation");
107 mac.update(message.as_bytes());
108 let result = mac.finalize().into_bytes();
109
110 hex::encode(result)
112 }
113
114 pub fn add_auth_headers(
152 &self,
153 headers: &mut HeaderMap,
154 timestamp: &str,
155 sign: &str,
156 recv_window: u64,
157 ) {
158 headers.insert(
159 "X-BAPI-API-KEY",
160 HeaderValue::from_str(&self.api_key).unwrap_or_else(|_| HeaderValue::from_static("")),
161 );
162 headers.insert(
163 "X-BAPI-SIGN",
164 HeaderValue::from_str(sign).unwrap_or_else(|_| HeaderValue::from_static("")),
165 );
166 headers.insert(
167 "X-BAPI-TIMESTAMP",
168 HeaderValue::from_str(timestamp).unwrap_or_else(|_| HeaderValue::from_static("")),
169 );
170 headers.insert(
171 "X-BAPI-RECV-WINDOW",
172 HeaderValue::from_str(&recv_window.to_string())
173 .unwrap_or_else(|_| HeaderValue::from_static("")),
174 );
175 }
176
177 pub fn create_auth_headers(
191 &self,
192 timestamp: &str,
193 recv_window: u64,
194 params: &str,
195 ) -> HeaderMap {
196 let sign = self.sign(timestamp, recv_window, params);
197 let mut headers = HeaderMap::new();
198 self.add_auth_headers(&mut headers, timestamp, &sign, recv_window);
199 headers
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn test_auth_new() {
209 let auth = BybitAuth::new("test-api-key".to_string(), "test-secret".to_string());
210
211 assert_eq!(auth.api_key(), "test-api-key");
212 }
213
214 #[test]
215 fn test_build_sign_string() {
216 let auth = BybitAuth::new("api-key".to_string(), "secret".to_string());
217
218 let sign_string = auth.build_sign_string("1234567890000", 5000, "symbol=BTCUSDT");
219 assert_eq!(sign_string, "1234567890000api-key5000symbol=BTCUSDT");
220
221 let sign_string_empty = auth.build_sign_string("1234567890000", 5000, "");
222 assert_eq!(sign_string_empty, "1234567890000api-key5000");
223 }
224
225 #[test]
226 fn test_sign_deterministic() {
227 let auth = BybitAuth::new("api-key".to_string(), "test-secret-key".to_string());
228
229 let sig1 = auth.sign("1234567890000", 5000, "symbol=BTCUSDT");
230 let sig2 = auth.sign("1234567890000", 5000, "symbol=BTCUSDT");
231
232 assert_eq!(sig1, sig2);
233 assert!(!sig1.is_empty());
234 }
235
236 #[test]
237 fn test_sign_different_inputs() {
238 let auth = BybitAuth::new("api-key".to_string(), "test-secret-key".to_string());
239
240 let sig1 = auth.sign("1234567890000", 5000, "symbol=BTCUSDT");
241 let sig2 = auth.sign("1234567890001", 5000, "symbol=BTCUSDT");
242 let sig3 = auth.sign("1234567890000", 10000, "symbol=BTCUSDT");
243 let sig4 = auth.sign("1234567890000", 5000, "symbol=ETHUSDT");
244
245 assert_ne!(sig1, sig2);
246 assert_ne!(sig1, sig3);
247 assert_ne!(sig1, sig4);
248 }
249
250 #[test]
251 fn test_add_auth_headers() {
252 let auth = BybitAuth::new("test-api-key".to_string(), "test-secret".to_string());
253
254 let mut headers = HeaderMap::new();
255 let timestamp = "1234567890000";
256 let recv_window = 5000u64;
257 let signature = auth.sign(timestamp, recv_window, "symbol=BTCUSDT");
258 auth.add_auth_headers(&mut headers, timestamp, &signature, recv_window);
259
260 assert_eq!(headers.get("X-BAPI-API-KEY").unwrap(), "test-api-key");
261 assert_eq!(headers.get("X-BAPI-SIGN").unwrap(), &signature);
262 assert_eq!(headers.get("X-BAPI-TIMESTAMP").unwrap(), "1234567890000");
263 assert_eq!(headers.get("X-BAPI-RECV-WINDOW").unwrap(), "5000");
264 }
265
266 #[test]
267 fn test_create_auth_headers() {
268 let auth = BybitAuth::new("test-api-key".to_string(), "test-secret".to_string());
269
270 let headers = auth.create_auth_headers("1234567890000", 5000, "symbol=BTCUSDT");
271
272 assert!(headers.contains_key("X-BAPI-API-KEY"));
273 assert!(headers.contains_key("X-BAPI-SIGN"));
274 assert!(headers.contains_key("X-BAPI-TIMESTAMP"));
275 assert!(headers.contains_key("X-BAPI-RECV-WINDOW"));
276 }
277
278 #[test]
279 fn test_signature_is_hex() {
280 let auth = BybitAuth::new("api-key".to_string(), "test-secret".to_string());
281
282 let signature = auth.sign("1234567890000", 5000, "symbol=BTCUSDT");
283
284 assert!(signature.chars().all(|c| c.is_ascii_hexdigit()));
286
287 assert_eq!(signature.len(), 64);
289
290 let decoded = hex::decode(&signature);
292 assert!(decoded.is_ok());
293 assert_eq!(decoded.unwrap().len(), 32);
294 }
295
296 #[test]
299 fn test_signature_with_known_inputs() {
300 let auth = BybitAuth::new("bybit_api_key".to_string(), "bybit-secret-key".to_string());
301
302 let timestamp = "1609459200000"; let recv_window = 5000u64;
305 let params = "category=spot&symbol=BTCUSDT";
306
307 let signature = auth.sign(timestamp, recv_window, params);
309
310 let expected_sign_string = "1609459200000bybit_api_key5000category=spot&symbol=BTCUSDT";
312 assert_eq!(
313 auth.build_sign_string(timestamp, recv_window, params),
314 expected_sign_string
315 );
316
317 assert!(!signature.is_empty());
319 assert_eq!(signature.len(), 64);
320 let decoded = hex::decode(&signature);
321 assert!(decoded.is_ok());
322 assert_eq!(decoded.unwrap().len(), 32);
323
324 let signature2 = auth.sign(timestamp, recv_window, params);
326 assert_eq!(signature, signature2);
327 }
328
329 #[test]
331 fn test_signature_with_post_body() {
332 let auth = BybitAuth::new("bybit_api_key".to_string(), "bybit-secret-key".to_string());
333
334 let timestamp = "1609459200000";
335 let recv_window = 5000u64;
336 let body = r#"{"category":"spot","symbol":"BTCUSDT","side":"Buy","orderType":"Limit","qty":"0.001","price":"50000"}"#;
337
338 let signature = auth.sign(timestamp, recv_window, body);
339
340 let expected_sign_string =
342 format!("{}{}{}{}", timestamp, auth.api_key(), recv_window, body);
343 assert_eq!(
344 auth.build_sign_string(timestamp, recv_window, body),
345 expected_sign_string
346 );
347
348 assert!(!signature.is_empty());
350 assert_eq!(signature.len(), 64);
351 let decoded = hex::decode(&signature);
352 assert!(decoded.is_ok());
353 assert_eq!(decoded.unwrap().len(), 32);
354 }
355
356 #[test]
358 fn test_header_values_correctness() {
359 let api_key = "my-api-key-12345";
360 let secret = "my-secret-key-67890";
361
362 let auth = BybitAuth::new(api_key.to_string(), secret.to_string());
363
364 let timestamp = "1609459200000";
365 let recv_window = 5000u64;
366 let params = "category=spot&symbol=BTCUSDT";
367
368 let headers = auth.create_auth_headers(timestamp, recv_window, params);
369
370 assert_eq!(
372 headers.get("X-BAPI-API-KEY").unwrap().to_str().unwrap(),
373 api_key
374 );
375
376 assert_eq!(
378 headers.get("X-BAPI-TIMESTAMP").unwrap().to_str().unwrap(),
379 timestamp
380 );
381
382 assert_eq!(
384 headers.get("X-BAPI-RECV-WINDOW").unwrap().to_str().unwrap(),
385 "5000"
386 );
387
388 let sign_header = headers.get("X-BAPI-SIGN").unwrap().to_str().unwrap();
390 assert!(!sign_header.is_empty());
391 assert_eq!(sign_header.len(), 64);
392 let decoded = hex::decode(sign_header);
393 assert!(decoded.is_ok());
394 }
395
396 #[test]
398 fn test_signature_with_empty_params() {
399 let auth = BybitAuth::new("api-key".to_string(), "secret-key".to_string());
400
401 let timestamp = "1609459200000";
402 let recv_window = 5000u64;
403 let params = "";
404
405 let signature = auth.sign(timestamp, recv_window, params);
406
407 let sign_string = auth.build_sign_string(timestamp, recv_window, params);
409 assert_eq!(sign_string, "1609459200000api-key5000");
410
411 assert!(!signature.is_empty());
413 assert_eq!(signature.len(), 64);
414 let decoded = hex::decode(&signature);
415 assert!(decoded.is_ok());
416 }
417
418 #[test]
420 fn test_different_recv_window() {
421 let auth = BybitAuth::new("api-key".to_string(), "secret-key".to_string());
422
423 let timestamp = "1609459200000";
424 let params = "symbol=BTCUSDT";
425
426 let sig_5000 = auth.sign(timestamp, 5000, params);
427 let sig_10000 = auth.sign(timestamp, 10000, params);
428 let sig_20000 = auth.sign(timestamp, 20000, params);
429
430 assert_ne!(sig_5000, sig_10000);
432 assert_ne!(sig_5000, sig_20000);
433 assert_ne!(sig_10000, sig_20000);
434 }
435}