binance_sdk/common/
utils.rs

1use anyhow::{Context, Result, bail};
2use base64::{Engine as _, engine::general_purpose};
3use ed25519_dalek::Signer as Ed25519Signer;
4use ed25519_dalek::SigningKey;
5use ed25519_dalek::pkcs8::DecodePrivateKey;
6use flate2::read::GzDecoder;
7use hex;
8use hmac::{Hmac, Mac};
9use http::HeaderMap;
10use http::header::ACCEPT_ENCODING;
11use once_cell::sync::OnceCell;
12use openssl::{hash::MessageDigest, pkey::PKey, sign::Signer as OpenSslSigner};
13use rand::RngCore;
14use regex::Captures;
15use regex::Regex;
16use reqwest::Client;
17use reqwest::Proxy;
18use reqwest::{Method, Request};
19use serde::de::DeserializeOwned;
20use serde_json::{Value, json};
21use sha2::Sha256;
22use std::fmt::Display;
23use std::hash::BuildHasher;
24use std::sync::LazyLock;
25use std::{
26    collections::BTreeMap,
27    collections::HashMap,
28    fs,
29    io::Read,
30    path::Path,
31    time::Duration,
32    time::{SystemTime, UNIX_EPOCH},
33};
34use tokio::time::sleep;
35use tracing::info;
36use url::{Url, form_urlencoded::Serializer};
37
38use super::config::HttpAgent;
39use super::config::ProxyConfig;
40use super::config::{ConfigurationRestApi, PrivateKey};
41use super::errors::ConnectorError;
42use super::models::TimeUnit;
43use super::models::{Interval, RateLimitType, RestApiRateLimit, RestApiResponse};
44
45static PLACEHOLDER_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(@)?<([^>]+)>").unwrap());
46
47/// A generator for creating cryptographic signatures with support for various key types and configurations.
48///
49/// This struct manages different authentication mechanisms including API secrets, private keys,
50/// and supports multiple key formats (file-based or raw bytes). It uses lazy initialization
51/// for key loading and supports different cryptographic key types like OpenSSL private keys
52/// and Ed25519 signing keys.
53///
54/// # Fields
55/// * `api_secret`: Optional API secret for signature generation
56/// * `private_key`: Optional private key source (file or raw bytes)
57/// * `private_key_passphrase`: Optional passphrase for decrypting private keys
58/// * `raw_key_data`: Lazily initialized raw key data as a string
59/// * `key_object`: Lazily initialized OpenSSL private key
60/// * `ed25519_signing_key`: Lazily initialized Ed25519 signing key
61#[derive(Debug, Default, Clone)]
62pub struct SignatureGenerator {
63    api_secret: Option<String>,
64    private_key: Option<PrivateKey>,
65    private_key_passphrase: Option<String>,
66    raw_key_data: OnceCell<String>,
67    key_object: OnceCell<PKey<openssl::pkey::Private>>,
68    ed25519_signing_key: OnceCell<SigningKey>,
69}
70
71impl SignatureGenerator {
72    #[must_use]
73    pub fn new(
74        api_secret: Option<String>,
75        private_key: Option<PrivateKey>,
76        private_key_passphrase: Option<String>,
77    ) -> Self {
78        SignatureGenerator {
79            api_secret,
80            private_key,
81            private_key_passphrase,
82            raw_key_data: OnceCell::new(),
83            key_object: OnceCell::new(),
84            ed25519_signing_key: OnceCell::new(),
85        }
86    }
87
88    /// Retrieves the raw key data from a private key source.
89    ///
90    /// This method lazily initializes the raw key data by reading it from either a file path
91    /// or a raw byte array. If the key is from a file, it checks for file existence before reading.
92    /// If the key is provided as raw bytes, it converts them to a UTF-8 string.
93    ///
94    /// # Returns
95    /// A reference to the raw key data as a `String`.
96    ///
97    /// # Errors
98    /// Returns an error if:
99    /// - No private key is provided
100    /// - The private key file does not exist
101    /// - The private key file cannot be read
102    fn get_raw_key_data(&self) -> Result<&String> {
103        self.raw_key_data.get_or_try_init(|| {
104            let pk = self
105                .private_key
106                .as_ref()
107                .ok_or_else(|| anyhow::anyhow!("No private_key provided"))?;
108            match pk {
109                PrivateKey::File(path) => {
110                    if Path::new(path).exists() {
111                        fs::read_to_string(path)
112                            .with_context(|| format!("Failed to read private key file: {path}"))
113                    } else {
114                        Err(anyhow::anyhow!("Private key file does not exist: {}", path))
115                    }
116                }
117                PrivateKey::Raw(bytes) => Ok(String::from_utf8_lossy(bytes).to_string()),
118            }
119        })
120    }
121
122    /// Retrieves the private key object, lazily initializing it from raw key data.
123    ///
124    /// This method attempts to parse the private key from PEM format, supporting both
125    /// passphrase-protected and unprotected keys. It uses the raw key data obtained
126    /// from `get_raw_key_data()` and attempts to create an OpenSSL private key object.
127    ///
128    /// # Returns
129    /// A reference to the parsed private key as a `PKey<openssl::pkey::Private>`.
130    ///
131    /// # Errors
132    /// Returns an error if:
133    /// - The key cannot be parsed from PEM format
134    /// - A passphrase is required but incorrect
135    /// - The key data is invalid
136    fn get_key_object(&self) -> Result<&PKey<openssl::pkey::Private>> {
137        self.key_object.get_or_try_init(|| {
138            let key_data = self.get_raw_key_data()?;
139            if let Some(pass) = self.private_key_passphrase.as_ref() {
140                PKey::private_key_from_pem_passphrase(key_data.as_bytes(), pass.as_bytes())
141                    .context("Failed to parse private key with passphrase")
142            } else {
143                PKey::private_key_from_pem(key_data.as_bytes())
144                    .context("Failed to parse private key")
145            }
146        })
147    }
148
149    /// Retrieves the Ed25519 signing key, lazily initializing it from raw key data.
150    ///
151    /// This method attempts to parse an Ed25519 private key from a PEM-formatted input,
152    /// extracting the base64-encoded key material and converting it to a `SigningKey`.
153    ///
154    /// # Returns
155    /// A reference to the parsed Ed25519 signing key.
156    ///
157    /// # Errors
158    /// Returns an error if:
159    /// - The key cannot be base64 decoded
160    /// - The key cannot be parsed from PKCS8 DER format
161    fn get_ed25519_signing_key(&self) -> Result<&SigningKey> {
162        self.ed25519_signing_key.get_or_try_init(|| {
163            let key_data = self.get_raw_key_data()?;
164            let b64 = key_data
165                .lines()
166                .filter(|l| !l.starts_with("-----"))
167                .collect::<String>();
168            let der = general_purpose::STANDARD
169                .decode(b64)
170                .context("Failed to base64 decode Ed25519 PEM")?;
171            SigningKey::from_pkcs8_der(&der)
172                .map_err(|e| anyhow::anyhow!("Failed to parse Ed25519 key: {}", e))
173        })
174    }
175
176    /// Generates a signature for the given query parameters using either HMAC-SHA256 or asymmetric key signing.
177    ///
178    /// # Arguments
179    ///
180    /// * `query_params` - A map of query parameters to be signed
181    ///
182    /// # Returns
183    ///
184    /// A base64-encoded signature string
185    ///
186    /// # Errors
187    ///
188    /// Returns an error if:
189    /// - No API secret or private key is provided
190    /// - Key initialization fails
191    /// - Signing process encounters an error
192    /// - An unsupported key type is used
193    ///
194    /// # Supported Key Types
195    /// - HMAC with API secret
196    /// - RSA private key
197    /// - ED25519 private key
198    pub fn get_signature(&self, query_params: &BTreeMap<String, Value>) -> Result<String> {
199        let params = build_query_string(query_params)?;
200
201        if let Some(secret) = self.api_secret.as_ref() {
202            if self.private_key.is_none() {
203                let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
204                    .context("HMAC key initialization failed")?;
205                mac.update(params.as_bytes());
206                let result = mac.finalize().into_bytes();
207                return Ok(hex::encode(result));
208            }
209        }
210
211        if self.private_key.is_some() {
212            let key_obj = self.get_key_object()?;
213            match key_obj.id() {
214                openssl::pkey::Id::RSA => {
215                    let mut signer = OpenSslSigner::new(MessageDigest::sha256(), key_obj)
216                        .context("Failed to create RSA signer")?;
217                    signer
218                        .update(params.as_bytes())
219                        .context("Failed to update RSA signer")?;
220                    let sig = signer.sign_to_vec().context("RSA signing failed")?;
221                    return Ok(general_purpose::STANDARD.encode(sig));
222                }
223                openssl::pkey::Id::ED25519 => {
224                    let signing_key = self.get_ed25519_signing_key()?;
225                    let signature = signing_key.sign(params.as_bytes());
226                    return Ok(general_purpose::STANDARD.encode(signature.to_bytes()));
227                }
228                other => {
229                    return Err(anyhow::anyhow!(
230                        "Unsupported private key type: {:?}. Must be RSA or ED25519.",
231                        other
232                    ));
233                }
234            }
235        }
236
237        Err(anyhow::anyhow!(
238            "Either 'api_secret' or 'private_key' must be provided for signed requests."
239        ))
240    }
241}
242
243/// Builds a reqwest HTTP client with configurable timeout, keep-alive, proxy, and custom agent settings.
244///
245/// # Arguments
246///
247/// * `timeout` - Timeout duration in milliseconds for HTTP requests
248/// * `keep_alive` - Whether to enable HTTP keep-alive connections
249/// * `proxy` - Optional proxy configuration for routing requests
250/// * `agent` - Optional custom HTTP agent configuration function
251///
252/// # Returns
253///
254/// A configured `reqwest::Client` instance
255///
256/// # Panics
257///
258/// Panics if the client cannot be built with the provided configuration
259///
260/// # Examples
261///
262///
263/// let client = `build_client(5000`, true, None, None);
264///
265#[must_use]
266pub fn build_client(
267    timeout: u64,
268    keep_alive: bool,
269    proxy: Option<&ProxyConfig>,
270    agent: Option<HttpAgent>,
271) -> Client {
272    let builder = Client::builder().timeout(Duration::from_millis(timeout));
273
274    let mut builder = if keep_alive {
275        builder
276    } else {
277        builder.pool_idle_timeout(Some(Duration::from_secs(0)))
278    };
279
280    if let Some(proxy_conf) = proxy {
281        let protocol = proxy_conf
282            .protocol
283            .clone()
284            .unwrap_or_else(|| "http".to_string());
285        let proxy_url = format!("{}://{}:{}", protocol, proxy_conf.host, proxy_conf.port);
286        let mut proxy_builder = Proxy::all(&proxy_url).expect("Failed to create proxy from URL");
287        if let Some(auth) = &proxy_conf.auth {
288            proxy_builder = proxy_builder.basic_auth(&auth.username, &auth.password);
289        }
290        builder = builder.proxy(proxy_builder);
291    }
292
293    if let Some(HttpAgent(agent_fn)) = agent {
294        builder = (agent_fn)(builder);
295    }
296
297    info!("Client builder {:?}", builder);
298
299    builder.build().expect("Failed to build reqwest client")
300}
301
302/// Generates a user agent string with package name, version, Rust version, operating system, and architecture.
303///
304/// # Returns
305///
306/// * A `String` containing the formatted user agent information.
307///
308/// # Examples
309///
310///
311/// let `user_agent` = `build_user_agent()`;
312/// println!("User Agent: {}", `user_agent`);
313/// // Might output something like: "`my_package/1.0.0` (Rust/1.55.0; linux; `x86_64`)"
314///
315#[must_use]
316pub fn build_user_agent() -> String {
317    format!(
318        "{}/{} (Rust/{}; {}; {})",
319        env!("CARGO_PKG_NAME"),
320        env!("CARGO_PKG_VERSION"),
321        env!("RUSTC_VERSION"),
322        std::env::consts::OS,
323        std::env::consts::ARCH
324    )
325}
326
327/// Validates the time unit string and returns an optional normalized time unit.
328///
329/// # Arguments
330///
331/// * `time_unit` - A string representing the time unit to validate.
332///
333/// # Returns
334///
335/// * `Ok(None)` if an empty string is provided
336/// * `Ok(Some(time_unit))` if the time unit is 'MILLISECOND', 'MICROSECOND', 'millisecond', or 'microsecond'
337/// * `Err` with an error message if an invalid time unit is provided
338///
339/// # Errors
340///
341/// Returns `Err(anyhow::Error)` if `time_unit` is non-empty and not one of the allowed values.
342///
343/// # Examples
344///
345/// let result = `validate_time_unit("MILLISECOND`");
346/// `assert!(result.is_ok())`;
347///
348/// let result = `validate_time_unit`("");
349/// `assert!(result.is_ok()` && `result.unwrap().is_none()`);
350///
351/// let result = `validate_time_unit("SECOND`");
352/// `assert!(result.is_err())`;
353///
354pub fn validate_time_unit(time_unit: &str) -> Result<Option<&str>, anyhow::Error> {
355    match time_unit {
356        "" => Ok(None),
357        "MILLISECOND" | "MICROSECOND" | "millisecond" | "microsecond" => Ok(Some(time_unit)),
358        _ => Err(anyhow::anyhow!(
359            "time_unit must be either 'MILLISECOND' or 'MICROSECOND'"
360        )),
361    }
362}
363
364/// Returns the current timestamp in milliseconds since the Unix epoch.
365///
366/// # Returns
367///
368/// * A `u128` representing the current timestamp in milliseconds.
369///
370/// # Panics
371///
372/// Panics if the system time is set to a time before the Unix epoch.
373///
374/// # Examples
375///
376///
377/// let timestamp = `get_timestamp()`;
378/// println!("Current timestamp: {}", timestamp);
379///
380#[must_use]
381pub fn get_timestamp() -> u128 {
382    SystemTime::now()
383        .duration_since(UNIX_EPOCH)
384        .expect("Time went backwards")
385        .as_millis()
386}
387
388/// Asynchronously pauses the current task for a specified number of milliseconds.
389///
390/// # Arguments
391///
392/// * `ms` - The number of milliseconds to pause the task.
393///
394/// # Examples
395///
396///
397/// let _ = delay(100).await; // Pause for 100 milliseconds
398///
399pub async fn delay(ms: u64) {
400    sleep(Duration::from_millis(ms)).await;
401}
402
403/// Builds a query string from a map of key-value parameters.
404///
405/// Converts various JSON `Value` types into URL query string segments, handling:
406/// - Strings, booleans, and numbers as direct key-value pairs
407/// - Arrays of strings, booleans, or numbers as comma-separated values
408/// - Nested arrays serialized as JSON strings
409///
410/// # Arguments
411///
412/// * `params` - A map of parameter names to their corresponding JSON values
413///
414/// # Returns
415///
416/// * `Result<String, anyhow::Error>` - A query string with URL-encoded parameters, or an error
417///
418/// # Errors
419///
420/// Returns an error if an object value is encountered or JSON serialization fails
421pub fn build_query_string(params: &BTreeMap<String, Value>) -> Result<String, anyhow::Error> {
422    let mut segments = Vec::with_capacity(params.len());
423
424    for (key, value) in params {
425        match value {
426            Value::Null => {}
427            Value::String(s) => {
428                let mut ser = Serializer::new(String::new());
429                ser.append_pair(key, s);
430                segments.push(ser.finish());
431            }
432            Value::Bool(b) => {
433                let val = b.to_string();
434                let mut ser = Serializer::new(String::new());
435                ser.append_pair(key, &val);
436                segments.push(ser.finish());
437            }
438            Value::Number(n) => {
439                let val = n.to_string();
440                let mut ser = Serializer::new(String::new());
441                ser.append_pair(key, &val);
442                segments.push(ser.finish());
443            }
444            Value::Array(arr)
445                if arr
446                    .iter()
447                    .all(|v| matches!(v, Value::String(_) | Value::Bool(_) | Value::Number(_))) =>
448            {
449                let mut parts = Vec::with_capacity(arr.len());
450                for v in arr {
451                    match v {
452                        Value::String(s) => parts.push(s.clone()),
453                        Value::Bool(b) => parts.push(b.to_string()),
454                        Value::Number(n) => parts.push(n.to_string()),
455                        _ => unreachable!(),
456                    }
457                }
458                segments.push(format!("{}={}", key, parts.join(",")));
459            }
460            Value::Array(arr) => {
461                let json =
462                    serde_json::to_string(arr).context("Failed to JSON-serialize nested array")?;
463                let mut ser = Serializer::new(String::new());
464                ser.append_pair(key, &json);
465                segments.push(ser.finish());
466            }
467            Value::Object(_) => {
468                bail!("Cannot serialize object for key `{}` in query params", key);
469            }
470        }
471    }
472
473    Ok(segments.join("&"))
474}
475
476/// Determines whether a request should be retried based on:
477/// - HTTP method (only GET or DELETE are retriable)
478/// - HTTP status (500, 502, 503, 504)
479/// - Number of retries left.
480///
481/// `error` is the reqwest error, `method` is the HTTP method (e.g. "GET"),
482/// and `retries_left` is the number of remaining retries.
483#[must_use]
484pub fn should_retry_request(
485    error: &reqwest::Error,
486    method: Option<&str>,
487    retries_left: Option<usize>,
488) -> bool {
489    let method = method.unwrap_or("");
490    let is_retriable_method =
491        method.eq_ignore_ascii_case("GET") || method.eq_ignore_ascii_case("DELETE");
492
493    let status = error.status().map_or(0, |s| s.as_u16());
494    let is_retriable_status = [500, 502, 503, 504].contains(&status);
495
496    let retries_left = retries_left.unwrap_or(0);
497    retries_left > 0 && is_retriable_method && (is_retriable_status || error.status().is_none())
498}
499
500/// Parses rate limit headers from a `HashMap` of headers and returns a vector of `RestApiRateLimit`.
501///
502/// This function extracts rate limit information from headers with specific patterns (x-mbx-used-weight or x-mbx-order-count)
503/// and converts them into `RestApiRateLimit` structures. It handles different intervals (seconds, minutes, hours, days)
504/// and distinguishes between request weight and order rate limits.
505///
506/// # Arguments
507///
508/// * `headers` - A reference to a `HashMap` containing HTTP headers
509///
510/// # Returns
511///
512/// A `Vec<RestApiRateLimit>` containing parsed rate limit information
513///
514/// # Panics
515///
516/// * If the static regex fails to compile (via `Regex::new(...).unwrap()`), which can only happen if the literal pattern is invalid.  
517/// * If a matching header’s key doesn’t actually contain both capture groups (so `caps.get(2).unwrap()` or `caps.get(3).unwrap()` fails).
518///
519/// # Examples
520///
521/// let headers: `HashMap`<String, String> = // ... headers with rate limit information
522/// let `rate_limits` = `parse_rate_limit_headers(&headers)`;
523///
524#[must_use]
525pub fn parse_rate_limit_headers<S>(headers: &HashMap<String, String, S>) -> Vec<RestApiRateLimit>
526where
527    S: BuildHasher,
528{
529    let mut rate_limits = Vec::new();
530    let re = Regex::new(r"x-mbx-(used-weight|order-count)-(\d+)([smhd])").unwrap();
531    for (key, value) in headers {
532        let normalized_key = key.to_lowercase();
533        if normalized_key.starts_with("x-mbx-used-weight-")
534            || normalized_key.starts_with("x-mbx-order-count-")
535        {
536            if let Some(caps) = re.captures(&normalized_key) {
537                let interval_num: u32 = caps.get(2).unwrap().as_str().parse().unwrap_or(0);
538                let interval_letter = caps.get(3).unwrap().as_str().to_uppercase();
539                let interval = match interval_letter.as_str() {
540                    "S" => Interval::Second,
541                    "M" => Interval::Minute,
542                    "H" => Interval::Hour,
543                    "D" => Interval::Day,
544                    _ => continue,
545                };
546                let count: u32 = value.parse().unwrap_or(0);
547                let rate_limit_type = if normalized_key.starts_with("x-mbx-used-weight-") {
548                    RateLimitType::RequestWeight
549                } else {
550                    RateLimitType::Orders
551                };
552                rate_limits.push(RestApiRateLimit {
553                    rate_limit_type,
554                    interval,
555                    interval_num,
556                    count,
557                    retry_after: headers.get("retry-after").and_then(|v| v.parse().ok()),
558                });
559            }
560        }
561    }
562    rate_limits
563}
564
565/// Sends an HTTP request with retry and error handling capabilities.
566///
567/// # Parameters
568///
569/// - `req`: The HTTP request to be sent
570/// - `configuration`: REST API configuration containing client, retry settings, and other parameters
571///
572/// # Returns
573///
574/// A `Result` containing a `RestApiResponse` with deserialized data, or a `ConnectorError` if the request fails
575///
576/// # Errors
577///
578/// Returns various `ConnectorError` types based on HTTP response status, such as:
579/// - `BadRequestError`
580/// - `UnauthorizedError`
581/// - `ForbiddenError`
582/// - `NotFoundError`
583/// - `RateLimitBanError`
584/// - `TooManyRequestsError`
585/// - `ServerError`
586/// - `ConnectorClientError`
587///
588/// # Behavior
589///
590/// - Supports request retries with configurable backoff
591/// - Handles gzip-encoded responses
592/// - Parses rate limit headers
593/// - Provides detailed error handling for different HTTP status codes
594pub async fn http_request<T: DeserializeOwned + Send + 'static>(
595    req: Request,
596    configuration: &ConfigurationRestApi,
597) -> Result<RestApiResponse<T>, ConnectorError> {
598    let client = &configuration.client;
599    let retries = configuration.retries as usize;
600    let backoff = configuration.backoff;
601    let mut attempt = 0;
602
603    loop {
604        let req_clone = req
605            .try_clone()
606            .context("Failed to clone request")
607            .map_err(|e| ConnectorError::ConnectorClientError(e.to_string()))?;
608        match client.execute(req_clone).await {
609            Ok(response) => {
610                let status = response.status();
611                let headers_map: HashMap<String, String> = response
612                    .headers()
613                    .iter()
614                    .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
615                    .collect();
616
617                let raw_bytes = match response.bytes().await {
618                    Ok(b) => b,
619                    Err(e) => {
620                        attempt += 1;
621                        if attempt <= retries {
622                            continue;
623                        }
624                        return Err(ConnectorError::ConnectorClientError(format!(
625                            "Failed to get response bytes: {e}"
626                        )));
627                    }
628                };
629
630                let content = if headers_map
631                    .get("content-encoding")
632                    .is_some_and(|enc| enc.to_lowercase().contains("gzip"))
633                {
634                    let mut decoder = GzDecoder::new(&raw_bytes[..]);
635                    let mut decompressed = String::new();
636                    decoder
637                        .read_to_string(&mut decompressed)
638                        .context("Failed to decompress gzip response")
639                        .map_err(|e| ConnectorError::ConnectorClientError(e.to_string()))?;
640                    decompressed
641                } else {
642                    String::from_utf8(raw_bytes.to_vec())
643                        .context("Failed to convert response to UTF-8")
644                        .map_err(|e| ConnectorError::ConnectorClientError(e.to_string()))?
645                };
646
647                let rate_limits = parse_rate_limit_headers(&headers_map);
648
649                if status.is_client_error() || status.is_server_error() {
650                    let error_msg = serde_json::from_str::<serde_json::Value>(&content)
651                        .ok()
652                        .and_then(|v| {
653                            v.get("msg")
654                                .and_then(|m| m.as_str())
655                                .map(std::string::ToString::to_string)
656                        })
657                        .unwrap_or_else(|| content.clone());
658
659                    match status.as_u16() {
660                        400 => return Err(ConnectorError::BadRequestError(error_msg)),
661                        401 => return Err(ConnectorError::UnauthorizedError(error_msg)),
662                        403 => return Err(ConnectorError::ForbiddenError(error_msg)),
663                        404 => return Err(ConnectorError::NotFoundError(error_msg)),
664                        418 => return Err(ConnectorError::RateLimitBanError(error_msg)),
665                        429 => return Err(ConnectorError::TooManyRequestsError(error_msg)),
666                        s if (500..600).contains(&s) => {
667                            return Err(ConnectorError::ServerError {
668                                msg: format!("Server error: {s}"),
669                                status_code: Some(s),
670                            });
671                        }
672                        _ => return Err(ConnectorError::ConnectorClientError(error_msg)),
673                    }
674                }
675
676                let raw = content.clone();
677                return Ok(RestApiResponse {
678                    data_fn: Box::new(move || {
679                        Box::pin(async move {
680                            let parsed: T = serde_json::from_str(&raw)
681                                .map_err(|e| ConnectorError::ConnectorClientError(e.to_string()))?;
682                            Ok(parsed)
683                        })
684                    }),
685                    status: status.as_u16(),
686                    headers: headers_map,
687                    rate_limits: if rate_limits.is_empty() {
688                        None
689                    } else {
690                        Some(rate_limits)
691                    },
692                });
693            }
694            Err(e) => {
695                attempt += 1;
696                if should_retry_request(&e, Some(req.method().as_str()), Some(retries - attempt)) {
697                    delay(backoff * attempt as u64).await;
698                    continue;
699                }
700                return Err(ConnectorError::ConnectorClientError(format!(
701                    "HTTP request failed: {e}"
702                )));
703            }
704        }
705    }
706}
707
708/// Sends an HTTP request to a REST API endpoint with optional authentication and configuration.
709///
710/// # Parameters
711///
712/// - `configuration`: REST API configuration containing client, base path, and authentication details
713/// - `endpoint`: The specific API endpoint path to send the request to
714/// - `method`: HTTP method for the request (GET, POST, etc.)
715/// - `params`: Parameters to be sent with the request, as a key-value map
716/// - `time_unit`: Optional time unit for the request header
717/// - `is_signed`: Optional flag to indicate whether the request requires authentication
718///
719/// # Returns
720///
721/// A `RestApiResponse` containing the deserialized response data, or an error if the request fails
722///
723/// # Panics
724///
725/// This function will panic if any of the following `.unwrap()` calls fail:
726/// - Parsing the literal `"application/json"` into a header value (should never fail)  
727/// - Parsing `configuration.user_agent` or `configuration.api_key` into header values  
728/// - Parsing the literal `"gzip, deflate, br"` into a header value when `compression` is enabled  
729///
730/// # Errors
731///
732/// Returns an `anyhow::Result` which can contain various connector-related errors during request processing
733pub async fn send_request<T: DeserializeOwned + Send + 'static>(
734    configuration: &ConfigurationRestApi,
735    endpoint: &str,
736    method: Method,
737    mut params: BTreeMap<String, Value>,
738    time_unit: Option<TimeUnit>,
739    is_signed: bool,
740) -> anyhow::Result<RestApiResponse<T>> {
741    let base = configuration.base_path.as_deref().unwrap_or("");
742    let full_url = reqwest::Url::parse(base)
743        .and_then(|u| u.join(endpoint))
744        .context("Failed to join base URL and endpoint")?
745        .to_string();
746
747    if is_signed {
748        let timestamp = get_timestamp();
749        params.insert("timestamp".to_string(), json!(timestamp));
750        let signature = configuration.signature_gen.get_signature(&params)?;
751        params.insert("signature".to_string(), Value::String(signature));
752    }
753
754    let mut url = Url::parse(&full_url)?;
755    {
756        let mut pairs = url.query_pairs_mut();
757        for (key, value) in &params {
758            let val_str = match value {
759                Value::String(s) => s.clone(),
760                _ => value.to_string(),
761            };
762            pairs.append_pair(key, &val_str);
763        }
764    }
765
766    let mut headers = HeaderMap::new();
767    headers.insert("Content-Type", "application/json".parse().unwrap());
768    headers.insert("User-Agent", configuration.user_agent.parse().unwrap());
769    if let Some(api_key) = &configuration.api_key {
770        headers.insert("X-MBX-APIKEY", api_key.parse().unwrap());
771    }
772
773    if configuration.compression {
774        headers.insert(ACCEPT_ENCODING, "gzip, deflate, br".parse().unwrap());
775    }
776
777    let time_unit_to_apply = time_unit.or(configuration.time_unit);
778    if let Some(time_unit) = time_unit_to_apply {
779        headers.insert("X-MBX-TIME-UNIT", time_unit.as_upper_str().parse()?);
780    }
781
782    let req_builder = configuration.client.request(method, url).headers(headers);
783    let req = req_builder.build()?;
784
785    Ok(http_request::<T>(req, configuration).await?)
786}
787
788/// Generates a random hexadecimal string of 32 characters.
789///
790/// Uses the thread-local random number generator to fill a 16-byte buffer,
791/// which is then encoded into a hexadecimal string.
792///
793/// # Returns
794///
795/// A randomly generated 32-character hexadecimal string.
796#[must_use]
797pub fn random_string() -> String {
798    let mut buf = [0u8; 16];
799    rand::thread_rng().fill_bytes(&mut buf);
800    hex::encode(buf)
801}
802
803/// Removes entries with empty or null values from an iterator of key-value pairs.
804///
805/// # Arguments
806///
807/// * `entries` - An iterator of key-value pairs where keys are strings and values are of type `Value`.
808///
809/// # Returns
810///
811/// A `BTreeMap` containing only the key-value pairs where the value is neither `null` nor an empty string.
812///
813/// # Examples
814///
815///
816/// let entries = vec![
817///     ("`key1".to_string()`, `Value::String("value1".to_string())`),
818///     ("`key2".to_string()`, `Value::Null`),
819///     ("`key3".to_string()`, `Value::String("".to_string())`),
820/// ];
821/// let filtered = `remove_empty_value(entries)`;
822/// // filtered will only contain the first key-value pair
823///
824pub fn remove_empty_value<I>(entries: I) -> BTreeMap<String, Value>
825where
826    I: IntoIterator<Item = (String, Value)>,
827{
828    entries
829        .into_iter()
830        .filter(|(_, value)| match value {
831            Value::Null => false,
832            Value::String(s) if s.is_empty() => false,
833            _ => true,
834        })
835        .collect()
836}
837
838/// Creates a sorted copy of a `BTreeMap` of parameters.
839///
840/// # Arguments
841///
842/// * `params` - A reference to a `BTreeMap` containing string keys and Value values.
843///
844/// # Returns
845///
846/// A new `BTreeMap` with the same key-value pairs as the input, sorted by keys.
847///
848/// # Examples
849///
850///
851/// let params = `BTreeMap::from`([
852///     ("`z".to_string()`, `Value::String("value1".to_string())`),
853///     ("`a".to_string()`, `Value::String("value2".to_string())`),
854/// ]);
855/// let `sorted_params` = `sort_object_params(&params)`;
856/// // `sorted_params` will have keys sorted in ascending order
857///
858#[must_use]
859pub fn sort_object_params(params: &BTreeMap<String, Value>) -> BTreeMap<String, Value> {
860    let mut sorted = BTreeMap::new();
861    for (k, v) in params {
862        sorted.insert(k.clone(), v.clone());
863    }
864    sorted
865}
866
867/// Normalizes a WebSocket streams key by converting it to lowercase and removing underscores and hyphens.
868///
869/// # Arguments
870///
871/// * `key` - The input key to be normalized
872///
873/// # Returns
874///
875/// A normalized string with lowercase characters and no underscores or hyphens
876fn normalize_ws_streams_key(key: &str) -> String {
877    key.to_lowercase().replace(&['_', '-'][..], "")
878}
879
880/// Replaces placeholders in a WebSocket stream key with corresponding values from a variables map.
881///
882/// # Arguments
883///
884/// * `input` - The input string containing placeholders to be replaced
885/// * `variables` - A `HashMap` of key-value pairs used for placeholder substitution
886///
887/// # Returns
888///
889/// A modified string with placeholders replaced by their corresponding values,
890/// with special handling for normalization, lowercasing, and '@' symbol stripping.
891///
892/// # Panics
893///
894/// Panics if the input string contains an invalid placeholder format.
895///
896/// # Examples
897///
898///
899/// let input = "/<symbol>@ticker";
900/// let variables = `HashMap::from`([("symbol", "BTCUSDT")]);
901/// let result = `replace_websocket_streams_placeholders(input`, &variables);
902/// // Possible result: "btcusdt@ticker"
903///
904pub fn replace_websocket_streams_placeholders<V, S>(
905    input: &str,
906    variables: &HashMap<&str, V, S>,
907) -> String
908where
909    V: Display,
910    S: BuildHasher,
911{
912    let original = input;
913
914    // Drop a leading slash for processing
915    let body = original.strip_prefix('/').unwrap_or(original);
916
917    // Normalize variables into String→String map
918    let normalized: HashMap<String, String> = variables
919        .iter()
920        .map(|(k, v)| (normalize_ws_streams_key(k), v.to_string()))
921        .collect();
922
923    // Replace all placeholders, preserving any '@' prefix captured by the regex
924    let replaced = PLACEHOLDER_RE
925        .replace_all(body, |caps: &Captures| {
926            let prefix = caps.get(1).map_or("", |m| m.as_str());
927            let key = normalize_ws_streams_key(caps.get(2).unwrap().as_str());
928            let val = normalized.get(&key).cloned().unwrap_or_default();
929            format!("{prefix}{val}")
930        })
931        .into_owned();
932
933    // Strip any trailing '@'
934    let stripped = replaced.trim_end_matches('@').to_string();
935
936    // Only lowercase head if original started with '/' and first placeholder at start
937    // (cases where `symbol` or `pair` are used and they are not lower-cased)
938    let should_lower_head =
939        original.starts_with('/') && PLACEHOLDER_RE.find(body).is_some_and(|m| m.start() == 0);
940
941    // Lowercase only that first placeholder's value
942    let result = if should_lower_head {
943        if let Some(caps) = PLACEHOLDER_RE.captures(body) {
944            let key = normalize_ws_streams_key(caps.get(2).unwrap().as_str());
945            let first_val = normalized.get(&key).cloned().unwrap_or_default();
946            if stripped.starts_with(&first_val) {
947                let tail = &stripped[first_val.len()..];
948                format!("{}{}", first_val.to_lowercase(), tail)
949            } else {
950                stripped.clone()
951            }
952        } else {
953            stripped.clone()
954        }
955    } else {
956        stripped.clone()
957    };
958
959    result
960}
961
962#[cfg(test)]
963mod tests {
964    use crate::TOKIO_SHARED_RT;
965
966    mod build_client {
967        use std::{
968            sync::{Arc, Mutex},
969            time::{Duration, Instant},
970        };
971
972        use reqwest::ClientBuilder;
973
974        use crate::{
975            common::utils::build_client,
976            config::{HttpAgent, ProxyAuth, ProxyConfig},
977        };
978
979        use super::TOKIO_SHARED_RT;
980
981        #[test]
982        fn enforces_timeout() {
983            TOKIO_SHARED_RT.block_on(async {
984                let client = build_client(100, true, None, None);
985                let start = Instant::now();
986                let res = client.get("http://10.255.255.1").send().await;
987                assert!(
988                    res.is_err(),
989                    "expected an error (timeout or connect) but got {res:?}"
990                );
991                let elapsed = start.elapsed();
992                assert!(
993                    elapsed < Duration::from_millis(500),
994                    "timed out too slowly: {elapsed:?}"
995                );
996            });
997        }
998
999        #[test]
1000        fn builds_with_keep_alive_disabled() {
1001            let client = build_client(200, false, None, None);
1002            let _: reqwest::Client = client;
1003        }
1004
1005        #[test]
1006        #[should_panic(expected = "Failed to create proxy from URL")]
1007        fn invalid_proxy_url_panics() {
1008            let bad_proxy = ProxyConfig {
1009                protocol: Some("http".to_string()),
1010                host: String::new(),
1011                port: 8080,
1012                auth: None,
1013            };
1014            let _ = build_client(1_000, true, Some(&bad_proxy), None);
1015        }
1016
1017        #[test]
1018        fn builds_with_proxy_and_auth() {
1019            let proxy = ProxyConfig {
1020                protocol: Some("https".to_string()),
1021                host: "127.0.0.1".to_string(),
1022                port: 3128,
1023                auth: Some(ProxyAuth {
1024                    username: "alice".to_string(),
1025                    password: "secret".to_string(),
1026                }),
1027            };
1028            let client = build_client(2_000, true, Some(&proxy), None);
1029            let _: reqwest::Client = client;
1030        }
1031
1032        #[test]
1033        fn custom_agent_invoked() {
1034            let called = Arc::new(Mutex::new(false));
1035            let called_clone = Arc::clone(&called);
1036
1037            let agent = HttpAgent(Arc::new(move |builder: ClientBuilder| {
1038                *called_clone.lock().unwrap() = true;
1039                builder
1040            }));
1041
1042            let client = build_client(1_000, true, None, Some(agent));
1043            assert!(*called.lock().unwrap(), "agent closure wasn’t invoked");
1044            let _: reqwest::Client = client;
1045        }
1046    }
1047
1048    mod build_user_agent {
1049        use crate::common::utils::build_user_agent;
1050
1051        #[test]
1052        fn build_user_agent_contains_crate_and_rust_info() {
1053            let ua = build_user_agent();
1054            let name = env!("CARGO_PKG_NAME");
1055            let version = env!("CARGO_PKG_VERSION");
1056            let rustc = env!("RUSTC_VERSION");
1057            let os = std::env::consts::OS;
1058            let arch = std::env::consts::ARCH;
1059
1060            let expected_prefix = format!("{name}/{version} (Rust/");
1061            assert!(ua.starts_with(&expected_prefix), "prefix mismatch: {ua}");
1062
1063            assert!(ua.contains(rustc), "user agent missing RUSTC_VERSION: {ua}");
1064
1065            let expected_os = format!("; {os}");
1066            let expected_arch = format!("; {arch}");
1067            assert!(ua.contains(&expected_os), "user agent missing OS: {ua}");
1068            assert!(ua.contains(&expected_arch), "user agent missing ARCH: {ua}");
1069
1070            assert!(ua.ends_with(')'), "missing trailing ')': {ua}");
1071        }
1072
1073        #[test]
1074        fn build_user_agent_is_deterministic() {
1075            let ua1 = build_user_agent();
1076            let ua2 = build_user_agent();
1077            assert_eq!(ua1, ua2, "user agent should be the same on repeated calls");
1078        }
1079    }
1080
1081    mod validate_time_unit {
1082        use crate::common::utils::validate_time_unit;
1083
1084        #[test]
1085        fn empty_string_returns_none() {
1086            let res = validate_time_unit("").expect("Should not error on empty string");
1087            assert_eq!(res, None);
1088        }
1089
1090        #[test]
1091        fn uppercase_millisecond() {
1092            let res = validate_time_unit("MILLISECOND").expect("Should accept MILLISECOND");
1093            assert_eq!(res, Some("MILLISECOND"));
1094        }
1095
1096        #[test]
1097        fn uppercase_microsecond() {
1098            let res = validate_time_unit("MICROSECOND").expect("Should accept MICROSECOND");
1099            assert_eq!(res, Some("MICROSECOND"));
1100        }
1101
1102        #[test]
1103        fn lowercase_millisecond() {
1104            let res = validate_time_unit("millisecond").expect("Should accept millisecond");
1105            assert_eq!(res, Some("millisecond"));
1106        }
1107
1108        #[test]
1109        fn lowercase_microsecond() {
1110            let res = validate_time_unit("microsecond").expect("Should accept microsecond");
1111            assert_eq!(res, Some("microsecond"));
1112        }
1113
1114        #[test]
1115        fn invalid_value_returns_err() {
1116            let err = validate_time_unit("SECOND").unwrap_err();
1117            let msg = format!("{err}");
1118            assert!(msg.contains("time_unit must be either 'MILLISECOND' or 'MICROSECOND'"));
1119        }
1120
1121        #[test]
1122        fn partial_match_returns_err() {
1123            let err = validate_time_unit("MILLI").unwrap_err();
1124            let msg = format!("{err}");
1125            assert!(msg.contains("time_unit must be either 'MILLISECOND' or 'MICROSECOND'"));
1126        }
1127    }
1128
1129    mod get_timestamp {
1130        use crate::common::utils::get_timestamp;
1131        use std::{
1132            thread::sleep,
1133            time::{Duration, SystemTime, UNIX_EPOCH},
1134        };
1135
1136        #[test]
1137        fn timestamp_is_within_system_time_bounds() {
1138            let before = SystemTime::now()
1139                .duration_since(UNIX_EPOCH)
1140                .expect("SystemTime before UNIX_EPOCH")
1141                .as_millis();
1142            let ts = get_timestamp();
1143            let after = SystemTime::now()
1144                .duration_since(UNIX_EPOCH)
1145                .expect("SystemTime before UNIX_EPOCH")
1146                .as_millis();
1147
1148            assert!(
1149                ts >= before,
1150                "timestamp {ts} is before captured before time {before}"
1151            );
1152            assert!(
1153                ts <= after,
1154                "timestamp {ts} is after captured after time {after}"
1155            );
1156        }
1157
1158        #[test]
1159        fn timestamps_are_monotonic() {
1160            let t1 = get_timestamp();
1161            sleep(Duration::from_millis(1));
1162            let t2 = get_timestamp();
1163            assert!(
1164                t2 >= t1,
1165                "second timestamp {t2} is not >= first timestamp {t1}"
1166            );
1167        }
1168    }
1169
1170    mod build_query_string {
1171        use std::collections::BTreeMap;
1172
1173        use anyhow::Result;
1174        use serde_json::{Value, json};
1175        use url::form_urlencoded::Serializer;
1176
1177        use crate::common::utils::build_query_string;
1178
1179        fn mk_map(pairs: Vec<(&str, Value)>) -> BTreeMap<String, Value> {
1180            let mut m = BTreeMap::new();
1181            for (k, v) in pairs {
1182                m.insert(k.to_string(), v);
1183            }
1184            m
1185        }
1186
1187        #[test]
1188        fn empty_map_returns_empty_string() -> Result<()> {
1189            let params = BTreeMap::new();
1190            let qs = build_query_string(&params)?;
1191            assert_eq!(qs, "");
1192            Ok(())
1193        }
1194
1195        #[test]
1196        fn string_and_number() -> Result<()> {
1197            let params = mk_map(vec![("foo", json!("bar")), ("num", json!(42))]);
1198            let qs = build_query_string(&params)?;
1199            assert_eq!(qs, "foo=bar&num=42");
1200            Ok(())
1201        }
1202
1203        #[test]
1204        fn bool_and_null_skipped() -> Result<()> {
1205            let params = mk_map(vec![("a", json!(true)), ("b", Value::Null)]);
1206            let qs = build_query_string(&params)?;
1207            assert_eq!(qs, "a=true");
1208            Ok(())
1209        }
1210
1211        #[test]
1212        fn flat_array() -> Result<()> {
1213            let params = mk_map(vec![("list", json!(vec!["x", "y", "z"]))]);
1214            let qs = build_query_string(&params)?;
1215            assert_eq!(qs, "list=x,y,z");
1216            Ok(())
1217        }
1218
1219        #[test]
1220        fn nested_array_json_encoded() -> Result<()> {
1221            let params = mk_map(vec![("nested", json!([[1, 2], [3, 4]]))]);
1222            let qs = build_query_string(&params)?;
1223
1224            let nested_json = serde_json::to_string(&json!([[1, 2], [3, 4]]))?;
1225            let mut ser = Serializer::new(String::new());
1226            ser.append_pair("nested", &nested_json);
1227            let expected = ser.finish();
1228
1229            assert_eq!(qs, expected);
1230            Ok(())
1231        }
1232
1233        #[test]
1234        fn object_not_supported() {
1235            let params = mk_map(vec![("obj", json!({"k":1}))]);
1236            let err = build_query_string(&params).unwrap_err();
1237            let msg = format!("{err}");
1238            assert!(msg.contains("Cannot serialize object for key `obj`"));
1239        }
1240    }
1241
1242    mod signature_generator {
1243        use base64::{Engine, engine::general_purpose};
1244        use ed25519_dalek::{SigningKey, ed25519::signature::SignerMut, pkcs8::DecodePrivateKey};
1245        use hex;
1246        use hmac::{Hmac, Mac};
1247        use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa, sign::Verifier};
1248        use serde_json::Value;
1249        use sha2::Sha256;
1250        use std::collections::BTreeMap;
1251        use std::io::Write;
1252        use tempfile::NamedTempFile;
1253
1254        use crate::{common::utils::SignatureGenerator, config::PrivateKey};
1255
1256        #[test]
1257        fn hmac_sha256_signature() {
1258            let mut params = BTreeMap::new();
1259            params.insert("b".into(), Value::Number(2.into()));
1260            params.insert("a".into(), Value::Number(1.into()));
1261
1262            let signature_gen = SignatureGenerator::new(Some("test-secret".into()), None, None);
1263            let sig = signature_gen
1264                .get_signature(&params)
1265                .expect("HMAC signing failed");
1266
1267            let mut mac = Hmac::<Sha256>::new_from_slice(b"test-secret").unwrap();
1268            let qs = "a=1&b=2";
1269            mac.update(qs.as_bytes());
1270            let expected = hex::encode(mac.finalize().into_bytes());
1271
1272            assert_eq!(sig, expected);
1273        }
1274
1275        #[test]
1276        fn repeated_hmac_signature() {
1277            let mut params = BTreeMap::new();
1278            params.insert("x".into(), Value::String("y".into()));
1279            let signature_gen = SignatureGenerator::new(Some("abc".into()), None, None);
1280            let s1 = signature_gen.get_signature(&params).unwrap();
1281            let s2 = signature_gen.get_signature(&params).unwrap();
1282            assert_eq!(s1, s2);
1283        }
1284
1285        #[test]
1286        fn rsa_signature_verification() {
1287            let mut params = BTreeMap::new();
1288            params.insert("a".into(), Value::Number(1.into()));
1289            params.insert("b".into(), Value::Number(2.into()));
1290
1291            let rsa = Rsa::generate(2048).unwrap();
1292            let priv_pem = rsa.private_key_to_pem().unwrap();
1293            let pub_pem = rsa.public_key_to_pem_pkcs1().unwrap();
1294
1295            let signature_gen =
1296                SignatureGenerator::new(None, Some(PrivateKey::Raw(priv_pem.clone())), None);
1297            let sig = signature_gen
1298                .get_signature(&params)
1299                .expect("RSA signing failed");
1300
1301            let sig_bytes = general_purpose::STANDARD.decode(&sig).unwrap();
1302            let pubkey = PKey::public_key_from_pem(&pub_pem).unwrap();
1303            let mut verifier = Verifier::new(MessageDigest::sha256(), &pubkey).unwrap();
1304            verifier.update(b"a=1&b=2").unwrap();
1305            assert!(verifier.verify(&sig_bytes).unwrap());
1306        }
1307
1308        #[test]
1309        fn repeated_rsa_signature() {
1310            let mut params = BTreeMap::new();
1311            params.insert("k".into(), Value::Number(5.into()));
1312            let rsa = Rsa::generate(2048).unwrap();
1313            let priv_pem = rsa.private_key_to_pem().unwrap();
1314            let signature_gen =
1315                SignatureGenerator::new(None, Some(PrivateKey::Raw(priv_pem)), None);
1316            let s1 = signature_gen.get_signature(&params).unwrap();
1317            let s2 = signature_gen.get_signature(&params).unwrap();
1318            assert_eq!(s1, s2);
1319        }
1320
1321        #[test]
1322        fn ed25519_signature_verification() {
1323            let mut params = BTreeMap::new();
1324            params.insert("a".into(), Value::Number(1.into()));
1325            params.insert("b".into(), Value::Number(2.into()));
1326            let qs = "a=1&b=2";
1327
1328            let ed = PKey::generate_ed25519().unwrap();
1329            let priv_pem = ed.private_key_to_pem_pkcs8().unwrap();
1330
1331            let signature_gen =
1332                SignatureGenerator::new(None, Some(PrivateKey::Raw(priv_pem.clone())), None);
1333            let sig = signature_gen
1334                .get_signature(&params)
1335                .expect("Ed25519 signing failed");
1336
1337            let pem_str = String::from_utf8(priv_pem).unwrap();
1338            let b64 = pem_str
1339                .lines()
1340                .filter(|l| !l.starts_with("-----"))
1341                .collect::<String>();
1342            let der = general_purpose::STANDARD.decode(b64).unwrap();
1343            let mut sk = SigningKey::from_pkcs8_der(&der).unwrap();
1344            let expected_bytes = sk.sign(qs.as_bytes()).to_bytes();
1345            let expected_sig = general_purpose::STANDARD.encode(expected_bytes);
1346            assert_eq!(sig, expected_sig);
1347        }
1348
1349        #[test]
1350        fn repeated_ed25519_signature() {
1351            let mut params = BTreeMap::new();
1352            params.insert("m".into(), Value::String("n".into()));
1353            let ed = PKey::generate_ed25519().unwrap();
1354            let priv_pem = ed.private_key_to_pem_pkcs8().unwrap();
1355            let signature_gen =
1356                SignatureGenerator::new(None, Some(PrivateKey::Raw(priv_pem.clone())), None);
1357            let s1 = signature_gen.get_signature(&params).unwrap();
1358            let s2 = signature_gen.get_signature(&params).unwrap();
1359            assert_eq!(s1, s2);
1360        }
1361
1362        #[test]
1363        fn file_based_key() {
1364            let rsa = Rsa::generate(1024).unwrap();
1365            let priv_pem = rsa.private_key_to_pem().unwrap();
1366            let pub_pem = rsa.public_key_to_pem_pkcs1().unwrap();
1367
1368            let mut file = NamedTempFile::new().unwrap();
1369            file.write_all(&priv_pem).unwrap();
1370            let path = file.path().to_str().unwrap().to_string();
1371
1372            let mut params = BTreeMap::new();
1373            params.insert("z".into(), Value::Number(9.into()));
1374
1375            let signature_gen = SignatureGenerator::new(None, Some(PrivateKey::File(path)), None);
1376            let sig = signature_gen.get_signature(&params).unwrap();
1377
1378            let sig_bytes = general_purpose::STANDARD.decode(&sig).unwrap();
1379            let pubkey = PKey::public_key_from_pem(&pub_pem).unwrap();
1380            let mut verifier = Verifier::new(MessageDigest::sha256(), &pubkey).unwrap();
1381            verifier.update(b"z=9").unwrap();
1382            assert!(verifier.verify(&sig_bytes).unwrap());
1383        }
1384
1385        #[test]
1386        fn unsupported_key_type_error() {
1387            let mut params = BTreeMap::new();
1388            params.insert("x".into(), Value::String("y".into()));
1389
1390            let group =
1391                openssl::ec::EcGroup::from_curve_name(openssl::nid::Nid::X9_62_PRIME256V1).unwrap();
1392            let ec_key = openssl::ec::EcKey::generate(&group).unwrap();
1393            let pkey_ec = PKey::from_ec_key(ec_key).unwrap();
1394            let raw = pkey_ec.private_key_to_pem_pkcs8().unwrap();
1395
1396            let signature_gen = SignatureGenerator::new(None, Some(PrivateKey::Raw(raw)), None);
1397            let err = signature_gen
1398                .get_signature(&params)
1399                .unwrap_err()
1400                .to_string();
1401            assert!(err.contains("Unsupported private key type"));
1402        }
1403
1404        #[test]
1405        fn invalid_private_key_error() {
1406            let mut params = BTreeMap::new();
1407            params.insert("foo".into(), Value::String("bar".into()));
1408
1409            let signature_gen =
1410                SignatureGenerator::new(None, Some(PrivateKey::Raw(b"not a key".to_vec())), None);
1411            let err = signature_gen
1412                .get_signature(&params)
1413                .unwrap_err()
1414                .to_string();
1415            assert!(err.contains("Failed to parse private key"));
1416        }
1417
1418        #[test]
1419        fn missing_credentials_error() {
1420            let mut params = BTreeMap::new();
1421            params.insert("a".into(), Value::Number(1.into()));
1422
1423            let signature_gen = SignatureGenerator::new(None, None, None);
1424            let err = signature_gen
1425                .get_signature(&params)
1426                .unwrap_err()
1427                .to_string();
1428            assert!(err.contains("Either 'api_secret' or 'private_key' must be provided"));
1429        }
1430    }
1431
1432    mod should_retry_request {
1433        use crate::common::utils::should_retry_request;
1434
1435        use reqwest::{Error, Response};
1436
1437        fn mk_http_error(code: u16) -> Error {
1438            let resp = Response::from(
1439                http::response::Response::builder()
1440                    .status(code)
1441                    .body("")
1442                    .unwrap(),
1443            );
1444            resp.error_for_status().unwrap_err()
1445        }
1446
1447        fn mk_network_error() -> Error {
1448            reqwest::blocking::get("http://256.256.256.256").unwrap_err()
1449        }
1450
1451        #[test]
1452        fn retry_on_retriable_status_and_method() {
1453            let err = mk_http_error(500);
1454            assert!(should_retry_request(&err, Some("GET"), Some(1)));
1455            assert!(should_retry_request(&err, Some("delete"), Some(2)));
1456        }
1457
1458        #[test]
1459        fn retry_when_status_none_and_retriable_method() {
1460            let retriable_methods = ["GET", "DELETE"];
1461
1462            for &method in &retriable_methods {
1463                let err = mk_network_error();
1464                assert!(
1465                    should_retry_request(&err, Some(method), Some(1)),
1466                    "Should retry when no status and method {method}"
1467                );
1468            }
1469        }
1470
1471        #[test]
1472        fn no_retry_when_no_retries_left() {
1473            let err = mk_http_error(503);
1474            assert!(!should_retry_request(&err, Some("GET"), Some(0)));
1475        }
1476
1477        #[test]
1478        fn no_retry_on_non_retriable_status() {
1479            let non_retriable_statuses = [400, 401, 404, 422];
1480
1481            for &status in &non_retriable_statuses {
1482                let err = mk_http_error(status);
1483                assert!(
1484                    !should_retry_request(&err, Some("GET"), Some(2)),
1485                    "Should not retry for non-retriable status {status}"
1486                );
1487            }
1488        }
1489
1490        #[test]
1491        fn no_retry_on_non_retriable_method() {
1492            let non_retriable_methods = ["POST", "PUT", "PATCH"];
1493
1494            for &method in &non_retriable_methods {
1495                let err = mk_http_error(500);
1496                assert!(
1497                    !should_retry_request(&err, Some(method), Some(2)),
1498                    "Should not retry for non-retriable method {method}"
1499                );
1500            }
1501        }
1502
1503        #[test]
1504        fn no_retry_when_status_none_and_non_retriable_method() {
1505            let non_retriable_methods = ["POST", "PUT"];
1506
1507            for &method in &non_retriable_methods {
1508                let err = mk_network_error();
1509                assert!(
1510                    !should_retry_request(&err, Some(method), Some(1)),
1511                    "Should not retry when no status and method {method}"
1512                );
1513            }
1514        }
1515    }
1516
1517    mod parse_rate_limit_headers_tests {
1518        use crate::common::{
1519            models::{Interval, RateLimitType},
1520            utils::parse_rate_limit_headers,
1521        };
1522        use std::collections::HashMap;
1523
1524        fn mk_headers(pairs: Vec<(&str, &str)>) -> HashMap<String, String> {
1525            let mut m = HashMap::new();
1526            for (k, v) in pairs {
1527                m.insert(k.to_string(), v.to_string());
1528            }
1529            m
1530        }
1531
1532        #[test]
1533        fn single_weight_header() {
1534            let headers = mk_headers(vec![("x-mbx-used-weight-1s", "123")]);
1535            let limits = parse_rate_limit_headers(&headers);
1536            assert_eq!(limits.len(), 1);
1537            let rl = &limits[0];
1538            assert_eq!(rl.rate_limit_type, RateLimitType::RequestWeight);
1539            assert_eq!(rl.interval, Interval::Second);
1540            assert_eq!(rl.interval_num, 1);
1541            assert_eq!(rl.count, 123);
1542            assert_eq!(rl.retry_after, None);
1543        }
1544
1545        #[test]
1546        fn single_order_count_with_retry_after() {
1547            let headers = mk_headers(vec![("x-mbx-order-count-5m", "42"), ("retry-after", "7")]);
1548            let limits = parse_rate_limit_headers(&headers);
1549            assert_eq!(limits.len(), 1);
1550            let rl = &limits[0];
1551            assert_eq!(rl.rate_limit_type, RateLimitType::Orders);
1552            assert_eq!(rl.interval, Interval::Minute);
1553            assert_eq!(rl.interval_num, 5);
1554            assert_eq!(rl.count, 42);
1555            assert_eq!(rl.retry_after, Some(7));
1556        }
1557
1558        #[test]
1559        fn multiple_headers() {
1560            let headers = mk_headers(vec![
1561                ("X-MBX-USED-WEIGHT-1h", "10"),
1562                ("x-mbx-order-count-2d", "20"),
1563            ]);
1564            let mut limits = parse_rate_limit_headers(&headers);
1565            limits.sort_by_key(|r| (r.interval_num, format!("{:?}", r.rate_limit_type)));
1566            assert_eq!(limits.len(), 2);
1567            let w = &limits[0];
1568            assert_eq!(w.rate_limit_type, RateLimitType::RequestWeight);
1569            assert_eq!(w.interval, Interval::Hour);
1570            assert_eq!(w.interval_num, 1);
1571            assert_eq!(w.count, 10);
1572            let o = &limits[1];
1573            assert_eq!(o.rate_limit_type, RateLimitType::Orders);
1574            assert_eq!(o.interval, Interval::Day);
1575            assert_eq!(o.interval_num, 2);
1576            assert_eq!(o.count, 20);
1577        }
1578
1579        #[test]
1580        fn ignores_unknown_and_malformed() {
1581            let headers = mk_headers(vec![
1582                ("x-mbx-used-weight-3x", "5"),
1583                ("random-header", "100"),
1584            ]);
1585            let limits = parse_rate_limit_headers(&headers);
1586            assert!(limits.is_empty());
1587        }
1588    }
1589
1590    mod http_request {
1591        use std::io::Write;
1592
1593        use flate2::{Compression, write::GzEncoder};
1594        use httpmock::MockServer;
1595        use reqwest::{Client, Method, Request};
1596        use serde::Deserialize;
1597
1598        use crate::{
1599            common::utils::http_request, config::ConfigurationRestApi, errors::ConnectorError,
1600            models::RestApiResponse,
1601        };
1602
1603        use super::TOKIO_SHARED_RT;
1604
1605        #[derive(Deserialize, Debug, PartialEq)]
1606        struct Dummy {
1607            foo: String,
1608        }
1609
1610        fn make_config(server_url: &str) -> ConfigurationRestApi {
1611            ConfigurationRestApi::builder()
1612                .api_key("key")
1613                .api_secret("secret")
1614                .base_path(server_url)
1615                .build()
1616                .expect("Failed to build configuration")
1617        }
1618
1619        #[test]
1620        fn http_request_success_plain_text() {
1621            TOKIO_SHARED_RT.block_on(async {
1622                let server = MockServer::start();
1623                let mock = server.mock(|when, then| {
1624                    when.method(httpmock::Method::GET).path("/test");
1625                    then.status(200)
1626                        .header("Content-Type", "application/json")
1627                        .body(r#"{"foo":"bar"}"#);
1628                });
1629
1630                let client = Client::new();
1631                let req: Request = client
1632                    .request(Method::GET, format!("{}{}", server.url(""), "/test"))
1633                    .build()
1634                    .unwrap();
1635
1636                let cfg = make_config(&server.url(""));
1637                let resp: RestApiResponse<Dummy> = http_request(req, &cfg).await.unwrap();
1638                assert_eq!(resp.status, 200);
1639                let data = resp.data().await.unwrap();
1640                assert_eq!(data, Dummy { foo: "bar".into() });
1641                mock.assert();
1642            });
1643        }
1644
1645        #[test]
1646        fn http_request_success_gzip() {
1647            TOKIO_SHARED_RT.block_on(async {
1648                let server = MockServer::start();
1649                let body = r#"{"foo":"baz"}"#;
1650                let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
1651                encoder.write_all(body.as_bytes()).unwrap();
1652                let gz = encoder.finish().unwrap();
1653
1654                let mock = server.mock(|when, then| {
1655                    when.method(httpmock::Method::GET).path("/gz");
1656                    then.status(200)
1657                        .header("Content-Type", "application/json")
1658                        .header("Content-Encoding", "gzip")
1659                        .body(gz);
1660                });
1661
1662                let client = Client::new();
1663                let req: Request = client
1664                    .request(Method::GET, format!("{}{}", server.url(""), "/gz"))
1665                    .build()
1666                    .unwrap();
1667                let mut cfg = make_config(&server.url(""));
1668                cfg.compression = true;
1669
1670                let resp: RestApiResponse<Dummy> = http_request(req, &cfg).await.unwrap();
1671                assert_eq!(resp.status, 200);
1672                let data = resp.data().await.unwrap();
1673                assert_eq!(data, Dummy { foo: "baz".into() });
1674                mock.assert();
1675            });
1676        }
1677
1678        #[test]
1679        fn http_request_client_error_bad_request() {
1680            TOKIO_SHARED_RT.block_on(async {
1681                let server = MockServer::start();
1682                let mock = server.mock(|when, then| {
1683                    when.method(httpmock::Method::GET).path("/400");
1684                    then.status(400)
1685                        .header("Content-Type", "application/json")
1686                        .body(r#"{"msg":"bad request"}"#);
1687                });
1688
1689                let client = Client::new();
1690                let req: Request = client
1691                    .request(Method::GET, format!("{}{}", server.url(""), "/400"))
1692                    .build()
1693                    .unwrap();
1694                let cfg = make_config(&server.url(""));
1695
1696                let result = http_request::<Dummy>(req, &cfg).await;
1697                assert!(matches!(result, Err(ConnectorError::BadRequestError(_))));
1698                if let Err(ConnectorError::BadRequestError(msg)) = result {
1699                    assert_eq!(msg, "bad request");
1700                }
1701                mock.assert();
1702            });
1703        }
1704
1705        #[test]
1706        fn http_request_client_error_unauthorized() {
1707            TOKIO_SHARED_RT.block_on(async {
1708                let server = MockServer::start();
1709                let mock = server.mock(|when, then| {
1710                    when.method(httpmock::Method::GET).path("/401");
1711                    then.status(401)
1712                        .header("Content-Type", "application/json")
1713                        .body(r#"{"msg":"unauthorized"}"#);
1714                });
1715
1716                let client = Client::new();
1717                let req: Request = client
1718                    .request(Method::GET, format!("{}{}", server.url(""), "/401"))
1719                    .build()
1720                    .unwrap();
1721                let cfg = make_config(&server.url(""));
1722
1723                let result = http_request::<Dummy>(req, &cfg).await;
1724                assert!(matches!(result, Err(ConnectorError::UnauthorizedError(_))));
1725                if let Err(ConnectorError::UnauthorizedError(msg)) = result {
1726                    assert_eq!(msg, "unauthorized");
1727                }
1728                mock.assert();
1729            });
1730        }
1731
1732        #[test]
1733        fn http_request_client_error_forbidden() {
1734            TOKIO_SHARED_RT.block_on(async {
1735                let server = MockServer::start();
1736                let mock = server.mock(|when, then| {
1737                    when.method(httpmock::Method::GET).path("/403");
1738                    then.status(403)
1739                        .header("Content-Type", "application/json")
1740                        .body(r#"{"msg":"forbidden"}"#);
1741                });
1742
1743                let client = Client::new();
1744                let req: Request = client
1745                    .request(Method::GET, format!("{}{}", server.url(""), "/403"))
1746                    .build()
1747                    .unwrap();
1748                let cfg = make_config(&server.url(""));
1749
1750                let result = http_request::<Dummy>(req, &cfg).await;
1751                assert!(matches!(result, Err(ConnectorError::ForbiddenError(_))));
1752                if let Err(ConnectorError::ForbiddenError(msg)) = result {
1753                    assert_eq!(msg, "forbidden");
1754                }
1755                mock.assert();
1756            });
1757        }
1758
1759        #[test]
1760        fn http_request_client_error_not_found() {
1761            TOKIO_SHARED_RT.block_on(async {
1762                let server = MockServer::start();
1763                let mock = server.mock(|when, then| {
1764                    when.method(httpmock::Method::GET).path("/404");
1765                    then.status(404)
1766                        .header("Content-Type", "application/json")
1767                        .body(r#"{"msg":"not found"}"#);
1768                });
1769
1770                let client = Client::new();
1771                let req: Request = client
1772                    .request(Method::GET, format!("{}{}", server.url(""), "/404"))
1773                    .build()
1774                    .unwrap();
1775                let cfg = make_config(&server.url(""));
1776
1777                let result = http_request::<Dummy>(req, &cfg).await;
1778                assert!(matches!(result, Err(ConnectorError::NotFoundError(_))));
1779                if let Err(ConnectorError::NotFoundError(msg)) = result {
1780                    assert_eq!(msg, "not found");
1781                }
1782                mock.assert();
1783            });
1784        }
1785
1786        #[test]
1787        fn http_request_client_error_rate_limit_exceeded() {
1788            TOKIO_SHARED_RT.block_on(async {
1789                let server = MockServer::start();
1790                let mock = server.mock(|when, then| {
1791                    when.method(httpmock::Method::GET).path("/418");
1792                    then.status(418)
1793                        .header("Content-Type", "application/json")
1794                        .body(r#"{"msg":"rate limit exceeded"}"#);
1795                });
1796
1797                let client = Client::new();
1798                let req: Request = client
1799                    .request(Method::GET, format!("{}{}", server.url(""), "/418"))
1800                    .build()
1801                    .unwrap();
1802                let cfg = make_config(&server.url(""));
1803
1804                let result = http_request::<Dummy>(req, &cfg).await;
1805                assert!(matches!(result, Err(ConnectorError::RateLimitBanError(_))));
1806                if let Err(ConnectorError::RateLimitBanError(msg)) = result {
1807                    assert_eq!(msg, "rate limit exceeded");
1808                }
1809                mock.assert();
1810            });
1811        }
1812
1813        #[test]
1814        fn http_request_client_error_too_many_requests() {
1815            TOKIO_SHARED_RT.block_on(async {
1816                let server = MockServer::start();
1817                let mock = server.mock(|when, then| {
1818                    when.method(httpmock::Method::GET).path("/429");
1819                    then.status(429)
1820                        .header("Content-Type", "application/json")
1821                        .body(r#"{"msg":"too many requests"}"#);
1822                });
1823
1824                let client = Client::new();
1825                let req: Request = client
1826                    .request(Method::GET, format!("{}{}", server.url(""), "/429"))
1827                    .build()
1828                    .unwrap();
1829                let cfg = make_config(&server.url(""));
1830
1831                let result = http_request::<Dummy>(req, &cfg).await;
1832                assert!(matches!(
1833                    result,
1834                    Err(ConnectorError::TooManyRequestsError(_))
1835                ));
1836                if let Err(ConnectorError::TooManyRequestsError(msg)) = result {
1837                    assert_eq!(msg, "too many requests");
1838                }
1839                mock.assert();
1840            });
1841        }
1842
1843        #[test]
1844        fn http_request_client_error_server_error() {
1845            TOKIO_SHARED_RT.block_on(async {
1846                let server = MockServer::start();
1847                let mock = server.mock(|when, then| {
1848                    when.method(httpmock::Method::GET).path("/500");
1849                    then.status(500)
1850                        .header("Content-Type", "application/json")
1851                        .body(r#"{"msg":"internal server error"}"#);
1852                });
1853
1854                let client = Client::new();
1855                let req: Request = client
1856                    .request(Method::GET, format!("{}{}", server.url(""), "/500"))
1857                    .build()
1858                    .unwrap();
1859                let cfg = make_config(&server.url(""));
1860
1861                let result = http_request::<Dummy>(req, &cfg).await;
1862                assert!(matches!(result, Err(ConnectorError::ServerError { .. })));
1863                if let Err(ConnectorError::ServerError {
1864                    msg,
1865                    status_code: Some(500),
1866                }) = result
1867                {
1868                    assert_eq!(msg, "Server error: 500".to_string());
1869                }
1870                mock.assert();
1871            });
1872        }
1873
1874        #[test]
1875        fn http_request_unexpected_status_maps_generic() {
1876            TOKIO_SHARED_RT.block_on(async {
1877                let server = MockServer::start();
1878                let code = 402;
1879                let mock = server.mock(|when, then| {
1880                    when.method(httpmock::Method::GET).path("/402");
1881                    then.status(code).body("error text");
1882                });
1883
1884                let client = Client::new();
1885                let req: Request = client
1886                    .request(Method::GET, format!("{}{}", server.url(""), "/402"))
1887                    .build()
1888                    .unwrap();
1889                let cfg = make_config(&server.url(""));
1890
1891                let result = http_request::<Dummy>(req, &cfg).await;
1892                assert!(matches!(
1893                    result,
1894                    Err(ConnectorError::ConnectorClientError(_))
1895                ));
1896                mock.assert();
1897            });
1898        }
1899
1900        #[test]
1901        fn http_request_malformed_json_maps_generic() {
1902            TOKIO_SHARED_RT.block_on(async {
1903                let server = MockServer::start();
1904                let mock = server.mock(|when, then| {
1905                    when.method(httpmock::Method::GET).path("/malformed");
1906                    then.status(200)
1907                        .header("Content-Type", "application/json")
1908                        .body("not json");
1909                });
1910
1911                let client = Client::new();
1912                let req: Request = client
1913                    .request(Method::GET, format!("{}{}", server.url(""), "/malformed"))
1914                    .build()
1915                    .unwrap();
1916                let cfg = make_config(&server.url(""));
1917
1918                // 1) HTTP layer still “succeeds”:
1919                let resp = http_request::<Dummy>(req, &cfg)
1920                    .await
1921                    .expect("http_request should succeed even if JSON is bad");
1922
1923                // 2) only when we call `.data().await` do we hit the parse‐error:
1924                let err = resp
1925                    .data() // or however you invoke that boxed future
1926                    .await
1927                    .expect_err("malformed JSON should turn into ConnectorClientError");
1928
1929                assert!(matches!(err, ConnectorError::ConnectorClientError(_)));
1930
1931                mock.assert();
1932            });
1933        }
1934    }
1935
1936    mod send_request {
1937        use anyhow::Result;
1938        use httpmock::prelude::*;
1939        use reqwest::Method;
1940        use serde::Deserialize;
1941        use serde_json::json;
1942        use std::collections::BTreeMap;
1943
1944        use crate::{
1945            common::{models::TimeUnit, utils::send_request},
1946            config::ConfigurationRestApi,
1947        };
1948
1949        use super::TOKIO_SHARED_RT;
1950
1951        #[derive(Deserialize, Debug, PartialEq)]
1952        struct TestResponse {
1953            message: String,
1954        }
1955
1956        #[test]
1957        fn basic_get_request() -> Result<()> {
1958            TOKIO_SHARED_RT.block_on(async {
1959                let server = MockServer::start();
1960
1961                server.mock(|when, then| {
1962                    when.method(GET).path("/api/v1/test");
1963                    then.status(200)
1964                        .header("content-type", "application/json")
1965                        .body(r#"{"message": "success"}"#);
1966                });
1967
1968                let configuration = ConfigurationRestApi::builder()
1969                    .api_key("key")
1970                    .api_secret("secret")
1971                    .base_path(server.base_url())
1972                    .compression(false)
1973                    .build()
1974                    .expect("Failed to build configuration");
1975
1976                let params = BTreeMap::new();
1977
1978                let result = send_request::<TestResponse>(
1979                    &configuration,
1980                    "/api/v1/test",
1981                    Method::GET,
1982                    params,
1983                    None,
1984                    false,
1985                )
1986                .await?;
1987
1988                let data = result.data().await.unwrap();
1989                assert_eq!(data.message, "success");
1990
1991                Ok(())
1992            })
1993        }
1994
1995        #[test]
1996        fn signed_post_request() -> Result<()> {
1997            TOKIO_SHARED_RT.block_on(async {
1998                let server = MockServer::start();
1999
2000                server.mock(|when, then| {
2001                    when.method(POST).path("/api/v3/order");
2002                    then.status(200)
2003                        .header("content-type", "application/json")
2004                        .body(r#"{"message": "order placed"}"#);
2005                });
2006
2007                let configuration = ConfigurationRestApi::builder()
2008                    .api_key("key")
2009                    .api_secret("secret")
2010                    .base_path(server.base_url())
2011                    .compression(false)
2012                    .build()
2013                    .expect("Failed to build configuration");
2014
2015                let mut params = BTreeMap::new();
2016                params.insert("symbol".to_string(), json!("ETHUSDT"));
2017                params.insert("side".to_string(), json!("BUY"));
2018                params.insert("type".to_string(), json!("MARKET"));
2019                params.insert("quantity".to_string(), json!("1"));
2020
2021                let result = send_request::<TestResponse>(
2022                    &configuration,
2023                    "/api/v3/order",
2024                    Method::POST,
2025                    params,
2026                    None,
2027                    true,
2028                )
2029                .await?;
2030
2031                let data = result.data().await.unwrap();
2032                assert_eq!(data.message, "order placed");
2033
2034                Ok(())
2035            })
2036        }
2037
2038        #[test]
2039        fn get_request_with_params() -> Result<()> {
2040            TOKIO_SHARED_RT.block_on(async {
2041                let server = MockServer::start();
2042
2043                server.mock(|when, then| {
2044                    when.method(GET)
2045                        .path("/api/v1/data")
2046                        .query_param("symbol", "BTCUSDT")
2047                        .query_param("limit", "10");
2048                    then.status(200)
2049                        .header("content-type", "application/json")
2050                        .body(r#"{"message": "data retrieved"}"#);
2051                });
2052
2053                let configuration = ConfigurationRestApi::builder()
2054                    .api_key("key")
2055                    .api_secret("secret")
2056                    .base_path(server.base_url())
2057                    .compression(false)
2058                    .build()
2059                    .expect("Failed to build configuration");
2060
2061                let mut params = BTreeMap::new();
2062                params.insert("symbol".to_string(), json!("BTCUSDT"));
2063                params.insert("limit".to_string(), json!(10));
2064
2065                let result = send_request::<TestResponse>(
2066                    &configuration,
2067                    "/api/v1/data",
2068                    Method::GET,
2069                    params,
2070                    None,
2071                    false,
2072                )
2073                .await?;
2074
2075                let data = result.data().await.unwrap();
2076                assert_eq!(data.message, "data retrieved");
2077
2078                Ok(())
2079            })
2080        }
2081
2082        #[test]
2083        fn invalid_endpoint() {
2084            TOKIO_SHARED_RT.block_on(async {
2085                let server = MockServer::start();
2086
2087                let configuration = ConfigurationRestApi::builder()
2088                    .api_key("key")
2089                    .api_secret("secret")
2090                    .base_path(server.base_url())
2091                    .compression(false)
2092                    .build()
2093                    .expect("Failed to build configuration");
2094
2095                let params = BTreeMap::new();
2096
2097                let result = send_request::<TestResponse>(
2098                    &configuration,
2099                    "http://invalid",
2100                    Method::GET,
2101                    params,
2102                    None,
2103                    false,
2104                )
2105                .await;
2106
2107                assert!(result.is_err());
2108            });
2109        }
2110
2111        #[test]
2112        fn missing_signature_on_signed_request() {
2113            TOKIO_SHARED_RT.block_on(async {
2114                let server = MockServer::start();
2115
2116                let configuration = ConfigurationRestApi::builder()
2117                    .api_key("key")
2118                    .api_secret("secret")
2119                    .base_path(server.base_url())
2120                    .compression(false)
2121                    .build()
2122                    .expect("Failed to build configuration");
2123
2124                let mut params = BTreeMap::new();
2125                params.insert("symbol".to_string(), json!("BTCUSDT"));
2126                params.insert("side".to_string(), json!("BUY"));
2127
2128                let result = send_request::<TestResponse>(
2129                    &configuration,
2130                    "/api/v3/order",
2131                    Method::POST,
2132                    params,
2133                    None,
2134                    true,
2135                )
2136                .await;
2137
2138                assert!(result.is_err());
2139            });
2140        }
2141
2142        #[test]
2143        fn compression_enabled() -> Result<()> {
2144            TOKIO_SHARED_RT.block_on(async {
2145                let server = MockServer::start();
2146
2147                server.mock(|when, then| {
2148                    when.method(GET).path("/api/v1/test");
2149                    then.status(200)
2150                        .header("content-type", "application/json")
2151                        .header("accept-encoding", "gzip, deflate, br")
2152                        .body(r#"{"message": "compression enabled"}"#);
2153                });
2154
2155                let configuration = ConfigurationRestApi::builder()
2156                    .api_key("key")
2157                    .api_secret("secret")
2158                    .base_path(server.base_url())
2159                    .compression(true)
2160                    .build()
2161                    .expect("Failed to build configuration");
2162
2163                let params = BTreeMap::new();
2164
2165                let result = send_request::<TestResponse>(
2166                    &configuration,
2167                    "/api/v1/test",
2168                    Method::GET,
2169                    params,
2170                    None,
2171                    false,
2172                )
2173                .await?;
2174
2175                let data = result.data().await.unwrap();
2176                assert_eq!(data.message, "compression enabled");
2177
2178                Ok(())
2179            })
2180        }
2181
2182        #[test]
2183        fn get_request_with_time_unit_header() -> Result<()> {
2184            TOKIO_SHARED_RT.block_on(async {
2185                let server = MockServer::start();
2186
2187                server.mock(|when, then| {
2188                    when.method(GET)
2189                        .path("/api/v1/test")
2190                        .header("X-MBX-TIME-UNIT", "MILLISECOND");
2191                    then.status(200)
2192                        .header("content-type", "application/json")
2193                        .body(r#"{"message": "time unit applied"}"#);
2194                });
2195
2196                let configuration = ConfigurationRestApi::builder()
2197                    .api_key("key")
2198                    .api_secret("secret")
2199                    .base_path(server.base_url())
2200                    .compression(false)
2201                    .time_unit(TimeUnit::Millisecond)
2202                    .build()
2203                    .expect("Failed to build configuration");
2204
2205                let params = BTreeMap::new();
2206
2207                let result = send_request::<TestResponse>(
2208                    &configuration,
2209                    "/api/v1/test",
2210                    Method::GET,
2211                    params,
2212                    Some(TimeUnit::Millisecond),
2213                    false,
2214                )
2215                .await?;
2216
2217                let data = result.data().await.unwrap();
2218                assert_eq!(data.message, "time unit applied");
2219
2220                Ok(())
2221            })
2222        }
2223    }
2224
2225    mod random_string {
2226        use crate::common::utils::random_string;
2227        use hex;
2228
2229        #[test]
2230        fn length_is_32() {
2231            let s = random_string();
2232            assert_eq!(
2233                s.len(),
2234                32,
2235                "random_string() should be 32 chars, got {}",
2236                s.len()
2237            );
2238        }
2239
2240        #[test]
2241        fn is_valid_lowercase_hex() {
2242            let s = random_string();
2243            assert!(
2244                s.chars().all(|c| matches!(c, '0'..='9' | 'a'..='f')),
2245                "random_string() contains invalid hex characters: {s}"
2246            );
2247        }
2248
2249        #[test]
2250        fn decodes_to_16_bytes() {
2251            let s = random_string();
2252            let bytes = hex::decode(&s).expect("random_string() output must be valid hex");
2253            assert_eq!(
2254                bytes.len(),
2255                16,
2256                "hex::decode returned {} bytes",
2257                bytes.len()
2258            );
2259        }
2260
2261        #[test]
2262        fn two_calls_are_different() {
2263            let a = random_string();
2264            let b = random_string();
2265            assert_ne!(
2266                a, b,
2267                "Two calls to random_string() returned the same value: {a}"
2268            );
2269        }
2270    }
2271
2272    mod remove_empty_value {
2273        use crate::common::utils::remove_empty_value;
2274        use serde_json::{Map, Value};
2275
2276        #[test]
2277        fn filters_out_null_and_empty_strings() {
2278            let entries = vec![
2279                ("key1".to_string(), Value::String("value1".to_string())),
2280                ("key2".to_string(), Value::Null),
2281                ("key3".to_string(), Value::String(String::new())),
2282            ];
2283            let result = remove_empty_value(entries);
2284            assert_eq!(
2285                result.len(),
2286                1,
2287                "expected only one entry, got {}",
2288                result.len()
2289            );
2290            assert_eq!(
2291                result.get("key1"),
2292                Some(&Value::String("value1".to_string()))
2293            );
2294            assert!(!result.contains_key("key2"));
2295            assert!(!result.contains_key("key3"));
2296        }
2297
2298        #[test]
2299        fn retains_other_value_types() {
2300            let entries = vec![
2301                ("bool".to_string(), Value::Bool(true)),
2302                ("num".to_string(), Value::Number(42.into())),
2303                ("arr".to_string(), Value::Array(vec![])),
2304                ("obj".to_string(), Value::Object(Map::default())),
2305                ("nil".to_string(), Value::Null),
2306                ("empty_str".to_string(), Value::String(String::new())),
2307            ];
2308            let result = remove_empty_value(entries);
2309            let keys: Vec<&String> = result.keys().collect();
2310            assert_eq!(keys.len(), 4, "expected 4 entries, got {}", keys.len());
2311            assert!(result.get("bool") == Some(&Value::Bool(true)));
2312            assert!(result.get("num") == Some(&Value::Number(42.into())));
2313            assert!(result.get("arr") == Some(&Value::Array(vec![])));
2314            assert!(result.get("obj") == Some(&Value::Object(Map::default())));
2315            assert!(!result.contains_key("nil"));
2316            assert!(!result.contains_key("empty_str"));
2317        }
2318
2319        #[test]
2320        fn empty_iterator_returns_empty_map() {
2321            let entries: Vec<(String, Value)> = vec![];
2322            let result = remove_empty_value(entries);
2323            assert!(result.is_empty(), "expected an empty map");
2324        }
2325
2326        #[test]
2327        fn keys_are_sorted() {
2328            let entries = vec![
2329                ("c".to_string(), Value::String("foo".to_string())),
2330                ("a".to_string(), Value::String("bar".to_string())),
2331                ("b".to_string(), Value::String("baz".to_string())),
2332            ];
2333            let result = remove_empty_value(entries);
2334            let sorted_keys: Vec<&String> = result.keys().collect();
2335            assert_eq!(
2336                sorted_keys,
2337                [&"a".to_string(), &"b".to_string(), &"c".to_string()]
2338            );
2339        }
2340    }
2341
2342    mod sort_object_params {
2343        use crate::common::utils::sort_object_params;
2344        use serde_json::Value;
2345        use std::collections::BTreeMap;
2346
2347        #[test]
2348        fn sorts_keys() {
2349            let mut params = BTreeMap::new();
2350            params.insert("z".to_string(), Value::String("last".to_string()));
2351            params.insert("a".to_string(), Value::String("first".to_string()));
2352            params.insert("m".to_string(), Value::String("middle".to_string()));
2353
2354            let sorted = sort_object_params(&params);
2355            let keys: Vec<&String> = sorted.keys().collect();
2356            assert_eq!(
2357                keys,
2358                [&"a".to_string(), &"m".to_string(), &"z".to_string()],
2359                "Keys should be sorted alphabetically"
2360            );
2361        }
2362
2363        #[test]
2364        fn preserves_values() {
2365            let mut params = BTreeMap::new();
2366            params.insert("one".to_string(), Value::Number(1.into()));
2367            params.insert("two".to_string(), Value::Bool(true));
2368
2369            let sorted = sort_object_params(&params);
2370            assert_eq!(sorted.get("one"), Some(&Value::Number(1.into())));
2371            assert_eq!(sorted.get("two"), Some(&Value::Bool(true)));
2372        }
2373
2374        #[test]
2375        fn empty_map_returns_empty() {
2376            let params: BTreeMap<String, Value> = BTreeMap::new();
2377            let sorted = sort_object_params(&params);
2378            assert!(sorted.is_empty(), "Expected empty map");
2379        }
2380
2381        #[test]
2382        fn independent_clone() {
2383            let mut params = BTreeMap::new();
2384            params.insert("key".to_string(), Value::String("val".to_string()));
2385
2386            let mut sorted = sort_object_params(&params);
2387            sorted.insert("new".to_string(), Value::String("x".to_string()));
2388
2389            assert!(
2390                !params.contains_key("new"),
2391                "Original should not be modified when changing sorted"
2392            );
2393            assert!(
2394                sorted.contains_key("new"),
2395                "Sorted map should reflect its own insertions"
2396            );
2397        }
2398    }
2399
2400    mod normalize_ws_streams_key {
2401        use crate::common::utils::normalize_ws_streams_key;
2402
2403        #[test]
2404        fn returns_empty_for_empty() {
2405            assert_eq!(normalize_ws_streams_key(""), "");
2406        }
2407
2408        #[test]
2409        fn already_normalized_stays_same() {
2410            assert_eq!(normalize_ws_streams_key("streamname"), "streamname");
2411        }
2412
2413        #[test]
2414        fn uppercases_are_lowercased() {
2415            assert_eq!(normalize_ws_streams_key("MyStream"), "mystream");
2416        }
2417
2418        #[test]
2419        fn underscores_are_removed() {
2420            assert_eq!(normalize_ws_streams_key("my_stream_name"), "mystreamname");
2421        }
2422
2423        #[test]
2424        fn hyphens_are_removed() {
2425            assert_eq!(normalize_ws_streams_key("my-stream-name"), "mystreamname");
2426        }
2427
2428        #[test]
2429        fn mixed_underscores_and_hyphens_and_case() {
2430            let input = "Mixed_Case-Stream_Name";
2431            let expected = "mixedcasestreamname";
2432            assert_eq!(normalize_ws_streams_key(input), expected);
2433        }
2434
2435        #[test]
2436        fn retains_other_punctuation() {
2437            assert_eq!(normalize_ws_streams_key("stream.name!"), "stream.name!");
2438        }
2439    }
2440
2441    mod replace_websocket_streams_placeholders {
2442        use crate::common::utils::replace_websocket_streams_placeholders;
2443        use std::collections::HashMap;
2444
2445        #[test]
2446        fn empty_string_unchanged() {
2447            let vars: HashMap<&str, &str> = HashMap::new();
2448            assert_eq!(replace_websocket_streams_placeholders("", &vars), "");
2449        }
2450
2451        #[test]
2452        fn unknown_placeholder_becomes_empty() {
2453            let vars: HashMap<&str, &str> = HashMap::new();
2454            assert_eq!(replace_websocket_streams_placeholders("<foo>", &vars), "");
2455        }
2456
2457        #[test]
2458        fn leading_slash_symbol_lowercases_head() {
2459            let mut vars = HashMap::new();
2460            vars.insert("symbol", "BTC");
2461            assert_eq!(
2462                replace_websocket_streams_placeholders("/<symbol>", &vars),
2463                "btc"
2464            );
2465        }
2466
2467        #[test]
2468        fn no_lowercase_without_slash() {
2469            let mut vars = HashMap::new();
2470            vars.insert("symbol", "BTC");
2471            assert_eq!(
2472                replace_websocket_streams_placeholders("<symbol>", &vars),
2473                "BTC"
2474            );
2475        }
2476
2477        #[test]
2478        fn multiple_placeholders_mid_preserve_ats() {
2479            let mut vars = HashMap::new();
2480            vars.insert("symbol", "BNBUSDT");
2481            vars.insert("levels", "10");
2482            vars.insert("updateSpeed", "1000ms");
2483            let out = replace_websocket_streams_placeholders(
2484                "/<symbol>@depth<levels>@<updateSpeed>",
2485                &vars,
2486            );
2487            assert_eq!(out, "bnbusdt@depth10@1000ms");
2488        }
2489
2490        #[test]
2491        fn trailing_at_removed_when_missing_var() {
2492            let mut vars = HashMap::new();
2493            vars.insert("symbol", "BNBUSDT");
2494            vars.insert("levels", "10");
2495            let out = replace_websocket_streams_placeholders(
2496                "/<symbol>@depth<levels>@<updateSpeed>",
2497                &vars,
2498            );
2499            assert_eq!(out, "bnbusdt@depth10");
2500        }
2501
2502        #[test]
2503        fn custom_key_normalization_and_value() {
2504            let mut vars = HashMap::new();
2505            vars.insert("my-stream_key", "Value");
2506            assert_eq!(
2507                replace_websocket_streams_placeholders("<My_Stream-Key>", &vars),
2508                "Value"
2509            );
2510        }
2511
2512        #[test]
2513        fn text_surrounding_placeholders_intact() {
2514            let mut vars = HashMap::new();
2515            vars.insert("symbol", "ABC");
2516            let input = "pre-<symbol>-post";
2517            assert_eq!(
2518                replace_websocket_streams_placeholders(input, &vars),
2519                "pre-ABC-post"
2520            );
2521        }
2522    }
2523}