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::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 tracing::debug!("🔍 [AUTH DEBUG] Generated signature: {}", signature);
68 Ok(signature)
69 }
70
71 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 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(¶ms_with_time)
123 }
124
125 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 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 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
169pub 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×tamp=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); }
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(¶ms);
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(¶ms, 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(¶ms);
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", ¶ms);
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", ¶ms);
286
287 assert_eq!(url, "https://api.binance.com/api/v3/time");
288 }
289}