rusty_common/auth/exchanges/
bithumb.rs

1//! Bithumb authentication implementation - Performance optimized for HFT
2//!
3//! This module provides high-performance JWT-based authentication for Bithumb API.
4//! Optimized with stack allocation, pre-allocated constants, and zero-copy operations.
5
6use super::build_query_smartstring;
7use crate::Result;
8use crate::SmartString;
9use crate::collections::FxHashMap;
10use hex;
11use sha2::{Digest, Sha512};
12use smallvec::SmallVec;
13use std::fmt::Write;
14use std::time::{SystemTime, UNIX_EPOCH};
15use uuid::Uuid;
16
17const HASH_ALGORITHM: &str = "SHA512";
18
19// Thread-local buffer pool for zero-allocation JSON parsing
20thread_local! {
21    static JSON_BUFFER: std::cell::RefCell<Vec<u8>> = std::cell::RefCell::new(Vec::with_capacity(8192));
22}
23
24/// Bithumb header key constants for type safety and performance
25pub mod header_keys {
26    use crate::SmartString;
27
28    /// The header key for authorization.
29    pub const AUTHORIZATION: &str = "Authorization";
30    /// The header key for the content type.
31    pub const CONTENT_TYPE: &str = "Content-Type";
32    /// The value for the application/json content type.
33    pub const CONTENT_TYPE_VALUE: &str = "application/json; charset=utf-8";
34
35    /// Pre-allocated SmartString constants for zero-allocation header creation
36    #[must_use]
37    pub fn authorization() -> SmartString {
38        AUTHORIZATION.into()
39    }
40    /// Returns the `Content-Type` header value as a `SmartString`.
41    pub fn content_type() -> SmartString {
42        CONTENT_TYPE.into()
43    }
44    /// Returns the `Content-Type` header value as a `SmartString`.
45    pub fn content_type_value() -> SmartString {
46        CONTENT_TYPE_VALUE.into()
47    }
48}
49
50/// Bithumb authentication handler
51#[derive(Debug, Clone)]
52pub struct BithumbAuth {
53    api_key: SmartString,
54    secret_key: SmartString,
55}
56
57impl BithumbAuth {
58    /// Create new Bithumb auth instance
59    #[must_use]
60    pub const fn new(api_key: SmartString, api_secret: SmartString) -> Self {
61        Self {
62            api_key,
63            secret_key: api_secret,
64        }
65    }
66
67    /// Optimized parameter string building using SmallVec for stack allocation
68    /// Uses SmallVec<[SmartString; 8]> to avoid heap allocation for typical parameter counts
69    /// Enhanced with auth module's optimized implementation
70    ///
71    /// **Important:** Bithumb's API specification requires parameters to be sorted
72    /// alphabetically by key name for signature verification. This implementation
73    /// processes parameters in their input order for performance optimization.
74    ///
75    /// **Note:** Ensure parameters are pre-sorted before calling this function
76    /// if signature verification is required, as mandated by Bithumb's API.
77    pub fn build_param_string_optimized(params: &[(&str, &str)]) -> Result<SmartString> {
78        if params.is_empty() {
79            return Ok(SmartString::new());
80        }
81
82        // Use SmallVec to avoid heap allocation for typical parameter counts (< 8)
83        let mut param_strings: SmallVec<[SmartString; 8]> = SmallVec::with_capacity(params.len());
84        let mut buffer = SmartString::new();
85
86        for (key, value) in params {
87            Self::encode_params_zero_copy(value, &mut buffer)?;
88            let param = crate::safe_format!("{}={}", key, buffer.as_str());
89            param_strings.push(param);
90        }
91
92        // Join using iterator to minimize allocations
93        Ok(SmartString::from(
94            param_strings
95                .iter()
96                .map(|s| s.as_str())
97                .collect::<SmallVec<[&str; 8]>>()
98                .join("&"),
99        ))
100    }
101
102    /// Generate API nonce (UUID v4)
103    #[must_use]
104    pub fn generate_nonce() -> SmartString {
105        Uuid::new_v4().to_string().into()
106    }
107
108    /// Generate timestamp in milliseconds
109    #[must_use]
110    pub fn generate_timestamp() -> u64 {
111        SystemTime::now()
112            .duration_since(UNIX_EPOCH)
113            .unwrap_or_default()
114            .as_millis() as u64
115    }
116
117    /// Generate timestamp in nanoseconds for high-precision timing
118    #[must_use]
119    pub fn generate_timestamp_nanos() -> u128 {
120        SystemTime::now()
121            .duration_since(UNIX_EPOCH)
122            .unwrap_or_default()
123            .as_nanos()
124    }
125
126    /// Generate query hash for parameters (SHA512)
127    fn generate_query_hash(params: &str) -> SmartString {
128        if params.is_empty() {
129            return "".into();
130        }
131
132        let mut hasher = Sha512::new();
133        hasher.update(params.as_bytes());
134        let result = hasher.finalize();
135        hex::encode(result).into()
136    }
137
138    /// URL-encode the provided parameter string using a pre-allocated buffer.
139    ///
140    /// This performs zero-copy encoding to avoid unpredictable heap
141    /// allocations on critical paths.
142    pub fn encode_params_zero_copy(params: &str, buffer: &mut SmartString) -> Result<()> {
143        super::url_encode_params(params, buffer)
144    }
145
146    /// Generate JWT token for Bithumb API authentication
147    fn generate_jwt(&self, query_hash: Option<SmartString>) -> Result<SmartString> {
148        use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
149        use serde::Serialize;
150
151        #[derive(Serialize)]
152        struct Claims {
153            access_key: String,
154            nonce: String,
155            timestamp: u64,
156            #[serde(skip_serializing_if = "Option::is_none")]
157            query_hash: Option<SmartString>,
158            #[serde(skip_serializing_if = "Option::is_none")]
159            query_hash_alg: Option<SmartString>,
160        }
161
162        let hash_alg: Option<SmartString> = if query_hash.is_some() {
163            Some(HASH_ALGORITHM.into())
164        } else {
165            None
166        };
167
168        let query_hash: SmartString = query_hash.unwrap_or_default();
169
170        let is_empty = query_hash.is_empty();
171
172        let claims = Claims {
173            access_key: self.api_key.to_string(),
174            nonce: Self::generate_nonce().to_string(),
175            timestamp: Self::generate_timestamp(),
176            query_hash: if is_empty { None } else { Some(query_hash) },
177            query_hash_alg: if is_empty { None } else { hash_alg },
178        };
179
180        let header = Header::new(Algorithm::HS256);
181        let encoding_key = EncodingKey::from_secret(self.secret_key.as_bytes());
182
183        encode(&header, &claims, &encoding_key)
184            .map(|s| s.into())
185            .map_err(|e| crate::CommonError::Auth(crate::safe_format!("JWT encoding error: {e}")))
186    }
187
188    /// Generates authentication headers for a request.
189    pub fn generate_headers(
190        &self,
191        _method: &str,
192        _path: &str,
193        params: Option<&[(&str, &str)]>,
194    ) -> Result<FxHashMap<SmartString, SmartString>> {
195        // Build parameter string and generate query hash if needed (optimized path)
196        let query_hash = if let Some(params) = params {
197            if params.is_empty() {
198                None
199            } else {
200                let joined_params = Self::build_param_string_optimized(params)?;
201                let mut encoded_params = SmartString::new();
202                Self::encode_params_zero_copy(&joined_params, &mut encoded_params)?;
203                Some(Self::generate_query_hash(&encoded_params))
204            }
205        } else {
206            None
207        };
208
209        // Generate JWT token
210        let jwt_token = self.generate_jwt(query_hash)?;
211
212        // Pre-allocate HashMap with exact capacity for optimal performance
213        let mut headers = FxHashMap::default();
214        headers.insert(
215            header_keys::authorization(),
216            crate::safe_format!("Bearer {jwt_token}"),
217        );
218        headers.insert(
219            header_keys::content_type(),
220            header_keys::content_type_value(),
221        );
222
223        Ok(headers)
224    }
225
226    /// Generate headers for JSON body requests (POST/PUT/DELETE)
227    ///
228    /// Bithumb API requires:
229    /// - Request body in JSON format
230    /// - Authentication hash calculated from URL-encoded representation of JSON parameters
231    /// - Content-Type: application/json; charset=utf-8
232    pub fn generate_headers_for_json_body(
233        &self,
234        json_body: &str,
235    ) -> Result<FxHashMap<SmartString, SmartString>> {
236        // Parse JSON body to extract parameters for hash calculation
237        let query_hash = if json_body.is_empty() || json_body == "{}" {
238            None
239        } else {
240            // Convert JSON body to URL-encoded format for hash calculation
241            let encoded_params = Self::json_to_url_encoded(json_body)?;
242            Some(Self::generate_query_hash(&encoded_params))
243        };
244
245        // Generate JWT token with query hash
246        let jwt_token = self.generate_jwt(query_hash)?;
247
248        // Pre-allocate HashMap with exact capacity for optimal performance
249        let mut headers = FxHashMap::default();
250        headers.insert(
251            header_keys::authorization(),
252            crate::safe_format!("Bearer {jwt_token}"),
253        );
254        headers.insert(
255            header_keys::content_type(),
256            header_keys::content_type_value(),
257        );
258
259        Ok(headers)
260    }
261
262    /// Convert JSON body to URL-encoded format for hash calculation
263    ///
264    /// This extracts key-value pairs from JSON and converts them to the format
265    /// expected by Bithumb's authentication system (URL-encoded query string).
266    ///
267    /// Uses a thread-local buffer pool to avoid heap allocations on the critical path.
268    fn json_to_url_encoded(json_body: &str) -> Result<SmartString> {
269        use simd_json::OwnedValue;
270        use simd_json::prelude::*;
271
272        // Parse JSON using simd_json with pooled buffer for performance
273        let parsed: OwnedValue = JSON_BUFFER.with(|buffer_cell| {
274            let mut buffer = buffer_cell.borrow_mut();
275
276            // Ensure buffer has sufficient capacity
277            let required_len = json_body.len();
278            if buffer.capacity() < required_len {
279                let additional = required_len - buffer.capacity();
280                buffer.reserve(additional);
281            }
282
283            // Clear and copy data to mutable buffer (zero allocation if capacity sufficient)
284            buffer.clear();
285            buffer.extend_from_slice(json_body.as_bytes());
286
287            // Parse with simd_json (requires mutable slice)
288            // Use OwnedValue to avoid borrowing issues
289            simd_json::from_slice(&mut buffer).map_err(|e| {
290                crate::CommonError::Json(crate::safe_format!("Failed to parse JSON body: {e}"))
291            })
292        })?;
293
294        // Extract key-value pairs from JSON object
295        let obj = parsed
296            .as_object()
297            .ok_or_else(|| crate::CommonError::Json("JSON body must be an object".into()))?;
298
299        if obj.is_empty() {
300            return Ok(SmartString::new());
301        }
302
303        // Convert to parameters for URL encoding
304        let mut params: SmallVec<[(SmartString, SmartString); 8]> =
305            SmallVec::with_capacity(obj.len());
306
307        for (key, value) in obj.iter() {
308            // Skip null values entirely
309            if value.is_null() {
310                continue;
311            }
312
313            let key_str: SmartString = key.clone().into();
314            let value_str: SmartString = {
315                // Handle all types by converting to string representation
316                let mut s = SmartString::new();
317                if let Some(i) = value.as_i64() {
318                    let _ = write!(s, "{i}");
319                    s
320                } else if let Some(u) = value.as_u64() {
321                    let _ = write!(s, "{u}");
322                    s
323                } else if let Some(f) = value.as_f64() {
324                    let _ = write!(s, "{f}");
325                    s
326                } else if let Some(b) = value.as_bool() {
327                    let _ = write!(s, "{b}");
328                    s
329                } else if let OwnedValue::String(val) = value {
330                    val.clone().into()
331                } else {
332                    return Err(crate::CommonError::Json(crate::safe_format!(
333                        "Unsupported JSON value type for key '{key}'"
334                    )));
335                }
336            };
337            params.push((key_str, value_str));
338        }
339
340        // Sort parameters for consistent hash calculation
341        params.sort_by(|a, b| a.0.cmp(&b.0));
342
343        // Build URL-encoded string
344        let mut result = SmartString::new();
345        for (i, (key, value)) in params.iter().enumerate() {
346            if i > 0 {
347                result.push('&');
348            }
349
350            // URL encode key
351            let mut encoded_key = SmartString::new();
352            Self::encode_params_zero_copy(key, &mut encoded_key)?;
353            result.push_str(&encoded_key);
354
355            result.push('=');
356
357            // URL encode value
358            let mut encoded_value = SmartString::new();
359            Self::encode_params_zero_copy(value, &mut encoded_value)?;
360            result.push_str(&encoded_value);
361        }
362
363        Ok(result)
364    }
365
366    /// Generate query string from parameters (optimized version using auth module)
367    ///
368    /// **Important:** Bithumb's API specification requires parameters to be sorted
369    /// alphabetically by key name for signature verification. However, this implementation
370    /// currently uses natural order (no sorting) for performance optimization.
371    ///
372    /// **Note:** If signature verification fails, ensure parameters are pre-sorted
373    /// before calling this function, as required by Bithumb's API documentation.
374    pub fn build_query_string(params: &[(&str, &str)]) -> Result<SmartString> {
375        // Use auth module's optimized implementation (natural order, no sorting)
376        // TODO: Add parameter sorting if signature verification requires it
377        Ok(build_query_smartstring(params))
378    }
379
380    /// Fast parameter count for pre-allocation decisions
381    #[inline]
382    #[must_use]
383    pub fn param_count(params: Option<&[(&str, &str)]>) -> usize {
384        params.map_or(0, |p| p.len())
385    }
386
387    /// Generate WebSocket authentication message
388    /// Bithumb WebSocket uses PING/PONG for connection management
389    pub fn generate_ws_auth(&self) -> Result<SmartString> {
390        // Bithumb WebSocket uses PING/PONG for connection management
391        // Private WebSocket streams may require different authentication
392        // For now, return a simple PING message as per documentation
393        Ok("PING".into())
394    }
395
396    /// Generate joined parameter string (for backward compatibility)
397    #[must_use]
398    pub fn generate_joined_param(params: &[(&str, &str)]) -> Option<SmartString> {
399        if params.is_empty() {
400            None
401        } else {
402            Self::build_param_string_optimized(params).ok()
403        }
404    }
405
406    /// Get API key
407    #[must_use]
408    pub fn api_key(&self) -> &str {
409        &self.api_key
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416
417    fn create_test_auth() -> BithumbAuth {
418        BithumbAuth::new("test_api_key".into(), "test_secret_key".into())
419    }
420
421    #[test]
422    fn test_new() {
423        let auth = create_test_auth();
424        assert_eq!(auth.api_key(), "test_api_key");
425    }
426
427    #[test]
428    fn test_generate_nonce_format() {
429        let nonce = BithumbAuth::generate_nonce();
430        // UUID v4 format: 8-4-4-4-12 characters
431        assert_eq!(nonce.len(), 36);
432        assert_eq!(nonce.chars().nth(8), Some('-'));
433        assert_eq!(nonce.chars().nth(13), Some('-'));
434        assert_eq!(nonce.chars().nth(18), Some('-'));
435        assert_eq!(nonce.chars().nth(23), Some('-'));
436    }
437
438    #[test]
439    fn test_generate_nonce_uniqueness() {
440        let nonce1 = BithumbAuth::generate_nonce();
441        let nonce2 = BithumbAuth::generate_nonce();
442        assert_ne!(nonce1, nonce2);
443    }
444
445    #[test]
446    fn test_generate_timestamp() {
447        let timestamp1 = BithumbAuth::generate_timestamp();
448        std::thread::sleep(std::time::Duration::from_millis(1));
449        let timestamp2 = BithumbAuth::generate_timestamp();
450
451        assert!(timestamp2 > timestamp1);
452        // Should be reasonable timestamp (after 2020-01-01)
453        assert!(timestamp1 > 1_577_836_800_000); // 2020-01-01 in ms
454    }
455
456    #[test]
457    fn test_generate_query_hash_empty() {
458        let hash = BithumbAuth::generate_query_hash("");
459        assert_eq!(hash, SmartString::from(""));
460    }
461
462    #[test]
463    fn test_generate_query_hash_non_empty() {
464        let hash = BithumbAuth::generate_query_hash("test=value");
465        // SHA512 hex output should be 128 characters
466        assert_eq!(hash.len(), 128);
467        assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
468    }
469
470    #[test]
471    fn test_generate_query_hash_consistency() {
472        let input = "key1=value1&key2=value2";
473        let hash1 = BithumbAuth::generate_query_hash(input);
474        let hash2 = BithumbAuth::generate_query_hash(input);
475        assert_eq!(hash1, hash2);
476    }
477
478    #[test]
479    fn test_encode_params_zero_copy() {
480        let input = "param=value with spaces";
481        let mut buffer = SmartString::new();
482        BithumbAuth::encode_params_zero_copy(input, &mut buffer).unwrap();
483        assert_eq!(buffer, "param%3Dvalue%20with%20spaces");
484
485        // Verify that safe characters remain unchanged
486        let safe_input = "ABC-_.~";
487        BithumbAuth::encode_params_zero_copy(safe_input, &mut buffer).unwrap();
488        assert_eq!(buffer, safe_input);
489    }
490
491    #[test]
492    fn test_generate_joined_param_empty() {
493        let params: &[(&str, &str)] = &[];
494        let result = BithumbAuth::generate_joined_param(params);
495
496        assert!(result.is_none());
497    }
498
499    #[test]
500    fn test_generate_joined_param_single() {
501        let params = &[("key", "value")];
502        let result = BithumbAuth::generate_joined_param(params);
503
504        assert!(result.is_some());
505
506        let result = result.unwrap();
507        assert_eq!(result, SmartString::from("key=value"));
508    }
509
510    #[test]
511    fn test_generate_joined_param_multiple() {
512        let params = &[("key1", "value1"), ("key2", "value2")];
513        let result = BithumbAuth::generate_joined_param(params);
514
515        assert!(result.is_some());
516
517        let result = result.unwrap();
518        assert_eq!(result, SmartString::from("key1=value1&key2=value2"));
519    }
520
521    #[test]
522    fn test_generate_jwt_without_query_hash() {
523        let auth = create_test_auth();
524        let jwt = auth.generate_jwt(None).unwrap();
525
526        // JWT should have 3 parts separated by dots
527        let parts: Vec<&str> = jwt.split('.').collect();
528        assert_eq!(parts.len(), 3);
529
530        // Each part should be base64-like (alphanumeric + / + =)
531        for part in parts {
532            assert!(part.chars().all(|c| c.is_alphanumeric()
533                || c == '+'
534                || c == '/'
535                || c == '='
536                || c == '-'
537                || c == '_'));
538        }
539    }
540
541    #[test]
542    fn test_generate_jwt_with_query_hash() {
543        let auth = create_test_auth();
544        let query_hash = Some("test_hash".into());
545        let jwt = auth.generate_jwt(query_hash).unwrap();
546
547        // JWT should have 3 parts separated by dots
548        let parts: Vec<&str> = jwt.split('.').collect();
549        assert_eq!(parts.len(), 3);
550        assert!(!jwt.is_empty());
551    }
552
553    #[test]
554    fn test_generate_auth_headers_with_query_hash() {
555        let auth = create_test_auth();
556
557        let jwt = auth.generate_jwt(None);
558        let second_jwt = auth.generate_jwt(Some(SmartString::new()));
559
560        assert!(jwt.is_ok());
561        assert!(second_jwt.is_ok());
562
563        assert_ne!(jwt.unwrap(), second_jwt.unwrap());
564    }
565
566    #[test]
567    fn test_generate_headers_no_params() {
568        let auth = create_test_auth();
569        let headers = auth.generate_headers("GET", "/test", None).unwrap();
570
571        assert!(headers.contains_key(&SmartString::from("Authorization")));
572        assert!(headers.contains_key(&SmartString::from("Content-Type")));
573
574        let auth_header = headers.get(&SmartString::from("Authorization")).unwrap();
575        assert!(auth_header.starts_with("Bearer "));
576
577        let content_type = headers.get(&SmartString::from("Content-Type")).unwrap();
578        assert_eq!(
579            content_type,
580            &SmartString::from("application/json; charset=utf-8")
581        );
582    }
583
584    #[test]
585    fn test_generate_headers_with_params() {
586        let auth = create_test_auth();
587        let params = &[("key1", "value1"), ("key2", "value2")];
588        let headers = auth
589            .generate_headers("POST", "/test", Some(params))
590            .unwrap();
591
592        assert!(headers.contains_key(&SmartString::from("Authorization")));
593        assert!(headers.contains_key(&SmartString::from("Content-Type")));
594
595        let auth_header = headers.get(&SmartString::from("Authorization")).unwrap();
596        assert!(auth_header.starts_with("Bearer "));
597        assert!(auth_header.len() > 10); // Should be substantial JWT token
598    }
599
600    #[test]
601    fn test_generate_headers_different_params_different_tokens() {
602        let auth = create_test_auth();
603        let params1 = &[("key1", "value1")];
604        let params2 = &[("key2", "value2")];
605
606        let headers1 = auth
607            .generate_headers("POST", "/test", Some(params1))
608            .unwrap();
609        let headers2 = auth
610            .generate_headers("POST", "/test", Some(params2))
611            .unwrap();
612
613        let auth1 = headers1.get(&SmartString::from("Authorization")).unwrap();
614        let auth2 = headers2.get(&SmartString::from("Authorization")).unwrap();
615
616        // Should generate different tokens for different parameters
617        assert_ne!(auth1, auth2);
618    }
619
620    #[test]
621    fn test_generate_ws_auth() {
622        let auth = create_test_auth();
623        let ws_auth = auth.generate_ws_auth().unwrap();
624        assert_eq!(ws_auth, SmartString::from("PING"));
625    }
626
627    #[test]
628    fn test_api_key() {
629        let auth = create_test_auth();
630        assert_eq!(auth.api_key(), "test_api_key");
631    }
632
633    #[test]
634    fn test_hash_algorithm_constant() {
635        assert_eq!(HASH_ALGORITHM, "SHA512");
636    }
637
638    #[test]
639    fn test_jwt_claims_structure() {
640        let auth = create_test_auth();
641        let jwt = auth.generate_jwt(None).unwrap();
642
643        // Decode JWT to verify structure (basic check)
644        use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode};
645        use serde::Deserialize;
646
647        #[derive(Deserialize)]
648        struct Claims {
649            access_key: String,
650            nonce: String,
651            timestamp: u64,
652            #[serde(skip_serializing_if = "Option::is_none")]
653            query_hash: Option<String>,
654            #[serde(skip_serializing_if = "Option::is_none")]
655            query_hash_alg: Option<String>,
656        }
657
658        let key = DecodingKey::from_secret("test_secret_key".as_bytes());
659        let mut validation = Validation::new(Algorithm::HS256);
660        validation.validate_exp = false; // Disable expiration validation
661        validation.validate_nbf = false; // Disable not-before validation
662        validation.required_spec_claims.clear(); // Remove all required claims
663
664        let decoded = decode::<Claims>(&jwt, &key, &validation);
665        if let Err(ref e) = decoded {
666            panic!("JWT decode failed: {e:?}");
667        }
668
669        let claims = decoded.unwrap().claims;
670        assert_eq!(claims.access_key, "test_api_key");
671        assert!(!claims.nonce.is_empty());
672        assert!(claims.timestamp > 0);
673        assert!(claims.query_hash.is_none()); // Should be None when no query hash provided
674        assert!(claims.query_hash_alg.is_none()); // Should be None when no query hash provided
675    }
676
677    #[test]
678    fn test_jwt_claims_with_query_hash() {
679        let auth = create_test_auth();
680        let query_hash = Some("test_query_hash".into());
681        let jwt = auth.generate_jwt(query_hash).unwrap();
682
683        // Decode JWT to verify structure with query hash
684        use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode};
685        use serde::Deserialize;
686
687        #[derive(Deserialize)]
688        struct Claims {
689            access_key: String,
690            nonce: String,
691            timestamp: u64,
692            query_hash: Option<String>,
693            query_hash_alg: Option<String>,
694        }
695
696        let key = DecodingKey::from_secret("test_secret_key".as_bytes());
697        let mut validation = Validation::new(Algorithm::HS256);
698        validation.validate_exp = false;
699        validation.validate_nbf = false;
700        validation.required_spec_claims.clear(); // Remove all required claims
701
702        let decoded = decode::<Claims>(&jwt, &key, &validation);
703        if let Err(ref e) = decoded {
704            panic!("JWT decode failed: {e:?}");
705        }
706
707        let claims = decoded.unwrap().claims;
708        assert_eq!(claims.access_key, "test_api_key");
709        assert!(!claims.nonce.is_empty());
710        assert!(claims.timestamp > 0);
711        assert_eq!(claims.query_hash, Some("test_query_hash".to_string()));
712        assert_eq!(claims.query_hash_alg, Some("SHA512".to_string()));
713    }
714
715    #[test]
716    fn test_smallvec_performance_advantage() {
717        // Test that SmallVec works with 8 parameters (within stack allocation)
718        let params = &[
719            ("param1", "value1"),
720            ("param2", "value2"),
721            ("param3", "value3"),
722            ("param4", "value4"),
723            ("param5", "value5"),
724            ("param6", "value6"),
725            ("param7", "value7"),
726            ("param8", "value8"),
727        ];
728
729        let result = BithumbAuth::build_param_string_optimized(params).unwrap();
730        assert!(result.contains("param1=value1"));
731        assert!(result.contains("param8=value8"));
732        assert_eq!(result.matches('&').count(), 7); // 7 separators for 8 params
733    }
734
735    #[test]
736    fn test_optimized_methods() {
737        let auth = create_test_auth();
738
739        // Test direct header generation method
740        let headers = auth.generate_headers("POST", "/trade/place", None).unwrap();
741        assert!(headers.contains_key(&SmartString::from("Authorization")));
742        assert!(headers.contains_key(&SmartString::from("Content-Type")));
743
744        // Test optimized parameter building
745        let params = &[("symbol", "BTC_KRW"), ("side", "buy")];
746        let query_string = BithumbAuth::build_query_string(params).unwrap();
747        assert!(!query_string.is_empty());
748        assert!(query_string.contains("symbol"));
749        assert!(query_string.contains("buy"));
750
751        // Test parameter count utility
752        assert_eq!(BithumbAuth::param_count(Some(params)), 2);
753        assert_eq!(BithumbAuth::param_count(None), 0);
754
755        // Test nanosecond timestamp
756        let nanos1 = BithumbAuth::generate_timestamp_nanos();
757        let nanos2 = BithumbAuth::generate_timestamp_nanos();
758        assert!(nanos2 >= nanos1);
759    }
760
761    #[test]
762    fn test_header_keys_module() {
763        // Test header key constants
764        assert_eq!(header_keys::AUTHORIZATION, "Authorization");
765        assert_eq!(header_keys::CONTENT_TYPE, "Content-Type");
766        assert_eq!(
767            header_keys::CONTENT_TYPE_VALUE,
768            "application/json; charset=utf-8"
769        );
770
771        // Test pre-allocated SmartString functions
772        let auth_header = header_keys::authorization();
773        let content_type = header_keys::content_type();
774        let content_value = header_keys::content_type_value();
775
776        assert_eq!(auth_header, "Authorization");
777        assert_eq!(content_type, "Content-Type");
778        assert_eq!(content_value, "application/json; charset=utf-8");
779    }
780
781    #[test]
782    fn test_generate_headers_consistency() {
783        let auth = create_test_auth();
784        let params = &[("symbol", "BTC_KRW"), ("amount", "100")];
785
786        // Multiple calls should produce consistent structure
787        let headers1 = auth
788            .generate_headers("POST", "/trade/place", Some(params))
789            .unwrap();
790        let headers2 = auth
791            .generate_headers("POST", "/trade/place", Some(params))
792            .unwrap();
793
794        // Should contain same keys and structure
795        assert_eq!(headers1.len(), headers2.len());
796        assert!(headers1.contains_key(&header_keys::authorization()));
797        assert!(headers1.contains_key(&header_keys::content_type()));
798        assert!(headers2.contains_key(&header_keys::authorization()));
799        assert!(headers2.contains_key(&header_keys::content_type()));
800    }
801
802    #[test]
803    fn test_clone_and_debug() {
804        let auth = create_test_auth();
805        let auth_clone = auth.clone();
806
807        assert_eq!(auth.api_key(), auth_clone.api_key());
808
809        // Test Debug implementation
810        let debug_output = format!("{auth:?}");
811        assert!(debug_output.contains("BithumbAuth"));
812    }
813
814    #[test]
815    fn test_performance_optimizations() {
816        let auth = create_test_auth();
817
818        // Test that headers are pre-allocated with exact capacity
819        let headers = auth.generate_headers("GET", "/info/balance", None).unwrap();
820        assert_eq!(headers.len(), 2); // Should have exactly 2 headers
821
822        // Test with parameters
823        let params = &[("order_id", "12345"), ("currency", "BTC")];
824        let headers_with_params = auth
825            .generate_headers("POST", "/trade/cancel", Some(params))
826            .unwrap();
827        assert_eq!(headers_with_params.len(), 2); // Should still have exactly 2 headers
828
829        // Verify header contents are optimized SmartStrings
830        let auth_value = headers.get(&header_keys::authorization()).unwrap();
831        let content_type_value = headers.get(&header_keys::content_type()).unwrap();
832
833        assert!(auth_value.starts_with("Bearer "));
834        assert_eq!(content_type_value, &header_keys::content_type_value());
835    }
836
837    #[test]
838    fn test_stack_allocation_performance() {
839        // Test parameter building with various sizes to ensure SmallVec stack allocation
840
841        // Test with 1 parameter (well within stack allocation)
842        let small_params = &[("key", "value")];
843        let small_result = BithumbAuth::build_param_string_optimized(small_params).unwrap();
844        assert_eq!(small_result, "key=value");
845
846        // Test with 8 parameters (at the edge of stack allocation)
847        let medium_params = &[
848            ("param1", "value1"),
849            ("param2", "value2"),
850            ("param3", "value3"),
851            ("param4", "value4"),
852            ("param5", "value5"),
853            ("param6", "value6"),
854            ("param7", "value7"),
855            ("param8", "value8"),
856        ];
857        let medium_result = BithumbAuth::build_param_string_optimized(medium_params).unwrap();
858        assert_eq!(medium_result.matches('&').count(), 7); // 7 separators for 8 params
859
860        // Test that empty parameters return empty string efficiently
861        let empty_params: &[(&str, &str)] = &[];
862        let empty_result = BithumbAuth::build_param_string_optimized(empty_params).unwrap();
863        assert!(empty_result.is_empty());
864    }
865
866    #[test]
867    fn test_generate_headers_for_json_body_empty() {
868        let auth = create_test_auth();
869        let headers = auth.generate_headers_for_json_body("{}").unwrap();
870
871        assert_eq!(headers.len(), 2);
872        assert!(headers.contains_key(&header_keys::authorization()));
873        assert!(headers.contains_key(&header_keys::content_type()));
874
875        let content_type = headers.get(&header_keys::content_type()).unwrap();
876        assert_eq!(content_type, &header_keys::content_type_value());
877    }
878
879    #[test]
880    fn test_generate_headers_for_json_body_with_data() {
881        let auth = create_test_auth();
882        let json_body = r#"{"symbol":"BTC_KRW","side":"buy","amount":"100"}"#;
883        let headers = auth.generate_headers_for_json_body(json_body).unwrap();
884
885        assert_eq!(headers.len(), 2);
886        assert!(headers.contains_key(&header_keys::authorization()));
887        assert!(headers.contains_key(&header_keys::content_type()));
888
889        let auth_header = headers.get(&header_keys::authorization()).unwrap();
890        assert!(auth_header.starts_with("Bearer "));
891        assert!(auth_header.len() > 10); // Should be substantial JWT token
892    }
893
894    #[test]
895    fn test_json_to_url_encoded_empty() {
896        let result = BithumbAuth::json_to_url_encoded("{}").unwrap();
897        assert!(result.is_empty());
898    }
899
900    #[test]
901    fn test_json_to_url_encoded_single_param() {
902        let json = r#"{"key":"value"}"#;
903        let result = BithumbAuth::json_to_url_encoded(json).unwrap();
904        assert_eq!(result, "key=value");
905    }
906
907    #[test]
908    fn test_json_to_url_encoded_multiple_params() {
909        let json = r#"{"symbol":"BTC_KRW","side":"buy","amount":"100"}"#;
910        let result = BithumbAuth::json_to_url_encoded(json).unwrap();
911
912        // Parameters should be sorted alphabetically
913        assert!(result.contains("amount=100"));
914        assert!(result.contains("side=buy"));
915        assert!(result.contains("symbol=BTC_KRW"));
916
917        // Should be in sorted order: amount, side, symbol
918        assert!(result.starts_with("amount=100"));
919        assert!(result.contains("side=buy"));
920        assert!(result.ends_with("symbol=BTC_KRW"));
921    }
922
923    #[test]
924    fn test_json_to_url_encoded_numeric_types() {
925        let json = r#"{"price":84000000,"volume":0.001,"is_test":true}"#;
926        let result = BithumbAuth::json_to_url_encoded(json).unwrap();
927
928        assert!(result.contains("price=84000000"));
929        assert!(result.contains("volume=0.001"));
930        assert!(result.contains("is_test=true"));
931    }
932
933    #[test]
934    fn test_json_to_url_encoded_url_encoding() {
935        let json = r#"{"message":"hello world","special":"key=value&other"}"#;
936        let result = BithumbAuth::json_to_url_encoded(json).unwrap();
937
938        // Spaces and special characters should be URL encoded
939        assert!(result.contains("hello%20world"));
940        assert!(result.contains("key%3Dvalue%26other"));
941    }
942
943    #[test]
944    fn test_json_to_url_encoded_invalid_json() {
945        let invalid_json = "{invalid json}";
946        let result = BithumbAuth::json_to_url_encoded(invalid_json);
947        assert!(result.is_err());
948    }
949
950    #[test]
951    fn test_json_to_url_encoded_non_object() {
952        let array_json = "[1, 2, 3]";
953        let result = BithumbAuth::json_to_url_encoded(array_json);
954        assert!(result.is_err());
955
956        let string_json = "\"hello\"";
957        let result = BithumbAuth::json_to_url_encoded(string_json);
958        assert!(result.is_err());
959    }
960
961    #[test]
962    fn test_json_to_url_encoded_null_values() {
963        // Test that null values are completely skipped
964        let json = r#"{
965            "symbol": "BTC_KRW",
966            "amount": 100,
967            "comment": null,
968            "side": "buy",
969            "metadata": null
970        }"#;
971        let result = BithumbAuth::json_to_url_encoded(json).unwrap();
972
973        // Should contain non-null values
974        assert!(result.contains("symbol=BTC_KRW"));
975        assert!(result.contains("amount=100"));
976        assert!(result.contains("side=buy"));
977
978        // Should NOT contain null values or their keys
979        assert!(!result.contains("comment"));
980        assert!(!result.contains("metadata"));
981        assert!(!result.contains("null"));
982
983        // Test edge case with only null values
984        let null_only_json = r#"{"field1": null, "field2": null}"#;
985        let null_result = BithumbAuth::json_to_url_encoded(null_only_json).unwrap();
986        assert_eq!(null_result, ""); // Should be empty string
987    }
988
989    #[test]
990    fn test_json_body_vs_params_consistency() {
991        let auth = create_test_auth();
992
993        // Create equivalent data as params and JSON
994        let params = &[("amount", "100"), ("side", "buy"), ("symbol", "BTC_KRW")];
995        let json_body = r#"{"symbol":"BTC_KRW","side":"buy","amount":"100"}"#;
996
997        let headers_params = auth
998            .generate_headers("POST", "/test", Some(params))
999            .unwrap();
1000        let headers_json = auth.generate_headers_for_json_body(json_body).unwrap();
1001
1002        // Both should have same structure
1003        assert_eq!(headers_params.len(), headers_json.len());
1004        assert!(headers_params.contains_key(&header_keys::authorization()));
1005        assert!(headers_json.contains_key(&header_keys::authorization()));
1006
1007        // Content-Type should be the same (JSON)
1008        let content_type_params = headers_params.get(&header_keys::content_type()).unwrap();
1009        let content_type_json = headers_json.get(&header_keys::content_type()).unwrap();
1010        assert_eq!(content_type_params, content_type_json);
1011        assert_eq!(content_type_json, &header_keys::content_type_value());
1012    }
1013
1014    #[test]
1015    fn test_json_body_authentication_flow() {
1016        let auth = create_test_auth();
1017
1018        // Test complete authentication flow for JSON body
1019        let order_data = r#"{
1020            "symbol": "BTC_KRW",
1021            "side": "buy",
1022            "order_type": "limit",
1023            "price": 84000000,
1024            "volume": 0.001
1025        }"#;
1026
1027        let headers = auth.generate_headers_for_json_body(order_data).unwrap();
1028
1029        // Verify all required headers are present
1030        assert!(headers.contains_key(&header_keys::authorization()));
1031        assert!(headers.contains_key(&header_keys::content_type()));
1032
1033        // Verify authorization header format
1034        let auth_header = headers.get(&header_keys::authorization()).unwrap();
1035        assert!(auth_header.starts_with("Bearer "));
1036
1037        // Verify JWT has proper format (3 base64 parts separated by dots)
1038        let jwt_token = auth_header.strip_prefix("Bearer ").unwrap();
1039        let parts: Vec<&str> = jwt_token.split('.').collect();
1040        assert_eq!(parts.len(), 3);
1041
1042        // Verify content type
1043        let content_type = headers.get(&header_keys::content_type()).unwrap();
1044        assert_eq!(content_type, "application/json; charset=utf-8");
1045    }
1046}