ccxt_exchanges/binance/
auth.rs1use 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#[derive(Debug, Clone)]
15pub struct BinanceAuth {
16 api_key: String,
18 secret: String,
20}
21
22impl BinanceAuth {
23 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 pub fn api_key(&self) -> &str {
38 &self.api_key
39 }
40
41 pub fn secret(&self) -> &str {
43 &self.secret
44 }
45
46 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 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 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(¶ms_with_time)
125 }
126
127 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 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 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
171pub 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×tamp=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); }
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(¶ms);
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(¶ms, 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(¶ms);
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", ¶ms);
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", ¶ms);
288
289 assert_eq!(url, "https://api.binance.com/api/v3/time");
290 }
291}