1use ccxt_core::credentials::SecretString;
6use ccxt_core::{Error, Result};
7use hmac::{Hmac, Mac};
8use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
9use sha2::Sha256;
10use std::collections::{BTreeMap, HashMap};
11
12type HmacSha256 = Hmac<Sha256>;
13
14#[derive(Debug, Clone)]
18pub struct BinanceAuth {
19 api_key: SecretString,
21 secret: SecretString,
23}
24
25impl BinanceAuth {
26 pub fn new(api_key: impl Into<String>, secret: impl Into<String>) -> Self {
37 Self {
38 api_key: SecretString::new(api_key),
39 secret: SecretString::new(secret),
40 }
41 }
42
43 pub fn api_key(&self) -> &str {
45 self.api_key.expose_secret()
46 }
47
48 pub fn secret(&self) -> &str {
50 self.secret.expose_secret()
51 }
52
53 pub fn sign(&self, query_string: &str) -> Result<String> {
67 let mut mac = HmacSha256::new_from_slice(self.secret.expose_secret_bytes())
68 .map_err(|e| Error::authentication(format!("Invalid secret key: {}", e)))?;
69
70 mac.update(query_string.as_bytes());
71 let result = mac.finalize();
72 let signature = hex::encode(result.into_bytes());
73
74 Ok(signature)
75 }
76
77 pub fn sign_params(
91 &self,
92 params: &BTreeMap<String, String>,
93 ) -> Result<BTreeMap<String, String>> {
94 let query_string = self.build_query_string(params);
95 let signature = self.sign(&query_string)?;
96
97 let mut signed_params = params.clone();
98 signed_params.insert("signature".to_string(), signature);
99
100 Ok(signed_params)
101 }
102
103 pub fn sign_with_timestamp(
119 &self,
120 params: &BTreeMap<String, String>,
121 timestamp: i64,
122 recv_window: Option<u64>,
123 ) -> Result<BTreeMap<String, String>> {
124 let mut params_with_time = params.clone();
125 params_with_time.insert("timestamp".to_string(), timestamp.to_string());
126
127 if let Some(window) = recv_window {
128 params_with_time.insert("recvWindow".to_string(), window.to_string());
129 }
130
131 self.sign_params(¶ms_with_time)
132 }
133
134 #[allow(clippy::unused_self)]
145 pub(crate) fn build_query_string(&self, params: &BTreeMap<String, String>) -> String {
146 let mut pairs: Vec<_> = params.iter().collect();
147 pairs.sort_by_key(|(k, _)| *k);
148
149 pairs
150 .iter()
151 .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
152 .collect::<Vec<_>>()
153 .join("&")
154 }
155
156 pub fn add_auth_headers(&self, headers: &mut HashMap<String, String>) {
162 headers.insert(
163 "X-MBX-APIKEY".to_string(),
164 self.api_key.expose_secret().to_string(),
165 );
166 }
167
168 pub fn add_auth_headers_reqwest(&self, headers: &mut HeaderMap) {
174 if let Ok(header_name) = HeaderName::from_bytes(b"X-MBX-APIKEY") {
175 if let Ok(header_value) = HeaderValue::from_str(self.api_key.expose_secret()) {
176 headers.insert(header_name, header_value);
177 }
178 }
179 }
180}
181
182pub fn build_signed_url<S: std::hash::BuildHasher>(
194 base_url: &str,
195 endpoint: &str,
196 params: &HashMap<String, String, S>,
197) -> String {
198 let query_string = params
199 .iter()
200 .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
201 .collect::<Vec<_>>()
202 .join("&");
203
204 if query_string.is_empty() {
205 format!("{base_url}{endpoint}")
206 } else {
207 format!("{base_url}{endpoint}?{query_string}")
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn test_sign() {
217 let auth = BinanceAuth::new("test_key", "test_secret");
218 let query = "symbol=BTCUSDT&side=BUY&type=LIMIT&quantity=1×tamp=1234567890";
219
220 let signature = auth.sign(query);
221 assert!(signature.is_ok());
222
223 let sig = signature.expect("Signature failed");
224 assert!(!sig.is_empty());
225 assert_eq!(sig.len(), 64); }
227
228 #[test]
229 fn test_sign_params() {
230 let auth = BinanceAuth::new("test_key", "test_secret");
231 let mut params = BTreeMap::new();
232 params.insert("symbol".to_string(), "BTCUSDT".to_string());
233 params.insert("side".to_string(), "BUY".to_string());
234
235 let signed = auth.sign_params(¶ms);
236 assert!(signed.is_ok());
237
238 let signed_params = signed.expect("Sign params failed");
239 assert!(signed_params.contains_key("signature"));
240 assert_eq!(
241 signed_params.get("symbol").expect("Missing symbol"),
242 "BTCUSDT"
243 );
244 }
245
246 #[test]
247 fn test_sign_with_timestamp() {
248 let auth = BinanceAuth::new("test_key", "test_secret");
249 let params = BTreeMap::new();
250 let timestamp = 1234567890i64;
251
252 let signed = auth.sign_with_timestamp(¶ms, timestamp, Some(5000));
253 assert!(signed.is_ok());
254
255 let signed_params = signed.expect("Sign timestamp failed");
256 assert!(signed_params.contains_key("timestamp"));
257 assert!(signed_params.contains_key("recvWindow"));
258 assert!(signed_params.contains_key("signature"));
259 assert_eq!(
260 signed_params.get("timestamp").expect("Missing timestamp"),
261 "1234567890"
262 );
263 assert_eq!(
264 signed_params.get("recvWindow").expect("Missing recvWindow"),
265 "5000"
266 );
267 }
268
269 #[test]
270 fn test_build_query_string() {
271 let auth = BinanceAuth::new("test_key", "test_secret");
272 let mut params = BTreeMap::new();
273 params.insert("symbol".to_string(), "BTCUSDT".to_string());
274 params.insert("side".to_string(), "BUY".to_string());
275
276 let query = auth.build_query_string(¶ms);
277 assert!(query == "side=BUY&symbol=BTCUSDT" || query == "symbol=BTCUSDT&side=BUY");
278 }
279
280 #[test]
281 fn test_add_auth_headers() {
282 let auth = BinanceAuth::new("my_api_key", "test_secret");
283 let mut headers = HashMap::new();
284
285 auth.add_auth_headers(&mut headers);
286
287 assert!(headers.contains_key("X-MBX-APIKEY"));
288 assert_eq!(
289 headers.get("X-MBX-APIKEY").expect("Missing header"),
290 "my_api_key"
291 );
292 }
293
294 #[test]
295 fn test_build_signed_url() {
296 let mut params = HashMap::new();
297 params.insert("symbol".to_string(), "BTCUSDT".to_string());
298 params.insert("signature".to_string(), "abc123".to_string());
299
300 let url = build_signed_url("https://api.binance.com", "/api/v3/order", ¶ms);
301
302 assert!(url.contains("https://api.binance.com/api/v3/order?"));
303 assert!(url.contains("symbol=BTCUSDT"));
304 assert!(url.contains("signature=abc123"));
305 }
306
307 #[test]
308 fn test_build_signed_url_empty_params() {
309 let params = HashMap::new();
310 let url = build_signed_url("https://api.binance.com", "/api/v3/time", ¶ms);
311
312 assert_eq!(url, "https://api.binance.com/api/v3/time");
313 }
314
315 #[test]
316 fn test_build_query_string_url_encoding() {
317 let auth = BinanceAuth::new("test_key", "test_secret");
318 let mut params = BTreeMap::new();
319 params.insert("clientOrderId".to_string(), "order+123&test".to_string());
321 params.insert("symbol".to_string(), "BTC/USDT".to_string());
322
323 let query = auth.build_query_string(¶ms);
324
325 assert!(query.contains("clientOrderId=order%2B123%26test"));
327 assert!(query.contains("symbol=BTC%2FUSDT"));
328 assert!(query.contains("&"));
330 }
331
332 #[test]
333 fn test_build_query_string_space_encoding() {
334 let auth = BinanceAuth::new("test_key", "test_secret");
335 let mut params = BTreeMap::new();
336 params.insert("note".to_string(), "hello world".to_string());
337
338 let query = auth.build_query_string(¶ms);
339
340 assert!(query.contains("note=hello%20world"));
342 }
343
344 #[test]
345 fn test_build_query_string_equals_encoding() {
346 let auth = BinanceAuth::new("test_key", "test_secret");
347 let mut params = BTreeMap::new();
348 params.insert("filter".to_string(), "price=100".to_string());
349
350 let query = auth.build_query_string(¶ms);
351
352 assert!(query.contains("filter=price%3D100"));
354 }
355
356 #[test]
357 fn test_signature_consistency_with_encoding() {
358 let auth = BinanceAuth::new("test_key", "test_secret");
359 let mut params = BTreeMap::new();
360 params.insert("symbol".to_string(), "BTC/USDT".to_string());
361 params.insert("side".to_string(), "BUY".to_string());
362
363 let signed1 = auth.sign_params(¶ms).expect("Sign failed");
365 let signed2 = auth.sign_params(¶ms).expect("Sign failed");
366
367 assert_eq!(
369 signed1.get("signature").expect("Missing sig1"),
370 signed2.get("signature").expect("Missing sig2")
371 );
372 }
373
374 #[test]
375 fn test_signature_changes_with_different_params() {
376 let auth = BinanceAuth::new("test_key", "test_secret");
377
378 let mut params1 = BTreeMap::new();
379 params1.insert("symbol".to_string(), "BTCUSDT".to_string());
380
381 let mut params2 = BTreeMap::new();
382 params2.insert("symbol".to_string(), "ETHUSDT".to_string());
383
384 let signed1 = auth.sign_params(¶ms1).expect("Sign failed");
385 let signed2 = auth.sign_params(¶ms2).expect("Sign failed");
386
387 assert_ne!(
389 signed1.get("signature").expect("Missing sig1"),
390 signed2.get("signature").expect("Missing sig2")
391 );
392 }
393
394 #[test]
395 fn test_build_signed_url_with_special_chars() {
396 let mut params = HashMap::new();
397 params.insert("clientOrderId".to_string(), "test+order".to_string());
398 params.insert("signature".to_string(), "abc123".to_string());
399
400 let url = build_signed_url("https://api.binance.com", "/api/v3/order", ¶ms);
401
402 assert!(url.contains("clientOrderId=test%2Border"));
404 }
405
406 #[test]
407 fn test_alphabetical_sorting() {
408 let auth = BinanceAuth::new("test_key", "test_secret");
409 let mut params = BTreeMap::new();
410 params.insert("zebra".to_string(), "z".to_string());
411 params.insert("apple".to_string(), "a".to_string());
412 params.insert("mango".to_string(), "m".to_string());
413
414 let query = auth.build_query_string(¶ms);
415
416 assert_eq!(query, "apple=a&mango=m&zebra=z");
418 }
419}