ccxt_exchanges/binance/
auth.rs1use 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)]
144 pub(crate) fn build_query_string(&self, params: &BTreeMap<String, String>) -> String {
145 let mut pairs: Vec<_> = params.iter().collect();
146 pairs.sort_by_key(|(k, _)| *k);
147
148 pairs
149 .iter()
150 .map(|(k, v)| format!("{k}={v}"))
151 .collect::<Vec<_>>()
152 .join("&")
153 }
154
155 pub fn add_auth_headers(&self, headers: &mut HashMap<String, String>) {
161 headers.insert(
162 "X-MBX-APIKEY".to_string(),
163 self.api_key.expose_secret().to_string(),
164 );
165 }
166
167 pub fn add_auth_headers_reqwest(&self, headers: &mut HeaderMap) {
173 if let Ok(header_name) = HeaderName::from_bytes(b"X-MBX-APIKEY") {
174 if let Ok(header_value) = HeaderValue::from_str(self.api_key.expose_secret()) {
175 headers.insert(header_name, header_value);
176 }
177 }
178 }
179}
180
181pub fn build_signed_url<S: std::hash::BuildHasher>(
193 base_url: &str,
194 endpoint: &str,
195 params: &HashMap<String, String, S>,
196) -> String {
197 let query_string = params
198 .iter()
199 .map(|(k, v)| format!("{k}={v}"))
200 .collect::<Vec<_>>()
201 .join("&");
202
203 if query_string.is_empty() {
204 format!("{base_url}{endpoint}")
205 } else {
206 format!("{base_url}{endpoint}?{query_string}")
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn test_sign() {
216 let auth = BinanceAuth::new("test_key", "test_secret");
217 let query = "symbol=BTCUSDT&side=BUY&type=LIMIT&quantity=1×tamp=1234567890";
218
219 let signature = auth.sign(query);
220 assert!(signature.is_ok());
221
222 let sig = signature.expect("Signature failed");
223 assert!(!sig.is_empty());
224 assert_eq!(sig.len(), 64); }
226
227 #[test]
228 fn test_sign_params() {
229 let auth = BinanceAuth::new("test_key", "test_secret");
230 let mut params = BTreeMap::new();
231 params.insert("symbol".to_string(), "BTCUSDT".to_string());
232 params.insert("side".to_string(), "BUY".to_string());
233
234 let signed = auth.sign_params(¶ms);
235 assert!(signed.is_ok());
236
237 let signed_params = signed.expect("Sign params failed");
238 assert!(signed_params.contains_key("signature"));
239 assert_eq!(
240 signed_params.get("symbol").expect("Missing symbol"),
241 "BTCUSDT"
242 );
243 }
244
245 #[test]
246 fn test_sign_with_timestamp() {
247 let auth = BinanceAuth::new("test_key", "test_secret");
248 let params = BTreeMap::new();
249 let timestamp = 1234567890i64;
250
251 let signed = auth.sign_with_timestamp(¶ms, timestamp, Some(5000));
252 assert!(signed.is_ok());
253
254 let signed_params = signed.expect("Sign timestamp failed");
255 assert!(signed_params.contains_key("timestamp"));
256 assert!(signed_params.contains_key("recvWindow"));
257 assert!(signed_params.contains_key("signature"));
258 assert_eq!(
259 signed_params.get("timestamp").expect("Missing timestamp"),
260 "1234567890"
261 );
262 assert_eq!(
263 signed_params.get("recvWindow").expect("Missing recvWindow"),
264 "5000"
265 );
266 }
267
268 #[test]
269 fn test_build_query_string() {
270 let auth = BinanceAuth::new("test_key", "test_secret");
271 let mut params = BTreeMap::new();
272 params.insert("symbol".to_string(), "BTCUSDT".to_string());
273 params.insert("side".to_string(), "BUY".to_string());
274
275 let query = auth.build_query_string(¶ms);
276 assert!(query == "side=BUY&symbol=BTCUSDT" || query == "symbol=BTCUSDT&side=BUY");
277 }
278
279 #[test]
280 fn test_add_auth_headers() {
281 let auth = BinanceAuth::new("my_api_key", "test_secret");
282 let mut headers = HashMap::new();
283
284 auth.add_auth_headers(&mut headers);
285
286 assert!(headers.contains_key("X-MBX-APIKEY"));
287 assert_eq!(
288 headers.get("X-MBX-APIKEY").expect("Missing header"),
289 "my_api_key"
290 );
291 }
292
293 #[test]
294 fn test_build_signed_url() {
295 let mut params = HashMap::new();
296 params.insert("symbol".to_string(), "BTCUSDT".to_string());
297 params.insert("signature".to_string(), "abc123".to_string());
298
299 let url = build_signed_url("https://api.binance.com", "/api/v3/order", ¶ms);
300
301 assert!(url.contains("https://api.binance.com/api/v3/order?"));
302 assert!(url.contains("symbol=BTCUSDT"));
303 assert!(url.contains("signature=abc123"));
304 }
305
306 #[test]
307 fn test_build_signed_url_empty_params() {
308 let params = HashMap::new();
309 let url = build_signed_url("https://api.binance.com", "/api/v3/time", ¶ms);
310
311 assert_eq!(url, "https://api.binance.com/api/v3/time");
312 }
313}