use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use crate::core::{
hmac_sha384, encode_hex_lower, timestamp_millis,
Credentials, ExchangeResult,
};
pub struct BitfinexAuth {
api_key: String,
api_secret: String,
last_nonce: AtomicU64,
}
impl BitfinexAuth {
pub fn new(credentials: &Credentials) -> ExchangeResult<Self> {
Ok(Self {
api_key: credentials.api_key.clone(),
api_secret: credentials.api_secret.clone(),
last_nonce: AtomicU64::new(0),
})
}
fn generate_nonce(&self) -> u64 {
let nonce = timestamp_millis() * 1000;
self.last_nonce.fetch_max(nonce, Ordering::SeqCst);
self.last_nonce.fetch_add(1, Ordering::SeqCst)
}
pub fn sign_request(
&self,
api_path: &str,
body: &str,
) -> HashMap<String, String> {
let nonce = self.generate_nonce();
let nonce_str = nonce.to_string();
let signature_string = format!("/api/{}{}{}", api_path, nonce_str, body);
let signature_bytes = hmac_sha384(
self.api_secret.as_bytes(),
signature_string.as_bytes(),
);
let signature = encode_hex_lower(&signature_bytes);
let mut headers = HashMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
headers.insert("bfx-nonce".to_string(), nonce_str);
headers.insert("bfx-apikey".to_string(), self.api_key.clone());
headers.insert("bfx-signature".to_string(), signature);
headers
}
pub fn api_key(&self) -> &str {
&self.api_key
}
pub fn sign_auth(&self, auth_payload: &str) -> String {
let signature_bytes = hmac_sha384(
self.api_secret.as_bytes(),
auth_payload.as_bytes(),
);
encode_hex_lower(&signature_bytes)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sign_request() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = BitfinexAuth::new(&credentials).unwrap();
let headers = auth.sign_request("v2/auth/r/wallets", "{}");
assert!(headers.contains_key("bfx-nonce"));
assert!(headers.contains_key("bfx-apikey"));
assert!(headers.contains_key("bfx-signature"));
assert_eq!(headers.get("bfx-apikey"), Some(&"test_key".to_string()));
assert_eq!(headers.get("Content-Type"), Some(&"application/json".to_string()));
let sig = headers.get("bfx-signature").unwrap();
assert_eq!(sig.len(), 96); assert!(sig.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_nonce_increasing() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = BitfinexAuth::new(&credentials).unwrap();
let nonce1 = auth.generate_nonce();
let nonce2 = auth.generate_nonce();
let nonce3 = auth.generate_nonce();
assert!(nonce2 > nonce1);
assert!(nonce3 > nonce2);
}
#[test]
fn test_signature_format() {
let credentials = Credentials::new("api_key_123", "api_secret_456");
let auth = BitfinexAuth::new(&credentials).unwrap();
let body = r#"{"type":"EXCHANGE LIMIT","symbol":"tBTCUSD","amount":"0.5","price":"10000"}"#;
let headers = auth.sign_request("v2/auth/w/order/submit", body);
assert!(headers.contains_key("bfx-nonce"));
assert!(headers.contains_key("bfx-apikey"));
assert!(headers.contains_key("bfx-signature"));
assert!(headers.contains_key("Content-Type"));
let nonce = headers.get("bfx-nonce").unwrap();
assert!(nonce.parse::<u64>().is_ok());
let sig = headers.get("bfx-signature").unwrap();
assert!(sig.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()));
}
}