ccxt_exchanges/binance/
auth.rs

1//! Binance authentication and signature module.
2//!
3//! Implements HMAC-SHA256 signature algorithm for API request authentication.
4
5use ccxt_core::{Error, Result};
6use hmac::{Hmac, Mac};
7use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
8use sha2::Sha256;
9use std::collections::HashMap;
10
11type HmacSha256 = Hmac<Sha256>;
12
13/// Binance authenticator.
14#[derive(Debug, Clone)]
15pub struct BinanceAuth {
16    /// API key.
17    api_key: String,
18    /// Secret key.
19    secret: String,
20}
21
22impl BinanceAuth {
23    /// Creates a new authenticator.
24    ///
25    /// # Arguments
26    ///
27    /// * `api_key` - API key
28    /// * `secret` - Secret key
29    pub fn new(api_key: impl Into<String>, secret: impl Into<String>) -> Self {
30        Self {
31            api_key: api_key.into(),
32            secret: secret.into(),
33        }
34    }
35
36    /// Returns the API key.
37    pub fn api_key(&self) -> &str {
38        &self.api_key
39    }
40
41    /// Returns the secret key.
42    pub fn secret(&self) -> &str {
43        &self.secret
44    }
45
46    /// Signs a query string using HMAC-SHA256.
47    ///
48    /// # Arguments
49    ///
50    /// * `query_string` - Query string to sign
51    ///
52    /// # Returns
53    ///
54    /// Returns the HMAC-SHA256 signature as a hex string.
55    ///
56    /// # Errors
57    ///
58    /// Returns an error if the secret key is invalid.
59    pub fn sign(&self, query_string: &str) -> Result<String> {
60        let mut mac = HmacSha256::new_from_slice(self.secret.as_bytes())
61            .map_err(|e| Error::authentication(format!("Invalid secret key: {}", e)))?;
62
63        mac.update(query_string.as_bytes());
64        let result = mac.finalize();
65        let signature = hex::encode(result.into_bytes());
66
67        tracing::debug!("🔍 [AUTH DEBUG] Generated signature: {}", signature);
68        Ok(signature)
69    }
70
71    /// Signs a parameter map.
72    ///
73    /// # Arguments
74    ///
75    /// * `params` - Parameter map
76    ///
77    /// # Returns
78    ///
79    /// Returns a new parameter map containing the signature.
80    ///
81    /// # Errors
82    ///
83    /// Returns an error if signature generation fails.
84    pub fn sign_params(&self, params: &HashMap<String, String>) -> Result<HashMap<String, String>> {
85        let query_string = self.build_query_string(params);
86        let signature = self.sign(&query_string)?;
87
88        let mut signed_params = params.clone();
89        signed_params.insert("signature".to_string(), signature);
90
91        Ok(signed_params)
92    }
93
94    /// Signs parameters with timestamp and optional receive window.
95    ///
96    /// # Arguments
97    ///
98    /// * `params` - Parameter map
99    /// * `timestamp` - Timestamp in milliseconds
100    /// * `recv_window` - Optional receive window in milliseconds
101    ///
102    /// # Returns
103    ///
104    /// Returns a new parameter map containing timestamp and signature.
105    ///
106    /// # Errors
107    ///
108    /// Returns an error if signature generation fails.
109    pub fn sign_with_timestamp(
110        &self,
111        params: &HashMap<String, String>,
112        timestamp: u64,
113        recv_window: Option<u64>,
114    ) -> Result<HashMap<String, String>> {
115        let mut params_with_time = params.clone();
116        params_with_time.insert("timestamp".to_string(), timestamp.to_string());
117
118        if let Some(window) = recv_window {
119            params_with_time.insert("recvWindow".to_string(), window.to_string());
120        }
121
122        self.sign_params(&params_with_time)
123    }
124
125    /// Builds a query string from parameters.
126    ///
127    /// # Arguments
128    ///
129    /// * `params` - Parameter map
130    ///
131    /// # Returns
132    ///
133    /// Returns a URL-encoded query string with parameters sorted by key.
134    pub(crate) fn build_query_string(&self, params: &HashMap<String, String>) -> String {
135        let mut pairs: Vec<_> = params.iter().collect();
136        pairs.sort_by_key(|(k, _)| *k);
137
138        let query_string = pairs
139            .iter()
140            .map(|(k, v)| format!("{}={}", k, v))
141            .collect::<Vec<_>>()
142            .join("&");
143        query_string
144    }
145
146    /// Adds authentication headers to the request.
147    ///
148    /// # Arguments
149    ///
150    /// * `headers` - Existing header map to modify
151    pub fn add_auth_headers(&self, headers: &mut HashMap<String, String>) {
152        headers.insert("X-MBX-APIKEY".to_string(), self.api_key.clone());
153    }
154
155    /// Adds authentication headers to a `reqwest` request.
156    ///
157    /// # Arguments
158    ///
159    /// * `headers` - Reqwest `HeaderMap` to modify
160    pub fn add_auth_headers_reqwest(&self, headers: &mut HeaderMap) {
161        if let Ok(header_name) = HeaderName::from_bytes(b"X-MBX-APIKEY") {
162            if let Ok(header_value) = HeaderValue::from_str(&self.api_key) {
163                headers.insert(header_name, header_value);
164            }
165        }
166    }
167}
168
169/// Builds a signed URL with query parameters.
170///
171/// # Arguments
172///
173/// * `base_url` - Base URL
174/// * `endpoint` - API endpoint path
175/// * `params` - Parameter map (must include signature)
176///
177/// # Returns
178///
179/// Returns the complete URL with query parameters.
180pub fn build_signed_url(
181    base_url: &str,
182    endpoint: &str,
183    params: &HashMap<String, String>,
184) -> String {
185    let query_string = params
186        .iter()
187        .map(|(k, v)| format!("{}={}", k, v))
188        .collect::<Vec<_>>()
189        .join("&");
190
191    if query_string.is_empty() {
192        format!("{}{}", base_url, endpoint)
193    } else {
194        format!("{}{}?{}", base_url, endpoint, query_string)
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn test_sign() {
204        let auth = BinanceAuth::new("test_key", "test_secret");
205        let query = "symbol=BTCUSDT&side=BUY&type=LIMIT&quantity=1&timestamp=1234567890";
206
207        let signature = auth.sign(query);
208        assert!(signature.is_ok());
209
210        let sig = signature.unwrap();
211        assert!(!sig.is_empty());
212        assert_eq!(sig.len(), 64); // HMAC-SHA256 produces 64 hex characters
213    }
214
215    #[test]
216    fn test_sign_params() {
217        let auth = BinanceAuth::new("test_key", "test_secret");
218        let mut params = HashMap::new();
219        params.insert("symbol".to_string(), "BTCUSDT".to_string());
220        params.insert("side".to_string(), "BUY".to_string());
221
222        let signed = auth.sign_params(&params);
223        assert!(signed.is_ok());
224
225        let signed_params = signed.unwrap();
226        assert!(signed_params.contains_key("signature"));
227        assert_eq!(signed_params.get("symbol").unwrap(), "BTCUSDT");
228    }
229
230    #[test]
231    fn test_sign_with_timestamp() {
232        let auth = BinanceAuth::new("test_key", "test_secret");
233        let params = HashMap::new();
234        let timestamp = 1234567890u64;
235
236        let signed = auth.sign_with_timestamp(&params, timestamp, Some(5000));
237        assert!(signed.is_ok());
238
239        let signed_params = signed.unwrap();
240        assert!(signed_params.contains_key("timestamp"));
241        assert!(signed_params.contains_key("recvWindow"));
242        assert!(signed_params.contains_key("signature"));
243        assert_eq!(signed_params.get("timestamp").unwrap(), "1234567890");
244        assert_eq!(signed_params.get("recvWindow").unwrap(), "5000");
245    }
246
247    #[test]
248    fn test_build_query_string() {
249        let auth = BinanceAuth::new("test_key", "test_secret");
250        let mut params = HashMap::new();
251        params.insert("symbol".to_string(), "BTCUSDT".to_string());
252        params.insert("side".to_string(), "BUY".to_string());
253
254        let query = auth.build_query_string(&params);
255        assert!(query == "side=BUY&symbol=BTCUSDT" || query == "symbol=BTCUSDT&side=BUY");
256    }
257
258    #[test]
259    fn test_add_auth_headers() {
260        let auth = BinanceAuth::new("my_api_key", "test_secret");
261        let mut headers = HashMap::new();
262
263        auth.add_auth_headers(&mut headers);
264
265        assert!(headers.contains_key("X-MBX-APIKEY"));
266        assert_eq!(headers.get("X-MBX-APIKEY").unwrap(), "my_api_key");
267    }
268
269    #[test]
270    fn test_build_signed_url() {
271        let mut params = HashMap::new();
272        params.insert("symbol".to_string(), "BTCUSDT".to_string());
273        params.insert("signature".to_string(), "abc123".to_string());
274
275        let url = build_signed_url("https://api.binance.com", "/api/v3/order", &params);
276
277        assert!(url.contains("https://api.binance.com/api/v3/order?"));
278        assert!(url.contains("symbol=BTCUSDT"));
279        assert!(url.contains("signature=abc123"));
280    }
281
282    #[test]
283    fn test_build_signed_url_empty_params() {
284        let params = HashMap::new();
285        let url = build_signed_url("https://api.binance.com", "/api/v3/time", &params);
286
287        assert_eq!(url, "https://api.binance.com/api/v3/time");
288    }
289}