use std::collections::HashMap;
use crate::core::{
hmac_sha256, encode_base64, timestamp_millis,
Credentials, ExchangeResult, ExchangeError,
};
#[derive(Clone)]
pub struct BitgetAuth {
api_key: String,
api_secret: String,
passphrase: String,
time_offset_ms: i64,
}
impl BitgetAuth {
pub fn new(credentials: &Credentials) -> ExchangeResult<Self> {
let passphrase = credentials.passphrase.clone()
.ok_or_else(|| ExchangeError::Auth("Bitget requires passphrase".to_string()))?;
Ok(Self {
api_key: credentials.api_key.clone(),
api_secret: credentials.api_secret.clone(),
passphrase,
time_offset_ms: 0,
})
}
pub fn sync_time(&mut self, server_time_ms: i64) {
let local_time = timestamp_millis() as i64;
self.time_offset_ms = server_time_ms - local_time;
}
fn get_timestamp(&self) -> u64 {
let local = timestamp_millis() as i64;
(local + self.time_offset_ms) as u64
}
pub fn sign_request(
&self,
method: &str,
request_path: &str,
query_string: &str,
body: &str,
) -> HashMap<String, String> {
let timestamp = self.get_timestamp();
let timestamp_str = timestamp.to_string();
let sign_string = format!(
"{}{}{}{}{}",
timestamp_str,
method.to_uppercase(),
request_path,
query_string,
body
);
let signature = encode_base64(&hmac_sha256(
self.api_secret.as_bytes(),
sign_string.as_bytes(),
));
let mut headers = HashMap::new();
headers.insert("ACCESS-KEY".to_string(), self.api_key.clone());
headers.insert("ACCESS-SIGN".to_string(), signature);
headers.insert("ACCESS-TIMESTAMP".to_string(), timestamp_str);
headers.insert("ACCESS-PASSPHRASE".to_string(), self.passphrase.clone());
headers.insert("Content-Type".to_string(), "application/json".to_string());
headers
}
pub fn api_key(&self) -> &str {
&self.api_key
}
pub fn api_secret(&self) -> &str {
&self.api_secret
}
pub fn passphrase(&self) -> &str {
&self.passphrase
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sign_request() {
let credentials = Credentials::new("test_key", "test_secret")
.with_passphrase("test_pass");
let auth = BitgetAuth::new(&credentials).unwrap();
let headers = auth.sign_request("GET", "/api/spot/v1/account/assets", "", "");
assert!(headers.contains_key("ACCESS-KEY"));
assert!(headers.contains_key("ACCESS-SIGN"));
assert!(headers.contains_key("ACCESS-TIMESTAMP"));
assert!(headers.contains_key("ACCESS-PASSPHRASE"));
assert_eq!(headers.get("ACCESS-KEY"), Some(&"test_key".to_string()));
assert_eq!(headers.get("ACCESS-PASSPHRASE"), Some(&"test_pass".to_string()));
}
#[test]
fn test_sign_request_with_query() {
let credentials = Credentials::new("test_key", "test_secret")
.with_passphrase("test_pass");
let auth = BitgetAuth::new(&credentials).unwrap();
let headers = auth.sign_request(
"GET",
"/api/spot/v1/market/ticker",
"?symbol=BTCUSDT_SPBL",
""
);
assert!(headers.contains_key("ACCESS-SIGN"));
let headers_no_query = auth.sign_request("GET", "/api/spot/v1/market/ticker", "", "");
assert_ne!(
headers.get("ACCESS-SIGN"),
headers_no_query.get("ACCESS-SIGN")
);
}
#[test]
fn test_sign_request_post() {
let credentials = Credentials::new("test_key", "test_secret")
.with_passphrase("test_pass");
let auth = BitgetAuth::new(&credentials).unwrap();
let body = r#"{"symbol":"BTCUSDT_SPBL","side":"buy"}"#;
let headers = auth.sign_request("POST", "/api/spot/v1/trade/orders", "", body);
assert!(headers.contains_key("ACCESS-SIGN"));
assert_eq!(headers.get("Content-Type"), Some(&"application/json".to_string()));
}
}