use std::collections::HashMap;
use uuid::Uuid;
use crate::core::{
hmac_sha256_hex, timestamp_millis,
Credentials,
};
#[derive(Clone)]
pub struct BitstampAuth {
api_key: String,
api_secret: String,
}
impl BitstampAuth {
pub fn new(credentials: &Credentials) -> Self {
Self {
api_key: credentials.api_key.clone(),
api_secret: credentials.api_secret.clone(),
}
}
pub fn sign_request(
&self,
method: &str,
path: &str,
query: &str,
body: &str,
) -> HashMap<String, String> {
let nonce = Uuid::new_v4().to_string();
let timestamp = timestamp_millis().to_string();
let host = "www.bitstamp.net";
let version = "v2";
let content_type = "application/x-www-form-urlencoded";
let str_to_sign = if body.is_empty() {
format!(
"BITSTAMP {}\n{}\n{}\n{}\n{}\n\n{}\n{}\n{}\n",
self.api_key,
method.to_uppercase(),
host,
path,
query,
nonce,
timestamp,
version
)
} else {
format!(
"BITSTAMP {}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
self.api_key,
method.to_uppercase(),
host,
path,
query,
content_type,
nonce,
timestamp,
version,
body
)
};
let signature = hmac_sha256_hex(
self.api_secret.as_bytes(),
str_to_sign.as_bytes(),
).to_uppercase();
let mut headers = HashMap::new();
headers.insert("X-Auth".to_string(), format!("BITSTAMP {}", self.api_key));
headers.insert("X-Auth-Signature".to_string(), signature);
headers.insert("X-Auth-Nonce".to_string(), nonce);
headers.insert("X-Auth-Timestamp".to_string(), timestamp);
headers.insert("X-Auth-Version".to_string(), version.to_string());
if !body.is_empty() {
headers.insert("Content-Type".to_string(), content_type.to_string());
}
headers
}
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 = BitstampAuth::new(&credentials);
let headers = auth.sign_request("GET", "/api/v2/ticker/btcusd/", "", "");
assert!(headers.contains_key("X-Auth"));
assert!(headers.contains_key("X-Auth-Signature"));
assert!(headers.contains_key("X-Auth-Nonce"));
assert!(headers.contains_key("X-Auth-Timestamp"));
assert!(headers.contains_key("X-Auth-Version"));
assert_eq!(headers.get("X-Auth"), Some(&"BITSTAMP test_key".to_string()));
assert_eq!(headers.get("X-Auth-Version"), Some(&"v2".to_string()));
assert!(!headers.contains_key("Content-Type"));
}
#[test]
fn test_sign_request_post_with_body() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = BitstampAuth::new(&credentials);
let body = "amount=1.0&price=1000";
let headers = auth.sign_request("POST", "/api/v2/buy/btcusd/", "", body);
assert!(headers.contains_key("X-Auth"));
assert!(headers.contains_key("X-Auth-Signature"));
assert!(headers.contains_key("X-Auth-Nonce"));
assert!(headers.contains_key("X-Auth-Timestamp"));
assert!(headers.contains_key("X-Auth-Version"));
assert_eq!(
headers.get("Content-Type"),
Some(&"application/x-www-form-urlencoded".to_string())
);
let signature = headers.get("X-Auth-Signature").unwrap();
assert_eq!(signature.len(), 64);
assert!(signature.chars().all(|c| c.is_ascii_hexdigit()));
assert!(signature.chars().any(|c| c.is_ascii_uppercase())); }
#[test]
fn test_sign_request_post_without_body() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = BitstampAuth::new(&credentials);
let headers = auth.sign_request("POST", "/api/v2/balance/", "", "");
assert!(headers.contains_key("X-Auth"));
assert!(headers.contains_key("X-Auth-Signature"));
assert!(!headers.contains_key("Content-Type"));
}
#[test]
fn test_nonce_uniqueness() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = BitstampAuth::new(&credentials);
let headers1 = auth.sign_request("GET", "/api/v2/ticker/btcusd/", "", "");
let headers2 = auth.sign_request("GET", "/api/v2/ticker/btcusd/", "", "");
let nonce1 = headers1.get("X-Auth-Nonce").unwrap();
let nonce2 = headers2.get("X-Auth-Nonce").unwrap();
assert_ne!(nonce1, nonce2);
assert_eq!(nonce1.len(), 36);
assert_eq!(nonce2.len(), 36);
}
}