use std::collections::HashMap;
use crate::core::{
hmac_sha256_hex, timestamp_millis,
Credentials,
};
#[derive(Clone)]
pub struct BybitAuth {
api_key: String,
api_secret: String,
time_offset_ms: i64,
}
impl BybitAuth {
pub fn new(credentials: &Credentials) -> Self {
Self {
api_key: credentials.api_key.clone(),
api_secret: credentials.api_secret.clone(),
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,
param_str: &str,
) -> HashMap<String, String> {
let timestamp = self.get_timestamp();
let timestamp_str = timestamp.to_string();
let recv_window = "5000";
let str_to_sign = format!(
"{}{}{}{}",
timestamp_str,
self.api_key,
recv_window,
param_str
);
let signature = hmac_sha256_hex(
self.api_secret.as_bytes(),
str_to_sign.as_bytes(),
);
let mut headers = HashMap::new();
headers.insert("X-BAPI-API-KEY".to_string(), self.api_key.clone());
headers.insert("X-BAPI-SIGN".to_string(), signature);
headers.insert("X-BAPI-TIMESTAMP".to_string(), timestamp_str);
headers.insert("X-BAPI-RECV-WINDOW".to_string(), recv_window.to_string());
if method.to_uppercase() == "POST" {
headers.insert("Content-Type".to_string(), "application/json".to_string());
}
headers
}
pub fn sign_websocket_auth(&self) -> (String, String, String) {
let timestamp = self.get_timestamp();
let expires = timestamp + 10_000; let expires_str = expires.to_string();
let str_to_sign = format!("GET/realtime{}", expires_str);
let signature = hmac_sha256_hex(
self.api_secret.as_bytes(),
str_to_sign.as_bytes(),
);
(self.api_key.clone(), expires_str, signature)
}
pub fn api_key(&self) -> &str {
&self.api_key
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sign_request_get() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = BybitAuth::new(&credentials);
let headers = auth.sign_request("GET", "category=spot&symbol=BTCUSDT");
assert!(headers.contains_key("X-BAPI-API-KEY"));
assert!(headers.contains_key("X-BAPI-SIGN"));
assert!(headers.contains_key("X-BAPI-TIMESTAMP"));
assert!(headers.contains_key("X-BAPI-RECV-WINDOW"));
assert_eq!(headers.get("X-BAPI-API-KEY"), Some(&"test_key".to_string()));
assert_eq!(headers.get("X-BAPI-RECV-WINDOW"), Some(&"5000".to_string()));
assert!(!headers.contains_key("Content-Type"));
}
#[test]
fn test_sign_request_post() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = BybitAuth::new(&credentials);
let body = r#"{"category":"spot","symbol":"BTCUSDT"}"#;
let headers = auth.sign_request("POST", body);
assert!(headers.contains_key("X-BAPI-API-KEY"));
assert!(headers.contains_key("X-BAPI-SIGN"));
assert!(headers.contains_key("X-BAPI-TIMESTAMP"));
assert!(headers.contains_key("X-BAPI-RECV-WINDOW"));
assert_eq!(headers.get("Content-Type"), Some(&"application/json".to_string()));
}
#[test]
fn test_sign_websocket_auth() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = BybitAuth::new(&credentials);
let (api_key, expires, signature) = auth.sign_websocket_auth();
assert_eq!(api_key, "test_key");
assert!(!expires.is_empty());
assert!(!signature.is_empty());
assert_eq!(signature.len(), 64);
assert!(signature.chars().all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()));
}
}