use std::collections::HashMap;
use crate::core::{
hmac_sha256_hex, timestamp_millis,
Credentials,
};
#[derive(Clone)]
pub struct MexcAuth {
api_key: String,
api_secret: String,
time_offset_ms: i64,
}
impl MexcAuth {
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,
mut params: HashMap<String, String>,
) -> (HashMap<String, String>, HashMap<String, String>) {
let timestamp = self.get_timestamp();
params.insert("timestamp".to_string(), timestamp.to_string());
params.insert("recvWindow".to_string(), "5000".to_string());
let mut sorted_keys: Vec<&String> = params.keys().collect();
sorted_keys.sort();
let query_string: Vec<String> = sorted_keys
.iter()
.map(|k| format!("{}={}", k, params.get(*k).expect("Key exists in params HashMap")))
.collect();
let query_string = query_string.join("&");
let signature = hmac_sha256_hex(
self.api_secret.as_bytes(),
query_string.as_bytes(),
);
params.insert("signature".to_string(), signature);
let mut headers = HashMap::new();
headers.insert("X-MEXC-APIKEY".to_string(), self.api_key.clone());
(headers, params)
}
pub fn api_key(&self) -> &str {
&self.api_key
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sign_request() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = MexcAuth::new(&credentials);
let mut params = HashMap::new();
params.insert("symbol".to_string(), "BTCUSDT".to_string());
let (headers, signed_params) = auth.sign_request(params);
assert_eq!(headers.get("X-MEXC-APIKEY"), Some(&"test_key".to_string()));
assert!(signed_params.contains_key("timestamp"));
assert!(signed_params.contains_key("recvWindow"));
assert!(signed_params.contains_key("signature"));
assert_eq!(signed_params.get("symbol"), Some(&"BTCUSDT".to_string()));
assert_eq!(signed_params.get("recvWindow"), Some(&"5000".to_string()));
let signature = signed_params.get("signature").unwrap();
assert_eq!(signature.len(), 64);
assert!(signature.chars().all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()));
}
#[test]
fn test_sign_request_multiple_params() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = MexcAuth::new(&credentials);
let mut params = HashMap::new();
params.insert("symbol".to_string(), "BTCUSDT".to_string());
params.insert("side".to_string(), "BUY".to_string());
params.insert("type".to_string(), "LIMIT".to_string());
params.insert("quantity".to_string(), "0.1".to_string());
params.insert("price".to_string(), "90000".to_string());
let (headers, signed_params) = auth.sign_request(params);
assert!(signed_params.contains_key("symbol"));
assert!(signed_params.contains_key("side"));
assert!(signed_params.contains_key("type"));
assert!(signed_params.contains_key("quantity"));
assert!(signed_params.contains_key("price"));
assert!(signed_params.contains_key("timestamp"));
assert!(signed_params.contains_key("recvWindow"));
assert!(signed_params.contains_key("signature"));
assert_eq!(headers.get("X-MEXC-APIKEY"), Some(&"test_key".to_string()));
}
}