maxt 0.2.1

One Rust API for Upbit, Bithumb, Binance, and Hyperliquid market data, accounts, and orders.
Documentation
//! Shared HTTP and WebSocket transport implementations.

#[cfg(target_arch = "wasm32")]
use std::sync::OnceLock;

#[cfg(any(test, target_arch = "wasm32"))]
use crate::error::{Error, Result};

pub(crate) mod http;
pub(crate) mod ws;

#[allow(unused_imports)]
pub(crate) use http::{HttpMethod, HttpRequest, HttpResponse, HttpTransport};
#[allow(unused_imports)]
pub(crate) use ws::{Heartbeat, HeartbeatFrame, WsCommand, WsConnect, WsSession, connect};

#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn ensure_crypto_provider() {
    if rustls::crypto::CryptoProvider::get_default().is_none() {
        let _ = rustls::crypto::ring::default_provider().install_default();
    }
}

#[cfg(any(test, target_arch = "wasm32"))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct BrowserRelay {
    pub(crate) http: String,
    pub(crate) websocket: String,
}

#[cfg(target_arch = "wasm32")]
static BROWSER_RELAY: OnceLock<BrowserRelay> = OnceLock::new();

#[cfg(target_arch = "wasm32")]
pub(crate) fn configure_browser_relay(relay_url: &str) -> Result<()> {
    let relay = relay_endpoints(relay_url)?;
    if let Some(configured) = BROWSER_RELAY.get() {
        return if configured == &relay {
            Ok(())
        } else {
            Err(Error::invalid_request(
                "relay_url",
                "the browser relay is already configured to a different origin",
            ))
        };
    }

    match BROWSER_RELAY.set(relay) {
        Ok(()) => Ok(()),
        Err(relay) if BROWSER_RELAY.get() == Some(&relay) => Ok(()),
        Err(_) => Err(Error::invalid_request(
            "relay_url",
            "the browser relay is already configured to a different origin",
        )),
    }
}

#[cfg(target_arch = "wasm32")]
pub(crate) fn browser_relay() -> Option<&'static BrowserRelay> {
    BROWSER_RELAY.get()
}

#[cfg(any(test, target_arch = "wasm32"))]
fn relay_endpoints(relay_url: &str) -> Result<BrowserRelay> {
    if relay_url.trim() != relay_url {
        return Err(Error::invalid_request(
            "relay_url",
            "must not contain leading or trailing whitespace",
        ));
    }

    let url = reqwest::Url::parse(relay_url)
        .map_err(|error| Error::invalid_request("relay_url", error.to_string()))?;
    if !matches!(url.scheme(), "http" | "https") {
        return Err(Error::invalid_request(
            "relay_url",
            "must use http or https",
        ));
    }
    if url.host().is_none()
        || !url.username().is_empty()
        || url.password().is_some()
        || url.path() != "/"
        || url.query().is_some()
        || url.fragment().is_some()
    {
        return Err(Error::invalid_request(
            "relay_url",
            "must be an origin without credentials, a path, query, or fragment",
        ));
    }

    let origin = url.origin().ascii_serialization();
    let websocket_scheme = if url.scheme() == "https" { "wss" } else { "ws" };
    let websocket_origin = origin.replacen(url.scheme(), websocket_scheme, 1);
    Ok(BrowserRelay {
        http: format!("{origin}/v1/http"),
        websocket: format!("{websocket_origin}/v1/ws"),
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn relay_origin_builds_fixed_http_and_websocket_endpoints() {
        assert_eq!(
            relay_endpoints("https://relay.example:8443").unwrap(),
            BrowserRelay {
                http: "https://relay.example:8443/v1/http".into(),
                websocket: "wss://relay.example:8443/v1/ws".into(),
            }
        );
    }

    #[test]
    fn relay_configuration_accepts_only_a_plain_http_origin() {
        for invalid in [
            "wss://relay.example",
            "https://user@relay.example",
            "https://relay.example/base",
            "https://relay.example?query=yes",
            "https://relay.example#fragment",
            " https://relay.example",
        ] {
            assert!(
                matches!(relay_endpoints(invalid), Err(Error::InvalidRequest { .. })),
                "{invalid}"
            );
        }
    }
}