simulator-client 0.9.0

Async WebSocket client for the Solana simulator backtest API
Documentation
use thiserror::Error;

/// Error converting a URL scheme.
#[derive(Debug, Error)]
pub enum UrlError {
    #[error("cannot derive WebSocket URL from `{url}`: expected http:// or https:// scheme")]
    InvalidScheme { url: String },
}

/// Convert an `http(s)://` RPC endpoint URL to a `ws(s)://` URL.
pub fn http_to_ws_url(url: &str) -> Result<String, UrlError> {
    let scheme = if url.starts_with("https://") {
        "wss"
    } else if url.starts_with("http://") {
        "ws"
    } else {
        return Err(UrlError::InvalidScheme {
            url: url.to_string(),
        });
    };
    let rest = url.split_once("://").map(|x| x.1).unwrap_or(url);
    Ok(format!("{scheme}://{rest}"))
}

/// Build the backtest WebSocket URL from a user-supplied endpoint: a bare
/// hostname becomes `wss://{host}/backtest`, while an explicit `ws(s)://` URL
/// keeps its scheme (for local development against plain-`ws://` servers) and
/// gains the `/backtest` path only if missing. Any other scheme passes
/// through untouched. Idempotent, so already-normalized URLs survive the
/// [`crate::BacktestClient`] builder applying this to every `url` input.
pub fn backtest_ws_url(url: &str) -> String {
    if url.starts_with("ws://") || url.starts_with("wss://") {
        let trimmed = url.trim_end_matches('/');
        if trimmed.ends_with("/backtest") {
            trimmed.to_string()
        } else {
            format!("{trimmed}/backtest")
        }
    } else if url.contains("://") {
        url.to_string()
    } else {
        format!("wss://{}/backtest", url.trim_end_matches('/'))
    }
}

/// Derive an HTTP base URL from a WebSocket URL.
/// `wss://host:port/path` → `https://host:port`
pub fn http_base_from_ws_url(ws_url: &str) -> String {
    let http = ws_url
        .replacen("wss://", "https://", 1)
        .replacen("ws://", "http://", 1);
    if let Some(start) = http.find("://").map(|i| i + 3)
        && let Some(slash) = http[start..].find('/')
    {
        return http[..start + slash].to_string();
    }
    http
}

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

    #[test]
    fn http_to_ws_converts_http() {
        assert_eq!(
            http_to_ws_url("http://localhost:8899").unwrap(),
            "ws://localhost:8899"
        );
    }

    #[test]
    fn http_to_ws_converts_https() {
        assert_eq!(
            http_to_ws_url("https://api.mainnet-beta.solana.com").unwrap(),
            "wss://api.mainnet-beta.solana.com"
        );
    }

    #[test]
    fn http_to_ws_rejects_other_schemes() {
        assert!(matches!(
            http_to_ws_url("ws://example.com"),
            Err(UrlError::InvalidScheme { .. })
        ));
        assert!(matches!(
            http_to_ws_url("ftp://example.com"),
            Err(UrlError::InvalidScheme { .. })
        ));
        assert!(matches!(
            http_to_ws_url("example.com"),
            Err(UrlError::InvalidScheme { .. })
        ));
    }

    #[test]
    fn backtest_ws_url_wraps_bare_hosts() {
        assert_eq!(
            backtest_ws_url("simulator.termina.technology"),
            "wss://simulator.termina.technology/backtest"
        );
    }

    #[test]
    fn backtest_ws_url_keeps_explicit_schemes() {
        assert_eq!(
            backtest_ws_url("ws://localhost:8900"),
            "ws://localhost:8900/backtest"
        );
        assert_eq!(
            backtest_ws_url("wss://staging.simulator.termina.technology/"),
            "wss://staging.simulator.termina.technology/backtest"
        );
    }

    #[test]
    fn backtest_ws_url_is_idempotent() {
        for url in [
            "simulator.termina.technology",
            "ws://localhost:8900",
            "wss://staging.simulator.termina.technology/",
        ] {
            let normalized = backtest_ws_url(url);
            assert_eq!(backtest_ws_url(&normalized), normalized);
        }
    }

    #[test]
    fn backtest_ws_url_passes_other_schemes_through() {
        assert_eq!(
            backtest_ws_url("http://127.0.0.1:8900"),
            "http://127.0.0.1:8900"
        );
        assert_eq!(
            backtest_ws_url("https://host/backtest"),
            "https://host/backtest"
        );
    }

    #[test]
    fn http_base_strips_path() {
        assert_eq!(
            http_base_from_ws_url("wss://host:8900/backtest"),
            "https://host:8900"
        );
        assert_eq!(
            http_base_from_ws_url("ws://localhost:8900/backtest"),
            "http://localhost:8900"
        );
    }

    #[test]
    fn http_base_no_path() {
        assert_eq!(
            http_base_from_ws_url("wss://host:8900"),
            "https://host:8900"
        );
    }
}