rusty_common/auth/exchanges/
binance.rs

1//! Binance authentication implementation - Performance optimized for HFT
2//!
3//! This module provides high-performance HMAC/Ed25519-based authentication for Binance API.
4//! Optimized with stack allocation, pre-allocated constants, and zero-copy operations.
5
6use 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
23/// Binance header key constants for type safety and performance
24pub mod header_keys {
25    use crate::SmartString;
26
27    /// The header key for the API key.
28    pub const X_MBX_APIKEY: &str = "X-MBX-APIKEY";
29    /// The header key for the content type.
30    pub const CONTENT_TYPE: &str = "Content-Type";
31    /// The value for the application/json content type.
32    pub const APPLICATION_JSON: &str = "application/json";
33    /// The value for the application/x-www-form-urlencoded content type.
34    pub const APPLICATION_FORM: &str = "application/x-www-form-urlencoded";
35
36    /// Pre-allocated SmartString constants for zero-allocation header creation
37    #[must_use]
38    pub fn x_mbx_apikey() -> SmartString {
39        X_MBX_APIKEY.into()
40    }
41    /// Returns the `Content-Type` header value as a `SmartString`.
42    pub fn content_type() -> SmartString {
43        CONTENT_TYPE.into()
44    }
45    #[must_use]
46    /// Returns the `application/json` content type as a `SmartString`.
47    pub fn application_json() -> SmartString {
48        APPLICATION_JSON.into()
49    }
50    #[must_use]
51    /// Returns the `application/x-www-form-urlencoded` content type as a `SmartString`.
52    pub fn application_form() -> SmartString {
53        APPLICATION_FORM.into()
54    }
55}
56
57/// Binance API key type
58#[derive(Debug, Clone)]
59pub enum BinanceKeyType {
60    /// HMAC-SHA256 (deprecated but still supported)
61    Hmac(SmartString),
62    /// Ed25519 (recommended for best performance and security)
63    Ed25519(SigningKey),
64}
65
66/// WebSocket session logon message for Binance
67#[derive(Debug, Clone, Serialize)]
68pub struct BinanceWsLogonMessage {
69    /// A unique identifier for the request.
70    pub id: SmartString,
71    /// The method to be called, e.g., `session.logon`.
72    pub method: SmartString,
73    /// The parameters for the logon request.
74    pub params: BinanceWsLogonParams,
75}
76
77/// The parameters for a WebSocket logon request.
78#[derive(Debug, Clone, Serialize)]
79pub struct BinanceWsLogonParams {
80    /// The API key for authentication.
81    #[serde(rename = "apiKey")]
82    pub api_key: SmartString,
83    /// The signature for the request.
84    pub signature: SmartString,
85    /// The timestamp for the request.
86    pub timestamp: u64,
87}
88
89/// Binance authentication implementation for Spot API
90#[derive(Debug, Clone)]
91pub struct BinanceAuth {
92    /// API key for HMAC authentication, or empty for Ed25519
93    api_key: SmartString,
94    key_type: BinanceKeyType,
95}
96
97impl BinanceAuth {
98    /// Create new Binance auth with HMAC key
99    #[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    /// Create new Binance auth with Ed25519 key
108    ///
109    /// Supports two private key formats (base64-encoded):
110    ///
111    /// ## Format 1: Raw 32-byte key
112    /// - Direct 32-byte Ed25519 private key
113    /// - Example: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
114    ///
115    /// ## Format 2: DER-encoded key (PKCS#8 format)
116    /// - ASN.1 DER structure per RFC 8410
117    /// - Structure: SEQUENCE { version, algorithm, privateKey }
118    /// - Example: "MC4CAQAwBQYDK2VwBCIEINTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU"
119    ///
120    /// # Errors
121    /// Returns `CommonError::Auth` if:
122    /// - Base64 decoding fails
123    /// - Key format is not recognized
124    /// - DER structure is malformed
125    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        // Check if it's already a raw 32-byte key
131        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        // Try to parse as DER/PKCS#8 format using the pkcs8 crate
143        // This is more secure and handles all variations properly
144        if let Ok(pki) = PrivateKeyInfo::from_der(&private_key_bytes) {
145            // Use the `der` crate's `OctetString` to safely parse the private key,
146            // falling back to the raw private_key field. This is more robust than
147            // manual byte-checking of the ASN.1 structure.
148            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    /// Generate Ed25519 public key for API registration
170    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    /// Optimized parameter string building using SmallVec for stack allocation
183    /// Uses SmallVec<[SmartString; 8]> to avoid heap allocation for typical parameter counts
184    /// Enhanced with auth module's optimized implementation
185    #[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        // Use SmallVec to avoid heap allocation for typical parameter counts (< 8)
192        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        // Join using iterator to minimize allocations
202        SmartString::from(
203            param_strings
204                .iter()
205                .map(|s| s.as_str())
206                .collect::<SmallVec<[&str; 8]>>()
207                .join("&"),
208        )
209    }
210
211    /// URL-encode the provided parameter string using a pre-allocated buffer.
212    ///
213    /// This performs zero-copy encoding to avoid unpredictable heap
214    /// allocations on critical paths.
215    pub fn encode_params_zero_copy(params: &str, buffer: &mut SmartString) -> Result<()> {
216        super::url_encode_params(params, buffer)
217    }
218
219    /// Get current timestamp in milliseconds (performance optimized)
220    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    /// Generate signature for REST API request
228    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    /// Generate REST API authentication headers (optimized for performance)
245    /// Returns headers directly as HashMap to avoid conversion overhead in hot paths
246    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(); // FxHashMap doesn't support with_capacity
254
255        // Always add API key header
256        headers.insert(header_keys::x_mbx_apikey(), self.api_key.clone());
257
258        // Add content type based on method
259        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            _ => {} // GET requests don't need content-type
268        }
269
270        Ok(headers)
271    }
272
273    /// Generate signed query string for REST API requests
274    /// This includes the signature and should be appended to the URL
275    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; // 5 seconds (Binance default)
281
282        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        // Add existing params
288        if let Some(params) = params {
289            all_params.extend_from_slice(params);
290        }
291
292        // Add timestamp and recvWindow
293        all_params.push(("timestamp", &timestamp_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    /// Generate parameters with signature for signed requests
307    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", &timestamp_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    /// Generate WebSocket session logon message
331    pub fn generate_ws_logon_message(&self) -> Result<BinanceWsLogonMessage> {
332        // Only Ed25519 keys are supported for WebSocket session authentication
333        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                // For Ed25519, use public key as apiKey if no API key is provided
340                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    /// Generate WebSocket authentication message
363    pub fn generate_ws_auth(&self) -> Result<SmartString> {
364        // Only Ed25519 keys are supported for WebSocket session authentication
365        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    /// Generate timestamp in milliseconds
383    pub fn generate_timestamp() -> Result<u64> {
384        Self::get_timestamp_ms()
385    }
386
387    /// Generate timestamp in nanoseconds for high-precision timing
388    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    /// Fast parameter count for pre-allocation decisions
396    #[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    /// Generate joined parameter string (for backward compatibility)
403    #[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    /// Get API key
413    #[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(&params);
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(&params);
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        // Verify that safe characters remain unchanged
445        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        // Test with a raw 32-byte Ed25519 private key (base64 encoded)
459        // This is a test key - never use in production
460        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        // Verify it's an Ed25519 key
474        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        // Test with a DER-encoded Ed25519 private key (PKCS#8 format)
483        // This is a test key - never use in production
484        // Format: SEQUENCE { version, algorithm, privateKey }
485        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        // Verify it's an Ed25519 key
499        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        // Test with invalid key formats
508
509        // Too short
510        let short_key = "dG9vX3Nob3J0"; // "too_short" in base64
511        let result = BinanceAuth::new_ed25519("test_api_key".into(), SmartString::from(short_key));
512        assert!(result.is_err());
513
514        // Invalid base64
515        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        // Valid base64 but not a valid key format (33 bytes)
521        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        // Test that we can generate a public key from the private key
530        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        // Ed25519 public keys are 32 bytes, which is 44 chars in base64 (with padding)
541        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(&params);
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        // Test with a valid 32-byte private key (base64 encoded)
571        let private_key = BASE64.encode([0u8; 32]); // 32-byte zero key for testing
572        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        // 32-byte raw private key, base64 encoded
582        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        // A valid DER-encoded Ed25519 private key (base64 encoded)
591        // This is a sample key, not a real one.
592        // SEQUENCE (48 bytes)
593        //   INTEGER 0
594        //   SEQUENCE (5 bytes)
595        //     OBJECT IDENTIFIER 1.3.101.112 (Ed25519)
596        //   OCTET STRING (34 bytes)
597        //     OCTET STRING (32 bytes) - the private key
598        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        // Test that malformed DER structures are properly rejected
607        // This ensures the parser is strict about key format validation
608        // Build an invalid DER structure
609        let key_data = [0xAA; 32];
610        let mut content = vec![
611            0x02, 0x01, 0x00, // INTEGER 0
612            0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, // Ed25519 OID
613            0x00, 0x00, 0x00, 0x00, // Some padding
614            0x04, 0x22, 0x04, 0x20, // OCTET STRING wrapping
615        ];
616        content.extend_from_slice(&key_data);
617        content.extend_from_slice(&[0x00; 16]); // Extra data
618
619        // Create SEQUENCE with correct length
620        let mut longer_der = vec![0x30]; // SEQUENCE tag
621        if content.len() < 128 {
622            longer_der.push(content.len() as u8);
623        } else {
624            longer_der.push(0x81); // Long form, 1 byte
625            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        // This malformed DER should be rejected
632        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        // Key generated by: openssl genpkey -algorithm ED25519 -outform DER | base64
644        // This format may have slight variations in structure
645        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        // Test valid PKCS#8 v0 format with proper Ed25519 structure
654        // This is a valid PKCS#8 Ed25519 key with all zeros as the private key
655        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        // Test raw 32-byte key format (also valid)
661        let raw_key = SmartString::from(
662            "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", // 32 zero bytes
663        );
664        let auth_raw = BinanceAuth::new_ed25519(api_key.clone(), raw_key);
665        assert!(auth_raw.is_ok());
666
667        // Test invalid key format (not 32 bytes, not valid DER)
668        let invalid_key = "aW52YWxpZCBrZXk=".into(); // "invalid key" in base64
669        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        // 16-byte key, base64 encoded
688        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        // Malformed DER key - starts with SEQUENCE but has wrong structure
704        // This has SEQUENCE tag (0x30) but incorrect content
705        let private_key = "MBgCAQAwBQYDK2Vw".into(); // Too short, missing key data
706        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        // Test with invalid key length
720        let private_key = BASE64.encode([0u8; 16]); // Only 16 bytes, should fail
721        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); // Base64 encoded 32-byte key is 44 chars
735    }
736
737    #[test]
738    fn test_ed25519_signed_params_generation() {
739        let private_key = BASE64.encode([1u8; 32]); // Use non-zero key
740        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(&params);
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        // Ed25519 signatures are base64 encoded, so they should not contain hex chars like 'g'
758        // But they can contain A-Z, a-z, 0-9, +, /, =
759        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        // Test GET request headers
782        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        // Test POST request headers with body
792        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        // Test POST request headers without body
806        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(&params));
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        // Example 1: HMAC Authentication (Legacy but still supported)
872        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(&params);
876        assert!(signed_params.is_ok());
877
878        // Generate headers for REST API
879        let headers = hmac_auth.generate_headers("GET", "/api/v3/account", None, None);
880        assert!(headers.is_ok());
881
882        // Generate signed query string
883        let signed_query = hmac_auth.generate_signed_query_string(Some(&params));
884        assert!(signed_query.is_ok());
885
886        // Example 2: Ed25519 Authentication (Recommended)
887        let private_key = BASE64.encode([1u8; 32]); // In practice, use a real private key
888        let ed25519_auth =
889            BinanceAuth::new_ed25519("your_api_key".into(), private_key.into()).unwrap();
890
891        // Get public key for API registration
892        let public_key = ed25519_auth.get_ed25519_public_key().unwrap();
893        assert!(!public_key.is_empty());
894
895        // Generate signed parameters for API calls
896        let ed25519_signed_params = ed25519_auth.generate_signed_params(&params);
897        assert!(ed25519_signed_params.is_ok());
898
899        // Generate headers for REST API
900        let headers = ed25519_auth.generate_headers("POST", "/api/v3/order", None, Some("{}"));
901        assert!(headers.is_ok());
902
903        // Generate WebSocket authentication (only works with Ed25519)
904        let ws_auth = ed25519_auth.generate_ws_auth();
905        assert!(ws_auth.is_ok());
906
907        // Generate WebSocket logon message
908        let ws_logon = ed25519_auth.generate_ws_logon_message();
909        assert!(ws_logon.is_ok());
910
911        // HMAC keys cannot be used for WebSocket session authentication
912        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        // Test optimized parameter building
921        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        // Test parameter count utility
928        assert_eq!(BinanceAuth::param_count(Some(params)), 2);
929        assert_eq!(BinanceAuth::param_count(None), 0);
930
931        // Test nanosecond timestamp
932        let nanos1 = BinanceAuth::generate_timestamp_nanos().unwrap();
933        let nanos2 = BinanceAuth::generate_timestamp_nanos().unwrap();
934        assert!(nanos2 >= nanos1);
935
936        // Test millisecond timestamp
937        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        // Test empty params
945        let empty_params: &[(&str, &str)] = &[];
946        let result = BinanceAuth::generate_joined_param(empty_params);
947        assert!(result.is_none());
948
949        // Test single param
950        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        // Test multiple params
957        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        // Test parameter building with various sizes to ensure SmallVec stack allocation
967
968        // Test with 1 parameter (well within stack allocation)
969        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        // Test with 8 parameters (at the edge of stack allocation)
974        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); // 7 separators for 8 params
986
987        // Test that empty parameters return empty string efficiently
988        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        // Test with URL encoding required
1000        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        // Test multiple encodings with same buffer (zero allocation after first)
1011        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        // Test that headers are pre-allocated with exact capacity
1024        let headers = auth
1025            .generate_headers("GET", "/api/v3/account", None, None)
1026            .unwrap();
1027        assert!(headers.len() <= 2); // Should have at most 2 headers for GET
1028
1029        // Test with parameters
1030        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); // Should have exactly 2 headers
1035
1036        // Verify header contents are optimized SmartStrings
1037        let api_key_value = headers.get(&header_keys::x_mbx_apikey()).unwrap();
1038        assert_eq!(api_key_value, "test_api_key");
1039    }
1040}