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::{BTreeMap, 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        Ok(signature)
68    }
69
70    /// Signs a parameter map.
71    ///
72    /// # Arguments
73    ///
74    /// * `params` - Parameter map
75    ///
76    /// # Returns
77    ///
78    /// Returns a new parameter map containing the signature.
79    ///
80    /// # Errors
81    ///
82    /// Returns an error if signature generation fails.
83    pub fn sign_params(
84        &self,
85        params: &BTreeMap<String, String>,
86    ) -> Result<BTreeMap<String, String>> {
87        let query_string = self.build_query_string(params);
88        let signature = self.sign(&query_string)?;
89
90        let mut signed_params = params.clone();
91        signed_params.insert("signature".to_string(), signature);
92
93        Ok(signed_params)
94    }
95
96    /// Signs parameters with timestamp and optional receive window.
97    ///
98    /// # Arguments
99    ///
100    /// * `params` - Parameter map
101    /// * `timestamp` - Timestamp in milliseconds
102    /// * `recv_window` - Optional receive window in milliseconds
103    ///
104    /// # Returns
105    ///
106    /// Returns a new parameter map containing timestamp and signature.
107    ///
108    /// # Errors
109    ///
110    /// Returns an error if signature generation fails.
111    pub fn sign_with_timestamp(
112        &self,
113        params: &BTreeMap<String, String>,
114        timestamp: i64,
115        recv_window: Option<u64>,
116    ) -> Result<BTreeMap<String, String>> {
117        let mut params_with_time = params.clone();
118        params_with_time.insert("timestamp".to_string(), timestamp.to_string());
119
120        if let Some(window) = recv_window {
121            params_with_time.insert("recvWindow".to_string(), window.to_string());
122        }
123
124        self.sign_params(&params_with_time)
125    }
126
127    /// Builds a query string from parameters.
128    ///
129    /// # Arguments
130    ///
131    /// * `params` - Parameter map
132    ///
133    /// # Returns
134    ///
135    /// Returns a URL-encoded query string with parameters sorted by key.
136    pub(crate) fn build_query_string(&self, params: &BTreeMap<String, String>) -> String {
137        let mut pairs: Vec<_> = params.iter().collect();
138        pairs.sort_by_key(|(k, _)| *k);
139
140        let query_string = pairs
141            .iter()
142            .map(|(k, v)| format!("{}={}", k, v))
143            .collect::<Vec<_>>()
144            .join("&");
145        query_string
146    }
147
148    /// Adds authentication headers to the request.
149    ///
150    /// # Arguments
151    ///
152    /// * `headers` - Existing header map to modify
153    pub fn add_auth_headers(&self, headers: &mut HashMap<String, String>) {
154        headers.insert("X-MBX-APIKEY".to_string(), self.api_key.clone());
155    }
156
157    /// Adds authentication headers to a `reqwest` request.
158    ///
159    /// # Arguments
160    ///
161    /// * `headers` - Reqwest `HeaderMap` to modify
162    pub fn add_auth_headers_reqwest(&self, headers: &mut HeaderMap) {
163        if let Ok(header_name) = HeaderName::from_bytes(b"X-MBX-APIKEY") {
164            if let Ok(header_value) = HeaderValue::from_str(&self.api_key) {
165                headers.insert(header_name, header_value);
166            }
167        }
168    }
169}
170
171/// Builds a signed URL with query parameters.
172///
173/// # Arguments
174///
175/// * `base_url` - Base URL
176/// * `endpoint` - API endpoint path
177/// * `params` - Parameter map (must include signature)
178///
179/// # Returns
180///
181/// Returns the complete URL with query parameters.
182pub fn build_signed_url(
183    base_url: &str,
184    endpoint: &str,
185    params: &HashMap<String, String>,
186) -> String {
187    let query_string = params
188        .iter()
189        .map(|(k, v)| format!("{}={}", k, v))
190        .collect::<Vec<_>>()
191        .join("&");
192
193    if query_string.is_empty() {
194        format!("{}{}", base_url, endpoint)
195    } else {
196        format!("{}{}?{}", base_url, endpoint, query_string)
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_sign() {
206        let auth = BinanceAuth::new("test_key", "test_secret");
207        let query = "symbol=BTCUSDT&side=BUY&type=LIMIT&quantity=1&timestamp=1234567890";
208
209        let signature = auth.sign(query);
210        assert!(signature.is_ok());
211
212        let sig = signature.unwrap();
213        assert!(!sig.is_empty());
214        assert_eq!(sig.len(), 64); // HMAC-SHA256 produces 64 hex characters
215    }
216
217    #[test]
218    fn test_sign_params() {
219        let auth = BinanceAuth::new("test_key", "test_secret");
220        let mut params = BTreeMap::new();
221        params.insert("symbol".to_string(), "BTCUSDT".to_string());
222        params.insert("side".to_string(), "BUY".to_string());
223
224        let signed = auth.sign_params(&params);
225        assert!(signed.is_ok());
226
227        let signed_params = signed.unwrap();
228        assert!(signed_params.contains_key("signature"));
229        assert_eq!(signed_params.get("symbol").unwrap(), "BTCUSDT");
230    }
231
232    #[test]
233    fn test_sign_with_timestamp() {
234        let auth = BinanceAuth::new("test_key", "test_secret");
235        let params = BTreeMap::new();
236        let timestamp = 1234567890i64;
237
238        let signed = auth.sign_with_timestamp(&params, timestamp, Some(5000));
239        assert!(signed.is_ok());
240
241        let signed_params = signed.unwrap();
242        assert!(signed_params.contains_key("timestamp"));
243        assert!(signed_params.contains_key("recvWindow"));
244        assert!(signed_params.contains_key("signature"));
245        assert_eq!(signed_params.get("timestamp").unwrap(), "1234567890");
246        assert_eq!(signed_params.get("recvWindow").unwrap(), "5000");
247    }
248
249    #[test]
250    fn test_build_query_string() {
251        let auth = BinanceAuth::new("test_key", "test_secret");
252        let mut params = BTreeMap::new();
253        params.insert("symbol".to_string(), "BTCUSDT".to_string());
254        params.insert("side".to_string(), "BUY".to_string());
255
256        let query = auth.build_query_string(&params);
257        assert!(query == "side=BUY&symbol=BTCUSDT" || query == "symbol=BTCUSDT&side=BUY");
258    }
259
260    #[test]
261    fn test_add_auth_headers() {
262        let auth = BinanceAuth::new("my_api_key", "test_secret");
263        let mut headers = HashMap::new();
264
265        auth.add_auth_headers(&mut headers);
266
267        assert!(headers.contains_key("X-MBX-APIKEY"));
268        assert_eq!(headers.get("X-MBX-APIKEY").unwrap(), "my_api_key");
269    }
270
271    #[test]
272    fn test_build_signed_url() {
273        let mut params = HashMap::new();
274        params.insert("symbol".to_string(), "BTCUSDT".to_string());
275        params.insert("signature".to_string(), "abc123".to_string());
276
277        let url = build_signed_url("https://api.binance.com", "/api/v3/order", &params);
278
279        assert!(url.contains("https://api.binance.com/api/v3/order?"));
280        assert!(url.contains("symbol=BTCUSDT"));
281        assert!(url.contains("signature=abc123"));
282    }
283
284    #[test]
285    fn test_build_signed_url_empty_params() {
286        let params = HashMap::new();
287        let url = build_signed_url("https://api.binance.com", "/api/v3/time", &params);
288
289        assert_eq!(url, "https://api.binance.com/api/v3/time");
290    }
291}