use std::collections::BTreeMap;
use serde_json::Value;
use crate::core::{
hmac_sha256_hex, timestamp_millis,
Credentials, ExchangeResult,
};
#[derive(Clone)]
pub struct CryptoComAuth {
api_key: String,
api_secret: String,
}
impl CryptoComAuth {
pub fn new(credentials: &Credentials) -> ExchangeResult<Self> {
Ok(Self {
api_key: credentials.api_key.clone(),
api_secret: credentials.api_secret.clone(),
})
}
pub fn api_key(&self) -> &str {
&self.api_key
}
pub fn sign_request(
&self,
method: &str,
id: i64,
params: &Value,
nonce: i64,
) -> String {
let params_string = self.build_params_string(params);
let payload = format!(
"{}{}{}{}{}",
method,
id,
&self.api_key,
params_string,
nonce
);
hmac_sha256_hex(self.api_secret.as_bytes(), payload.as_bytes())
}
pub fn sign_ws_auth(&self, id: i64, nonce: i64) -> String {
let payload = format!("public/auth{}{}{}", id, &self.api_key, nonce);
hmac_sha256_hex(self.api_secret.as_bytes(), payload.as_bytes())
}
fn build_params_string(&self, params: &Value) -> String {
let obj = match params.as_object() {
Some(o) => o,
None => return String::new(), };
let sorted: BTreeMap<&String, &Value> = obj.iter().collect();
let mut result = String::new();
for (key, value) in sorted {
result.push_str(key);
result.push_str(&self.value_to_string(value));
}
result
}
fn value_to_string(&self, value: &Value) -> String {
match value {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
Value::Null => "null".to_string(),
_ => value.to_string(), }
}
pub fn generate_nonce() -> i64 {
timestamp_millis() as i64
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_build_params_string() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = CryptoComAuth::new(&credentials).unwrap();
let params = json!({
"instrument_name": "BTCUSD-PERP",
"side": "BUY",
"type": "LIMIT",
"price": "50000.00",
"quantity": "0.5"
});
let result = auth.build_params_string(¶ms);
assert_eq!(
result,
"instrument_nameBTCUSD-PERPprice50000.00quantity0.5sideBUYtypeLIMIT"
);
}
#[test]
fn test_build_params_string_empty() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = CryptoComAuth::new(&credentials).unwrap();
let params = json!({});
let result = auth.build_params_string(¶ms);
assert_eq!(result, "");
}
#[test]
fn test_sign_request() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = CryptoComAuth::new(&credentials).unwrap();
let params = json!({
"instrument_name": "BTCUSD-PERP"
});
let signature = auth.sign_request("private/get-open-orders", 1, ¶ms, 1234567890);
assert_eq!(signature.len(), 64);
assert!(signature.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_sign_ws_auth() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = CryptoComAuth::new(&credentials).unwrap();
let signature = auth.sign_ws_auth(1, 1234567890);
assert_eq!(signature.len(), 64);
assert!(signature.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_value_to_string() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = CryptoComAuth::new(&credentials).unwrap();
assert_eq!(auth.value_to_string(&json!("test")), "test");
assert_eq!(auth.value_to_string(&json!(123)), "123");
assert_eq!(auth.value_to_string(&json!(123.45)), "123.45");
assert_eq!(auth.value_to_string(&json!(true)), "true");
assert_eq!(auth.value_to_string(&json!(false)), "false");
assert_eq!(auth.value_to_string(&json!(null)), "null");
}
#[test]
fn test_signature_consistency() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = CryptoComAuth::new(&credentials).unwrap();
let params = json!({"side": "BUY", "price": "50000"});
let sig1 = auth.sign_request("private/create-order", 1, ¶ms, 1234567890);
let sig2 = auth.sign_request("private/create-order", 1, ¶ms, 1234567890);
assert_eq!(sig1, sig2);
}
}