1use crate::collections::FxHashMap;
7use crate::{CommonError, Result, SmartString};
8use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
9use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
10use hmac::{Hmac, Mac};
11use pkcs8::{
12 PrivateKeyInfo,
13 der::{Decode, asn1::OctetString},
14};
15use serde::Serialize;
16use sha2::Sha256;
17use smallvec::SmallVec;
18use std::time::{SystemTime, UNIX_EPOCH};
19use uuid::Uuid;
20
21type HmacSha256 = Hmac<Sha256>;
22
23pub mod header_keys {
25 use crate::SmartString;
26
27 pub const X_MBX_APIKEY: &str = "X-MBX-APIKEY";
29 pub const CONTENT_TYPE: &str = "Content-Type";
31 pub const APPLICATION_JSON: &str = "application/json";
33 pub const APPLICATION_FORM: &str = "application/x-www-form-urlencoded";
35
36 #[must_use]
38 pub fn x_mbx_apikey() -> SmartString {
39 X_MBX_APIKEY.into()
40 }
41 pub fn content_type() -> SmartString {
43 CONTENT_TYPE.into()
44 }
45 #[must_use]
46 pub fn application_json() -> SmartString {
48 APPLICATION_JSON.into()
49 }
50 #[must_use]
51 pub fn application_form() -> SmartString {
53 APPLICATION_FORM.into()
54 }
55}
56
57#[derive(Debug, Clone)]
59pub enum BinanceKeyType {
60 Hmac(SmartString),
62 Ed25519(SigningKey),
64}
65
66#[derive(Debug, Clone, Serialize)]
68pub struct BinanceWsLogonMessage {
69 pub id: SmartString,
71 pub method: SmartString,
73 pub params: BinanceWsLogonParams,
75}
76
77#[derive(Debug, Clone, Serialize)]
79pub struct BinanceWsLogonParams {
80 #[serde(rename = "apiKey")]
82 pub api_key: SmartString,
83 pub signature: SmartString,
85 pub timestamp: u64,
87}
88
89#[derive(Debug, Clone)]
91pub struct BinanceAuth {
92 api_key: SmartString,
94 key_type: BinanceKeyType,
95}
96
97impl BinanceAuth {
98 #[must_use]
100 pub const fn new_hmac(api_key: SmartString, secret_key: SmartString) -> Self {
101 Self {
102 api_key,
103 key_type: BinanceKeyType::Hmac(secret_key),
104 }
105 }
106
107 pub fn new_ed25519(api_key: SmartString, private_key: SmartString) -> Result<Self> {
126 let private_key_bytes = BASE64.decode(private_key.as_str()).map_err(|e| {
127 CommonError::Auth(crate::safe_format!("Invalid Ed25519 private key: {e}"))
128 })?;
129
130 if private_key_bytes.len() == 32 {
132 let mut key_array = [0u8; 32];
133 key_array.copy_from_slice(&private_key_bytes);
134 let signing_key = SigningKey::from_bytes(&key_array);
135
136 return Ok(Self {
137 api_key,
138 key_type: BinanceKeyType::Ed25519(signing_key),
139 });
140 }
141
142 if let Ok(pki) = PrivateKeyInfo::from_der(&private_key_bytes) {
145 let key_data = OctetString::from_der(pki.private_key)
149 .map(|s| s.as_bytes().to_vec())
150 .unwrap_or_else(|_| pki.private_key.to_vec());
151
152 if key_data.len() == 32 {
153 let mut key_array = [0u8; 32];
154 key_array.copy_from_slice(&key_data);
155 let signing_key = SigningKey::from_bytes(&key_array);
156
157 return Ok(Self {
158 api_key,
159 key_type: BinanceKeyType::Ed25519(signing_key),
160 });
161 }
162 }
163
164 Err(CommonError::Auth(SmartString::from(
165 "Invalid Ed25519 private key format. Expected either 32-byte raw key or valid PKCS#8 DER format",
166 )))
167 }
168
169 pub fn get_ed25519_public_key(&self) -> Result<SmartString> {
171 match &self.key_type {
172 BinanceKeyType::Ed25519(signing_key) => {
173 let verifying_key: VerifyingKey = signing_key.verifying_key();
174 Ok(BASE64.encode(verifying_key.as_bytes()).into())
175 }
176 _ => Err(CommonError::Auth(SmartString::from(
177 "Not an Ed25519 key type",
178 ))),
179 }
180 }
181
182 #[must_use]
186 pub fn build_query_string_optimized(params: &[(&str, &str)]) -> SmartString {
187 if params.is_empty() {
188 return SmartString::new();
189 }
190
191 let mut param_strings: SmallVec<[SmartString; 8]> = SmallVec::with_capacity(params.len());
193 let mut buffer = SmartString::new();
194
195 for (key, value) in params {
196 Self::encode_params_zero_copy(value, &mut buffer).unwrap_or_default();
197 let param = crate::safe_format!("{}={}", key, buffer.as_str());
198 param_strings.push(param);
199 }
200
201 SmartString::from(
203 param_strings
204 .iter()
205 .map(|s| s.as_str())
206 .collect::<SmallVec<[&str; 8]>>()
207 .join("&"),
208 )
209 }
210
211 pub fn encode_params_zero_copy(params: &str, buffer: &mut SmartString) -> Result<()> {
216 super::url_encode_params(params, buffer)
217 }
218
219 fn get_timestamp_ms() -> Result<u64> {
221 SystemTime::now()
222 .duration_since(UNIX_EPOCH)
223 .map_err(|e| CommonError::Auth(crate::safe_format!("System time error: {e}")))
224 .map(|d| d.as_millis() as u64)
225 }
226
227 fn generate_signature(&self, payload: &str) -> Result<SmartString> {
229 match &self.key_type {
230 BinanceKeyType::Hmac(secret) => {
231 let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
232 .map_err(|e| CommonError::Auth(crate::safe_format!("HMAC error: {e}")))?;
233 mac.update(payload.as_bytes());
234 let result = mac.finalize();
235 Ok(hex::encode(result.into_bytes()).into())
236 }
237 BinanceKeyType::Ed25519(signing_key) => {
238 let signature = signing_key.sign(payload.as_bytes());
239 Ok(BASE64.encode(signature.to_bytes()).into())
240 }
241 }
242 }
243
244 pub fn generate_headers(
247 &self,
248 method: &str,
249 _path: &str,
250 _params: Option<&[(&str, &str)]>,
251 body: Option<&str>,
252 ) -> Result<FxHashMap<SmartString, SmartString>> {
253 let mut headers = FxHashMap::default(); headers.insert(header_keys::x_mbx_apikey(), self.api_key.clone());
257
258 match method.to_uppercase().as_str() {
260 "POST" | "PUT" | "DELETE" => {
261 if body.is_some() {
262 headers.insert(header_keys::content_type(), header_keys::application_json());
263 } else {
264 headers.insert(header_keys::content_type(), header_keys::application_form());
265 }
266 }
267 _ => {} }
269
270 Ok(headers)
271 }
272
273 pub fn generate_signed_query_string(
276 &self,
277 params: Option<&[(&str, &str)]>,
278 ) -> Result<SmartString> {
279 let timestamp = Self::get_timestamp_ms()?;
280 let recv_window = 5000u64; let timestamp_str = timestamp.to_string();
283 let recv_window_str = recv_window.to_string();
284
285 let mut all_params = Vec::new();
286
287 if let Some(params) = params {
289 all_params.extend_from_slice(params);
290 }
291
292 all_params.push(("timestamp", ×tamp_str));
294 all_params.push(("recvWindow", &recv_window_str));
295
296 let payload = Self::build_query_string_optimized(&all_params);
297 let signature = self.generate_signature(&payload)?;
298
299 let mut signed_query = payload;
300 signed_query.push_str("&signature=");
301 signed_query.push_str(&signature);
302
303 Ok(signed_query)
304 }
305
306 pub fn generate_signed_params(&self, params: &[(&str, &str)]) -> Result<SmartString> {
308 let timestamp = Self::get_timestamp_ms()?;
309 let recv_window = 5000u64;
310
311 let timestamp_str = timestamp.to_string();
312 let recv_window_str = recv_window.to_string();
313
314 let mut all_params = params.to_vec();
315 all_params.push(("timestamp", ×tamp_str));
316 all_params.push(("recvWindow", &recv_window_str));
317
318 let payload = Self::build_query_string_optimized(&all_params);
319 let signature = self.generate_signature(&payload)?;
320
321 let mut signed_params = payload;
322 signed_params.push_str("&signature=");
323 signed_params.push_str(&signature);
324
325 Ok(signed_params)
326 }
327}
328
329impl BinanceAuth {
330 pub fn generate_ws_logon_message(&self) -> Result<BinanceWsLogonMessage> {
332 match &self.key_type {
334 BinanceKeyType::Ed25519(_) => {
335 let timestamp = Self::get_timestamp_ms()?;
336 let payload = crate::safe_format!("timestamp={timestamp}");
337 let signature = self.generate_signature(&payload)?;
338
339 let api_key = if self.api_key.is_empty() {
341 self.get_ed25519_public_key()?
342 } else {
343 self.api_key.clone()
344 };
345
346 Ok(BinanceWsLogonMessage {
347 id: Uuid::new_v4().to_string().into(),
348 method: "session.logon".into(),
349 params: BinanceWsLogonParams {
350 api_key,
351 signature,
352 timestamp,
353 },
354 })
355 }
356 BinanceKeyType::Hmac(_) => Err(CommonError::Auth(SmartString::from(
357 "WebSocket session authentication requires Ed25519 keys. HMAC keys are not supported for WebSocket session auth.",
358 ))),
359 }
360 }
361
362 pub fn generate_ws_auth(&self) -> Result<SmartString> {
364 match &self.key_type {
366 BinanceKeyType::Ed25519(_) => {
367 let logon_message = self.generate_ws_logon_message()?;
368 simd_json::to_string(&logon_message)
369 .map(|s| s.into())
370 .map_err(|e| {
371 CommonError::Auth(crate::safe_format!(
372 "Failed to serialize WebSocket logon message: {e}"
373 ))
374 })
375 }
376 BinanceKeyType::Hmac(_) => Err(CommonError::Auth(SmartString::from(
377 "WebSocket session authentication requires Ed25519 keys. HMAC keys are not supported.",
378 ))),
379 }
380 }
381
382 pub fn generate_timestamp() -> Result<u64> {
384 Self::get_timestamp_ms()
385 }
386
387 pub fn generate_timestamp_nanos() -> Result<u128> {
389 SystemTime::now()
390 .duration_since(UNIX_EPOCH)
391 .map_err(|e| CommonError::Auth(crate::safe_format!("System time error: {e}")))
392 .map(|d| d.as_nanos())
393 }
394
395 #[inline]
397 #[must_use]
398 pub fn param_count(params: Option<&[(&str, &str)]>) -> usize {
399 params.map_or(0, |p| p.len())
400 }
401
402 #[must_use]
404 pub fn generate_joined_param(params: &[(&str, &str)]) -> Option<SmartString> {
405 if params.is_empty() {
406 None
407 } else {
408 Some(Self::build_query_string_optimized(params))
409 }
410 }
411
412 #[must_use]
414 pub fn api_key(&self) -> &str {
415 &self.api_key
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422
423 #[test]
424 fn test_build_query_string() {
425 let params = [("symbol", "BTCUSDT"), ("side", "BUY"), ("type", "LIMIT")];
426 let result = BinanceAuth::build_query_string_optimized(¶ms);
427 assert_eq!(result, "symbol=BTCUSDT&side=BUY&type=LIMIT");
428 }
429
430 #[test]
431 fn test_build_query_string_optimized() {
432 let params = [("symbol", "BTCUSDT"), ("side", "BUY"), ("type", "LIMIT")];
433 let result = BinanceAuth::build_query_string_optimized(¶ms);
434 assert_eq!(result, "symbol=BTCUSDT&side=BUY&type=LIMIT");
435 }
436
437 #[test]
438 fn test_encode_params_zero_copy() {
439 let input = "param=value with spaces";
440 let mut buffer = SmartString::new();
441 BinanceAuth::encode_params_zero_copy(input, &mut buffer).unwrap();
442 assert_eq!(buffer, "param%3Dvalue%20with%20spaces");
443
444 let safe_input = "ABC-_.~";
446 BinanceAuth::encode_params_zero_copy(safe_input, &mut buffer).unwrap();
447 assert_eq!(buffer, safe_input);
448 }
449
450 #[test]
451 fn test_hmac_auth_creation() {
452 let auth = BinanceAuth::new_hmac("test_api_key".into(), "test_secret".into());
453 assert_eq!(auth.api_key(), "test_api_key");
454 }
455
456 #[test]
457 fn test_ed25519_raw_key_format() {
458 let raw_key_base64 = "1Dm2fKi3BRmgY9qMZ7F1PZQE+C8OLgKPUkZJfT/oD3w=";
461
462 let result =
463 BinanceAuth::new_ed25519("test_api_key".into(), SmartString::from(raw_key_base64));
464
465 assert!(
466 result.is_ok(),
467 "Failed to parse raw Ed25519 key: {:?}",
468 result.err()
469 );
470 let auth = result.unwrap();
471 assert_eq!(auth.api_key(), "test_api_key");
472
473 match &auth.key_type {
475 BinanceKeyType::Ed25519(_) => (),
476 _ => panic!("Expected Ed25519 key type"),
477 }
478 }
479
480 #[test]
481 fn test_ed25519_der_format() {
482 let der_key_base64 = "MC4CAQAwBQYDK2VwBCIEINQZtoSotwUZoGPajGexdT2UBPgvDi4Cj1JGSX0/6A98";
486
487 let result =
488 BinanceAuth::new_ed25519("test_api_key".into(), SmartString::from(der_key_base64));
489
490 assert!(
491 result.is_ok(),
492 "Failed to parse DER Ed25519 key: {:?}",
493 result.err()
494 );
495 let auth = result.unwrap();
496 assert_eq!(auth.api_key(), "test_api_key");
497
498 match &auth.key_type {
500 BinanceKeyType::Ed25519(_) => (),
501 _ => panic!("Expected Ed25519 key type"),
502 }
503 }
504
505 #[test]
506 fn test_ed25519_invalid_formats() {
507 let short_key = "dG9vX3Nob3J0"; let result = BinanceAuth::new_ed25519("test_api_key".into(), SmartString::from(short_key));
512 assert!(result.is_err());
513
514 let invalid_base64 = "not valid base64!@#$";
516 let result =
517 BinanceAuth::new_ed25519("test_api_key".into(), SmartString::from(invalid_base64));
518 assert!(result.is_err());
519
520 let wrong_length = BASE64.encode([0u8; 33]);
522 let result =
523 BinanceAuth::new_ed25519("test_api_key".into(), SmartString::from(wrong_length));
524 assert!(result.is_err());
525 }
526
527 #[test]
528 fn test_ed25519_public_key_from_raw_format() {
529 let raw_key_base64 = "1Dm2fKi3BRmgY9qMZ7F1PZQE+C8OLgKPUkZJfT/oD3w=";
531
532 let auth =
533 BinanceAuth::new_ed25519("test_api_key".into(), SmartString::from(raw_key_base64))
534 .expect("Failed to create auth");
535
536 let public_key_result = auth.get_ed25519_public_key();
537 assert!(public_key_result.is_ok());
538
539 let public_key = public_key_result.unwrap();
540 assert_eq!(public_key.len(), 44);
542 }
543
544 #[test]
545 fn test_signed_params_generation() {
546 let auth = BinanceAuth::new_hmac(
547 "vmPUZE6mv9SD5VNHk4HlWFsOr6aKE2zvsw0MuIgwCIPy6utIco14y7Ju91duEh8A".into(),
548 "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j".into(),
549 );
550
551 let params = [
552 ("symbol", "LTCBTC"),
553 ("side", "BUY"),
554 ("type", "LIMIT"),
555 ("timeInForce", "GTC"),
556 ("quantity", "1"),
557 ("price", "0.1"),
558 ];
559
560 let result = auth.generate_signed_params(¶ms);
561 assert!(result.is_ok());
562 let signed_params = result.unwrap();
563 assert!(signed_params.contains("signature="));
564 assert!(signed_params.contains("timestamp="));
565 assert!(signed_params.contains("recvWindow=5000"));
566 }
567
568 #[test]
569 fn test_ed25519_auth_creation() {
570 let private_key = BASE64.encode([0u8; 32]); let auth = BinanceAuth::new_ed25519("test_api_key".into(), private_key.into());
573 assert!(auth.is_ok());
574 let auth = auth.unwrap();
575 assert_eq!(auth.api_key(), "test_api_key");
576 }
577
578 #[test]
579 fn test_new_ed25519_valid_raw_key() {
580 let api_key = "test_api_key".into();
581 let private_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".into();
583 let auth = BinanceAuth::new_ed25519(api_key, private_key);
584 assert!(auth.is_ok());
585 }
586
587 #[test]
588 fn test_new_ed25519_valid_der_key() {
589 let api_key = "test_api_key".into();
590 let private_key = "MC4CAQAwBQYDK2VwBCIEINTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU".into();
599 let auth = BinanceAuth::new_ed25519(api_key, private_key);
600 assert!(auth.is_ok());
601 }
602
603 #[test]
604 fn test_new_ed25519_der_longer_format() {
605 let api_key = "test_api_key".into();
606 let key_data = [0xAA; 32];
610 let mut content = vec![
611 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x00, 0x00, 0x00, 0x00, 0x04, 0x22, 0x04, 0x20, ];
616 content.extend_from_slice(&key_data);
617 content.extend_from_slice(&[0x00; 16]); let mut longer_der = vec![0x30]; if content.len() < 128 {
622 longer_der.push(content.len() as u8);
623 } else {
624 longer_der.push(0x81); longer_der.push(content.len() as u8);
626 }
627 longer_der.extend_from_slice(&content);
628
629 let private_key = BASE64.encode(&longer_der).into();
630 let auth = BinanceAuth::new_ed25519(api_key, private_key);
631 assert!(auth.is_err());
633 assert!(
634 auth.unwrap_err()
635 .to_string()
636 .contains("Invalid Ed25519 private key format")
637 );
638 }
639
640 #[test]
641 fn test_new_ed25519_openssl_generated_key() {
642 let api_key = "test_api_key".into();
643 let openssl_key = "MC4CAQAwBQYDK2VwBCIEIHjl9sgmE5hzlz7pe6Mrc2K9L7JYQWhpabNQ8T9Ls3tO".into();
646 let auth = BinanceAuth::new_ed25519(api_key, openssl_key);
647 assert!(auth.is_ok());
648 }
649
650 #[test]
651 fn test_new_ed25519_pkcs8_wrapped_key() {
652 let api_key: SmartString = "test_api_key".into();
653 let pkcs8_v0_key =
656 "MC4CAQAwBQYDK2VwBCIEINTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU".into();
657 let auth = BinanceAuth::new_ed25519(api_key.clone(), pkcs8_v0_key);
658 assert!(auth.is_ok());
659
660 let raw_key = SmartString::from(
662 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", );
664 let auth_raw = BinanceAuth::new_ed25519(api_key.clone(), raw_key);
665 assert!(auth_raw.is_ok());
666
667 let invalid_key = "aW52YWxpZCBrZXk=".into(); let auth_invalid = BinanceAuth::new_ed25519(api_key, invalid_key);
670 assert!(auth_invalid.is_err());
671 }
672
673 #[test]
674 fn test_new_ed25519_invalid_key_not_base64() {
675 let api_key = "test_api_key".into();
676 let private_key = "this-is-not-base64".into();
677 let auth = BinanceAuth::new_ed25519(api_key, private_key);
678 assert!(auth.is_err());
679 let error = auth.unwrap_err();
680 assert!(matches!(error, CommonError::Auth(_)));
681 assert!(error.to_string().contains("Invalid Ed25519 private key"));
682 }
683
684 #[test]
685 fn test_new_ed25519_invalid_key_wrong_length() {
686 let api_key = "test_api_key".into();
687 let private_key = "AAAAAAAAAAAAAAAAAAAAAA==".into();
689 let auth = BinanceAuth::new_ed25519(api_key, private_key);
690 assert!(auth.is_err());
691 let error = auth.unwrap_err();
692 assert!(matches!(error, CommonError::Auth(_)));
693 assert!(
694 error
695 .to_string()
696 .contains("Invalid Ed25519 private key format")
697 );
698 }
699
700 #[test]
701 fn test_new_ed25519_invalid_der_malformed() {
702 let api_key = "test_api_key".into();
703 let private_key = "MBgCAQAwBQYDK2Vw".into(); let auth = BinanceAuth::new_ed25519(api_key, private_key);
707 assert!(auth.is_err());
708 let error = auth.unwrap_err();
709 assert!(matches!(error, CommonError::Auth(_)));
710 assert!(
711 error
712 .to_string()
713 .contains("Invalid Ed25519 private key format")
714 );
715 }
716
717 #[test]
718 fn test_ed25519_invalid_key_length() {
719 let private_key = BASE64.encode([0u8; 16]); let auth = BinanceAuth::new_ed25519("test_api_key".into(), private_key.into());
722 assert!(auth.is_err());
723 }
724
725 #[test]
726 fn test_ed25519_public_key_generation() {
727 let private_key = BASE64.encode([0u8; 32]);
728 let auth = BinanceAuth::new_ed25519("test_api_key".into(), private_key.into()).unwrap();
729
730 let public_key = auth.get_ed25519_public_key();
731 assert!(public_key.is_ok());
732 let public_key = public_key.unwrap();
733 assert!(!public_key.is_empty());
734 assert_eq!(public_key.len(), 44); }
736
737 #[test]
738 fn test_ed25519_signed_params_generation() {
739 let private_key = BASE64.encode([1u8; 32]); let auth = BinanceAuth::new_ed25519("test_api_key".into(), private_key.into()).unwrap();
741
742 let params = [
743 ("symbol", "BTCUSDT"),
744 ("side", "BUY"),
745 ("type", "LIMIT"),
746 ("quantity", "1"),
747 ("price", "50000"),
748 ];
749
750 let result = auth.generate_signed_params(¶ms);
751 assert!(result.is_ok());
752 let signed_params = result.unwrap();
753 assert!(signed_params.contains("signature="));
754 assert!(signed_params.contains("timestamp="));
755 assert!(signed_params.contains("recvWindow=5000"));
756
757 let signature_part = signed_params.split("signature=").nth(1).unwrap();
760 assert!(!signature_part.is_empty());
761 }
762
763 #[test]
764 fn test_ed25519_websocket_auth() {
765 let private_key = BASE64.encode([2u8; 32]);
766 let auth = BinanceAuth::new_ed25519("test_api_key".into(), private_key.into()).unwrap();
767
768 let ws_auth = auth.generate_ws_auth();
769 assert!(ws_auth.is_ok());
770 let ws_auth = ws_auth.unwrap();
771 assert!(ws_auth.contains("session.logon"));
772 assert!(ws_auth.contains("test_api_key"));
773 assert!(ws_auth.contains("signature"));
774 assert!(ws_auth.contains("timestamp"));
775 }
776
777 #[test]
778 fn test_headers_generation() {
779 let auth = BinanceAuth::new_hmac("test_api_key".into(), "test_secret".into());
780
781 let headers = auth.generate_headers("GET", "/api/v3/account", None, None);
783 assert!(headers.is_ok());
784 let headers = headers.unwrap();
785 assert_eq!(
786 headers.get(&header_keys::x_mbx_apikey()).unwrap(),
787 "test_api_key"
788 );
789 assert!(!headers.contains_key(&header_keys::content_type()));
790
791 let headers =
793 auth.generate_headers("POST", "/api/v3/order", None, Some("{\"test\": \"data\"}"));
794 assert!(headers.is_ok());
795 let headers = headers.unwrap();
796 assert_eq!(
797 headers.get(&header_keys::x_mbx_apikey()).unwrap(),
798 "test_api_key"
799 );
800 assert_eq!(
801 headers.get(&header_keys::content_type()).unwrap(),
802 &header_keys::application_json()
803 );
804
805 let headers = auth.generate_headers("POST", "/api/v3/order", None, None);
807 assert!(headers.is_ok());
808 let headers = headers.unwrap();
809 assert_eq!(
810 headers.get(&header_keys::content_type()).unwrap(),
811 &header_keys::application_form()
812 );
813 }
814
815 #[test]
816 fn test_signed_query_string_generation() {
817 let auth = BinanceAuth::new_hmac(
818 "vmPUZE6mv9SD5VNHk4HlWFsOr6aKE2zvsw0MuIgwCIPy6utIco14y7Ju91duEh8A".into(),
819 "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j".into(),
820 );
821
822 let params = [
823 ("symbol", "LTCBTC"),
824 ("side", "BUY"),
825 ("type", "LIMIT"),
826 ("timeInForce", "GTC"),
827 ("quantity", "1"),
828 ("price", "0.1"),
829 ];
830
831 let result = auth.generate_signed_query_string(Some(¶ms));
832 assert!(result.is_ok());
833 let signed_query = result.unwrap();
834 assert!(signed_query.contains("signature="));
835 assert!(signed_query.contains("timestamp="));
836 assert!(signed_query.contains("recvWindow=5000"));
837 assert!(signed_query.contains("symbol=LTCBTC"));
838 }
839
840 #[test]
841 fn test_ws_logon_message_ed25519() {
842 let private_key = BASE64.encode([1u8; 32]);
843 let auth = BinanceAuth::new_ed25519("test_api_key".into(), private_key.into()).unwrap();
844
845 let logon_message = auth.generate_ws_logon_message();
846 assert!(logon_message.is_ok());
847 let message = logon_message.unwrap();
848 assert_eq!(message.method, "session.logon");
849 assert_eq!(message.params.api_key, "test_api_key");
850 assert!(!message.params.signature.is_empty());
851 assert!(message.params.timestamp > 0);
852 }
853
854 #[test]
855 fn test_ws_logon_message_hmac_fails() {
856 let auth = BinanceAuth::new_hmac("test_api_key".into(), "test_secret".into());
857
858 let logon_message = auth.generate_ws_logon_message();
859 assert!(logon_message.is_err());
860 let error = logon_message.unwrap_err();
861 match error {
862 CommonError::Auth(msg) => {
863 assert!(msg.contains("WebSocket session authentication requires Ed25519 keys"));
864 }
865 _ => panic!("Expected Auth error"),
866 }
867 }
868
869 #[test]
870 fn test_usage_example() {
871 let hmac_auth = BinanceAuth::new_hmac("your_api_key".into(), "your_secret_key".into());
873
874 let params = [("symbol", "BTCUSDT"), ("side", "BUY")];
875 let signed_params = hmac_auth.generate_signed_params(¶ms);
876 assert!(signed_params.is_ok());
877
878 let headers = hmac_auth.generate_headers("GET", "/api/v3/account", None, None);
880 assert!(headers.is_ok());
881
882 let signed_query = hmac_auth.generate_signed_query_string(Some(¶ms));
884 assert!(signed_query.is_ok());
885
886 let private_key = BASE64.encode([1u8; 32]); let ed25519_auth =
889 BinanceAuth::new_ed25519("your_api_key".into(), private_key.into()).unwrap();
890
891 let public_key = ed25519_auth.get_ed25519_public_key().unwrap();
893 assert!(!public_key.is_empty());
894
895 let ed25519_signed_params = ed25519_auth.generate_signed_params(¶ms);
897 assert!(ed25519_signed_params.is_ok());
898
899 let headers = ed25519_auth.generate_headers("POST", "/api/v3/order", None, Some("{}"));
901 assert!(headers.is_ok());
902
903 let ws_auth = ed25519_auth.generate_ws_auth();
905 assert!(ws_auth.is_ok());
906
907 let ws_logon = ed25519_auth.generate_ws_logon_message();
909 assert!(ws_logon.is_ok());
910
911 let hmac_ws_auth = hmac_auth.generate_ws_auth();
913 assert!(hmac_ws_auth.is_err());
914 }
915
916 #[test]
917 fn test_optimized_methods() {
918 let _auth = BinanceAuth::new_hmac("test_api_key".into(), "test_secret".into());
919
920 let params = &[("symbol", "BTCUSDT"), ("side", "BUY")];
922 let query_string = BinanceAuth::build_query_string_optimized(params);
923 assert!(!query_string.is_empty());
924 assert!(query_string.contains("symbol"));
925 assert!(query_string.contains("BUY"));
926
927 assert_eq!(BinanceAuth::param_count(Some(params)), 2);
929 assert_eq!(BinanceAuth::param_count(None), 0);
930
931 let nanos1 = BinanceAuth::generate_timestamp_nanos().unwrap();
933 let nanos2 = BinanceAuth::generate_timestamp_nanos().unwrap();
934 assert!(nanos2 >= nanos1);
935
936 let millis1 = BinanceAuth::generate_timestamp().unwrap();
938 let millis2 = BinanceAuth::generate_timestamp().unwrap();
939 assert!(millis2 >= millis1);
940 }
941
942 #[test]
943 fn test_generate_joined_param() {
944 let empty_params: &[(&str, &str)] = &[];
946 let result = BinanceAuth::generate_joined_param(empty_params);
947 assert!(result.is_none());
948
949 let single_params = &[("key", "value")];
951 let result = BinanceAuth::generate_joined_param(single_params);
952 assert!(result.is_some());
953 let result = result.unwrap();
954 assert_eq!(result, SmartString::from("key=value"));
955
956 let multiple_params = &[("key1", "value1"), ("key2", "value2")];
958 let result = BinanceAuth::generate_joined_param(multiple_params);
959 assert!(result.is_some());
960 let result = result.unwrap();
961 assert_eq!(result, SmartString::from("key1=value1&key2=value2"));
962 }
963
964 #[test]
965 fn test_stack_allocation_performance() {
966 let small_params = &[("key", "value")];
970 let small_result = BinanceAuth::build_query_string_optimized(small_params);
971 assert_eq!(small_result, "key=value");
972
973 let medium_params = &[
975 ("param1", "value1"),
976 ("param2", "value2"),
977 ("param3", "value3"),
978 ("param4", "value4"),
979 ("param5", "value5"),
980 ("param6", "value6"),
981 ("param7", "value7"),
982 ("param8", "value8"),
983 ];
984 let medium_result = BinanceAuth::build_query_string_optimized(medium_params);
985 assert_eq!(medium_result.matches('&').count(), 7); let empty_params: &[(&str, &str)] = &[];
989 let empty_result = BinanceAuth::build_query_string_optimized(empty_params);
990 assert!(empty_result.is_empty());
991 }
992
993 #[test]
994 fn test_url_encoding_special_chars() {
995 let params = &[("market", "BTC-USDT"), ("state", "wait")];
996 let result = BinanceAuth::build_query_string_optimized(params);
997 assert_eq!(result, SmartString::from("market=BTC-USDT&state=wait"));
998
999 let params_with_encoding = &[("message", "hello world!"), ("special", "@#$%")];
1001 let result_encoded = BinanceAuth::build_query_string_optimized(params_with_encoding);
1002 assert!(result_encoded.contains("hello%20world%21"));
1003 assert!(result_encoded.contains("%40%23%24%25"));
1004 }
1005
1006 #[test]
1007 fn test_zero_copy_encoding_performance() {
1008 let mut buffer = SmartString::new();
1009
1010 let test_strings = ["simple", "with spaces", "special@chars", "unicodeā¢"];
1012
1013 for test_str in &test_strings {
1014 BinanceAuth::encode_params_zero_copy(test_str, &mut buffer).unwrap();
1015 assert!(!buffer.is_empty());
1016 }
1017 }
1018
1019 #[test]
1020 fn test_performance_optimizations() {
1021 let auth = BinanceAuth::new_hmac("test_api_key".into(), "test_secret".into());
1022
1023 let headers = auth
1025 .generate_headers("GET", "/api/v3/account", None, None)
1026 .unwrap();
1027 assert!(headers.len() <= 2); let params = &[("symbol", "BTCUSDT"), ("side", "BUY")];
1031 let headers_with_params = auth
1032 .generate_headers("POST", "/api/v3/order", Some(params), Some("{}"))
1033 .unwrap();
1034 assert_eq!(headers_with_params.len(), 2); let api_key_value = headers.get(&header_keys::x_mbx_apikey()).unwrap();
1038 assert_eq!(api_key_value, "test_api_key");
1039 }
1040}