1use base64::{Engine as _, engine::general_purpose};
11use hmac::{Hmac, Mac};
12use reqwest::header::{HeaderMap, HeaderValue};
13use sha2::Sha256;
14
15#[derive(Debug, Clone)]
20pub struct BitgetAuth {
21 api_key: String,
23 secret: String,
25 passphrase: String,
27}
28
29impl BitgetAuth {
30 pub fn new(api_key: String, secret: String, passphrase: String) -> Self {
50 Self {
51 api_key,
52 secret,
53 passphrase,
54 }
55 }
56
57 pub fn api_key(&self) -> &str {
59 &self.api_key
60 }
61
62 pub fn passphrase(&self) -> &str {
64 &self.passphrase
65 }
66
67 pub fn build_sign_string(
82 &self,
83 timestamp: &str,
84 method: &str,
85 path: &str,
86 body: &str,
87 ) -> String {
88 format!("{}{}{}{}", timestamp, method.to_uppercase(), path, body)
89 }
90
91 pub fn sign(&self, timestamp: &str, method: &str, path: &str, body: &str) -> String {
119 let sign_string = self.build_sign_string(timestamp, method, path, body);
120 self.hmac_sha256_base64(&sign_string)
121 }
122
123 fn hmac_sha256_base64(&self, message: &str) -> String {
125 type HmacSha256 = Hmac<Sha256>;
126
127 let mut mac = HmacSha256::new_from_slice(self.secret.as_bytes())
129 .expect("HMAC-SHA256 accepts keys of any length; this is an infallible operation");
130 mac.update(message.as_bytes());
131 let result = mac.finalize().into_bytes();
132
133 general_purpose::STANDARD.encode(result)
134 }
135
136 pub fn add_auth_headers(&self, headers: &mut HeaderMap, timestamp: &str, sign: &str) {
173 headers.insert(
174 "ACCESS-KEY",
175 HeaderValue::from_str(&self.api_key).unwrap_or_else(|_| HeaderValue::from_static("")),
176 );
177 headers.insert(
178 "ACCESS-SIGN",
179 HeaderValue::from_str(sign).unwrap_or_else(|_| HeaderValue::from_static("")),
180 );
181 headers.insert(
182 "ACCESS-TIMESTAMP",
183 HeaderValue::from_str(timestamp).unwrap_or_else(|_| HeaderValue::from_static("")),
184 );
185 headers.insert(
186 "ACCESS-PASSPHRASE",
187 HeaderValue::from_str(&self.passphrase)
188 .unwrap_or_else(|_| HeaderValue::from_static("")),
189 );
190 }
191
192 pub fn create_auth_headers(
207 &self,
208 timestamp: &str,
209 method: &str,
210 path: &str,
211 body: &str,
212 ) -> HeaderMap {
213 let sign = self.sign(timestamp, method, path, body);
214 let mut headers = HeaderMap::new();
215 self.add_auth_headers(&mut headers, timestamp, &sign);
216 headers
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_auth_new() {
226 let auth = BitgetAuth::new(
227 "test-api-key".to_string(),
228 "test-secret".to_string(),
229 "test-passphrase".to_string(),
230 );
231
232 assert_eq!(auth.api_key(), "test-api-key");
233 assert_eq!(auth.passphrase(), "test-passphrase");
234 }
235
236 #[test]
237 fn test_build_sign_string() {
238 let auth = BitgetAuth::new(
239 "api-key".to_string(),
240 "secret".to_string(),
241 "passphrase".to_string(),
242 );
243
244 let sign_string =
245 auth.build_sign_string("1234567890", "GET", "/api/v2/spot/account/assets", "");
246 assert_eq!(sign_string, "1234567890GET/api/v2/spot/account/assets");
247
248 let sign_string_post = auth.build_sign_string(
249 "1234567890",
250 "POST",
251 "/api/v2/spot/trade/place-order",
252 r#"{"symbol":"BTCUSDT","side":"buy"}"#,
253 );
254 assert_eq!(
255 sign_string_post,
256 r#"1234567890POST/api/v2/spot/trade/place-order{"symbol":"BTCUSDT","side":"buy"}"#
257 );
258 }
259
260 #[test]
261 fn test_sign_deterministic() {
262 let auth = BitgetAuth::new(
263 "api-key".to_string(),
264 "test-secret-key".to_string(),
265 "passphrase".to_string(),
266 );
267
268 let sig1 = auth.sign("1234567890", "GET", "/api/v2/spot/account/assets", "");
269 let sig2 = auth.sign("1234567890", "GET", "/api/v2/spot/account/assets", "");
270
271 assert_eq!(sig1, sig2);
272 assert!(!sig1.is_empty());
273 }
274
275 #[test]
276 fn test_sign_different_inputs() {
277 let auth = BitgetAuth::new(
278 "api-key".to_string(),
279 "test-secret-key".to_string(),
280 "passphrase".to_string(),
281 );
282
283 let sig1 = auth.sign("1234567890", "GET", "/api/v2/spot/account/assets", "");
284 let sig2 = auth.sign("1234567891", "GET", "/api/v2/spot/account/assets", "");
285 let sig3 = auth.sign("1234567890", "POST", "/api/v2/spot/account/assets", "");
286
287 assert_ne!(sig1, sig2);
288 assert_ne!(sig1, sig3);
289 }
290
291 #[test]
292 fn test_add_auth_headers() {
293 let auth = BitgetAuth::new(
294 "test-api-key".to_string(),
295 "test-secret".to_string(),
296 "test-passphrase".to_string(),
297 );
298
299 let mut headers = HeaderMap::new();
300 let timestamp = "1234567890";
301 let signature = auth.sign(timestamp, "GET", "/api/v2/spot/account/assets", "");
302 auth.add_auth_headers(&mut headers, timestamp, &signature);
303
304 assert_eq!(headers.get("ACCESS-KEY").unwrap(), "test-api-key");
305 assert_eq!(headers.get("ACCESS-SIGN").unwrap(), &signature);
306 assert_eq!(headers.get("ACCESS-TIMESTAMP").unwrap(), "1234567890");
307 assert_eq!(headers.get("ACCESS-PASSPHRASE").unwrap(), "test-passphrase");
308 }
309
310 #[test]
311 fn test_create_auth_headers() {
312 let auth = BitgetAuth::new(
313 "test-api-key".to_string(),
314 "test-secret".to_string(),
315 "test-passphrase".to_string(),
316 );
317
318 let headers =
319 auth.create_auth_headers("1234567890", "GET", "/api/v2/spot/account/assets", "");
320
321 assert!(headers.contains_key("ACCESS-KEY"));
322 assert!(headers.contains_key("ACCESS-SIGN"));
323 assert!(headers.contains_key("ACCESS-TIMESTAMP"));
324 assert!(headers.contains_key("ACCESS-PASSPHRASE"));
325 }
326
327 #[test]
328 fn test_method_case_insensitive() {
329 let auth = BitgetAuth::new(
330 "api-key".to_string(),
331 "test-secret".to_string(),
332 "passphrase".to_string(),
333 );
334
335 let sign_string_lower =
337 auth.build_sign_string("1234567890", "get", "/api/v2/spot/account/assets", "");
338 let sign_string_upper =
339 auth.build_sign_string("1234567890", "GET", "/api/v2/spot/account/assets", "");
340
341 assert_eq!(sign_string_lower, sign_string_upper);
342 }
343
344 #[test]
345 fn test_signature_is_base64() {
346 let auth = BitgetAuth::new(
347 "api-key".to_string(),
348 "test-secret".to_string(),
349 "passphrase".to_string(),
350 );
351
352 let signature = auth.sign("1234567890", "GET", "/api/v2/spot/account/assets", "");
353
354 assert!(
356 signature
357 .chars()
358 .all(|c| c.is_alphanumeric() || c == '+' || c == '/' || c == '=')
359 );
360
361 let decoded = general_purpose::STANDARD.decode(&signature);
363 assert!(decoded.is_ok());
364
365 assert_eq!(decoded.unwrap().len(), 32);
367 }
368
369 #[test]
376 fn test_signature_with_known_inputs() {
377 let auth = BitgetAuth::new(
378 "bg_api_key".to_string(),
379 "bitget-secret-key".to_string(),
380 "my-passphrase".to_string(),
381 );
382
383 let timestamp = "1609459200000"; let method = "GET";
386 let path = "/api/v2/spot/account/assets";
387 let body = "";
388
389 let signature = auth.sign(timestamp, method, path, body);
391
392 let expected_sign_string = "1609459200000GET/api/v2/spot/account/assets";
394 assert_eq!(
395 auth.build_sign_string(timestamp, method, path, body),
396 expected_sign_string
397 );
398
399 assert!(!signature.is_empty());
401 let decoded = general_purpose::STANDARD.decode(&signature);
402 assert!(decoded.is_ok());
403 assert_eq!(decoded.unwrap().len(), 32);
404
405 let signature2 = auth.sign(timestamp, method, path, body);
407 assert_eq!(signature, signature2);
408 }
409
410 #[test]
412 fn test_signature_with_post_body() {
413 let auth = BitgetAuth::new(
414 "bg_api_key".to_string(),
415 "bitget-secret-key".to_string(),
416 "my-passphrase".to_string(),
417 );
418
419 let timestamp = "1609459200000";
420 let method = "POST";
421 let path = "/api/v2/spot/trade/place-order";
422 let body = r#"{"symbol":"BTCUSDT","side":"buy","orderType":"limit","price":"50000","size":"0.001"}"#;
423
424 let signature = auth.sign(timestamp, method, path, body);
425
426 let expected_sign_string = format!("{}{}{}{}", timestamp, method, path, body);
428 assert_eq!(
429 auth.build_sign_string(timestamp, method, path, body),
430 expected_sign_string
431 );
432
433 assert!(!signature.is_empty());
435 let decoded = general_purpose::STANDARD.decode(&signature);
436 assert!(decoded.is_ok());
437 assert_eq!(decoded.unwrap().len(), 32);
438 }
439
440 #[test]
442 fn test_header_values_correctness() {
443 let api_key = "my-api-key-12345";
444 let secret = "my-secret-key-67890";
445 let passphrase = "my-passphrase-abc";
446
447 let auth = BitgetAuth::new(
448 api_key.to_string(),
449 secret.to_string(),
450 passphrase.to_string(),
451 );
452
453 let timestamp = "1609459200000";
454 let method = "GET";
455 let path = "/api/v2/spot/account/assets";
456 let body = "";
457
458 let headers = auth.create_auth_headers(timestamp, method, path, body);
459
460 assert_eq!(
462 headers.get("ACCESS-KEY").unwrap().to_str().unwrap(),
463 api_key
464 );
465
466 assert_eq!(
468 headers.get("ACCESS-TIMESTAMP").unwrap().to_str().unwrap(),
469 timestamp
470 );
471
472 assert_eq!(
474 headers.get("ACCESS-PASSPHRASE").unwrap().to_str().unwrap(),
475 passphrase
476 );
477
478 let sign_header = headers.get("ACCESS-SIGN").unwrap().to_str().unwrap();
480 assert!(!sign_header.is_empty());
481 let decoded = general_purpose::STANDARD.decode(sign_header);
482 assert!(decoded.is_ok());
483 }
484
485 #[test]
487 fn test_signature_with_query_params() {
488 let auth = BitgetAuth::new(
489 "api-key".to_string(),
490 "secret-key".to_string(),
491 "passphrase".to_string(),
492 );
493
494 let timestamp = "1609459200000";
495 let method = "GET";
496 let path = "/api/v2/spot/market/tickers?symbol=BTCUSDT";
497 let body = "";
498
499 let signature = auth.sign(timestamp, method, path, body);
500
501 let sign_string = auth.build_sign_string(timestamp, method, path, body);
503 assert!(sign_string.contains("?symbol=BTCUSDT"));
504
505 assert!(!signature.is_empty());
507 let decoded = general_purpose::STANDARD.decode(&signature);
508 assert!(decoded.is_ok());
509 }
510}