use std::collections::HashMap;
use chrono::Utc;
use url::form_urlencoded;
use crate::core::{
hmac_sha256, encode_base64,
Credentials,
};
#[derive(Clone)]
pub struct HtxAuth {
api_key: String,
api_secret: String,
time_offset_ms: i64,
}
impl HtxAuth {
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 = Utc::now().timestamp_millis();
self.time_offset_ms = server_time_ms - local_time;
}
fn get_timestamp(&self) -> String {
let local = Utc::now().timestamp_millis();
let adjusted = local + self.time_offset_ms;
let dt = chrono::DateTime::from_timestamp(adjusted / 1000, ((adjusted % 1000) * 1_000_000) as u32)
.unwrap_or_else(Utc::now);
dt.format("%Y-%m-%dT%H:%M:%S").to_string()
}
pub fn build_signed_query(
&self,
method: &str,
host: &str,
path: &str,
params: &HashMap<String, String>,
) -> String {
let timestamp = self.get_timestamp();
let mut all_params = params.clone();
all_params.insert("AccessKeyId".to_string(), self.api_key.clone());
all_params.insert("SignatureMethod".to_string(), "HmacSHA256".to_string());
all_params.insert("SignatureVersion".to_string(), "2".to_string());
all_params.insert("Timestamp".to_string(), timestamp);
let mut sorted_params: Vec<(&String, &String)> = all_params.iter().collect();
sorted_params.sort_by(|a, b| a.0.cmp(b.0));
let query_string = form_urlencoded::Serializer::new(String::new())
.extend_pairs(sorted_params.iter())
.finish();
let pre_sign = format!(
"{}\n{}\n{}\n{}",
method.to_uppercase(),
host,
path,
query_string
);
let signature_bytes = hmac_sha256(
self.api_secret.as_bytes(),
pre_sign.as_bytes(),
);
let signature_b64 = encode_base64(&signature_bytes);
let signature_encoded = form_urlencoded::byte_serialize(signature_b64.as_bytes())
.collect::<String>();
format!("{}&Signature={}", query_string, signature_encoded)
}
pub fn sign_websocket_auth(&self, host: &str) -> (String, String, String, String, String) {
let timestamp = self.get_timestamp();
let mut params = HashMap::new();
params.insert("AccessKeyId".to_string(), self.api_key.clone());
params.insert("SignatureMethod".to_string(), "HmacSHA256".to_string());
params.insert("SignatureVersion".to_string(), "2.1".to_string());
params.insert("Timestamp".to_string(), timestamp.clone());
let mut sorted_params: Vec<(&String, &String)> = params.iter().collect();
sorted_params.sort_by(|a, b| a.0.cmp(b.0));
let query_string = form_urlencoded::Serializer::new(String::new())
.extend_pairs(sorted_params.iter())
.finish();
let pre_sign = format!(
"GET\n{}\n/ws/v2\n{}",
host,
query_string
);
let signature_bytes = hmac_sha256(
self.api_secret.as_bytes(),
pre_sign.as_bytes(),
);
let signature = encode_base64(&signature_bytes);
(
self.api_key.clone(),
timestamp,
"HmacSHA256".to_string(),
"2.1".to_string(),
signature,
)
}
pub fn api_key(&self) -> &str {
&self.api_key
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_signed_query() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = HtxAuth::new(&credentials);
let mut params = HashMap::new();
params.insert("symbol".to_string(), "btcusdt".to_string());
let query = auth.build_signed_query(
"GET",
"api.huobi.pro",
"/v1/order/openOrders",
¶ms,
);
assert!(query.contains("AccessKeyId=test_key"));
assert!(query.contains("SignatureMethod=HmacSHA256"));
assert!(query.contains("SignatureVersion=2"));
assert!(query.contains("Timestamp="));
assert!(query.contains("symbol=btcusdt"));
assert!(query.contains("Signature="));
let access_pos = query.find("AccessKeyId").unwrap();
let symbol_pos = query.find("symbol").unwrap();
assert!(access_pos < symbol_pos, "Parameters should be sorted in ASCII order");
}
#[test]
fn test_build_signed_query_post() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = HtxAuth::new(&credentials);
let params = HashMap::new();
let query = auth.build_signed_query(
"POST",
"api.huobi.pro",
"/v1/order/orders/place",
¶ms,
);
assert!(query.contains("AccessKeyId=test_key"));
assert!(query.contains("SignatureMethod=HmacSHA256"));
assert!(query.contains("SignatureVersion=2"));
assert!(query.contains("Timestamp="));
assert!(query.contains("Signature="));
}
#[test]
fn test_sign_websocket_auth() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = HtxAuth::new(&credentials);
let (api_key, timestamp, method, version, signature) =
auth.sign_websocket_auth("api.huobi.pro");
assert_eq!(api_key, "test_key");
assert!(!timestamp.is_empty());
assert_eq!(method, "HmacSHA256");
assert_eq!(version, "2.1");
assert!(!signature.is_empty());
assert!(timestamp.contains('T'));
assert_eq!(timestamp.len(), 19); }
#[test]
fn test_timestamp_format() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = HtxAuth::new(&credentials);
let timestamp = auth.get_timestamp();
assert_eq!(timestamp.len(), 19);
assert!(timestamp.contains('T'));
let parts: Vec<&str> = timestamp.split('T').collect();
assert_eq!(parts.len(), 2);
let date_parts: Vec<&str> = parts[0].split('-').collect();
assert_eq!(date_parts.len(), 3);
let time_parts: Vec<&str> = parts[1].split(':').collect();
assert_eq!(time_parts.len(), 3); }
}