Skip to main content

simulator_client/
urls.rs

1use thiserror::Error;
2
3/// Error converting a URL scheme.
4#[derive(Debug, Error)]
5pub enum UrlError {
6    #[error("cannot derive WebSocket URL from `{url}`: expected http:// or https:// scheme")]
7    InvalidScheme { url: String },
8}
9
10/// Convert an `http(s)://` RPC endpoint URL to a `ws(s)://` URL.
11pub fn http_to_ws_url(url: &str) -> Result<String, UrlError> {
12    let scheme = if url.starts_with("https://") {
13        "wss"
14    } else if url.starts_with("http://") {
15        "ws"
16    } else {
17        return Err(UrlError::InvalidScheme {
18            url: url.to_string(),
19        });
20    };
21    let rest = url.split_once("://").map(|x| x.1).unwrap_or(url);
22    Ok(format!("{scheme}://{rest}"))
23}
24
25/// Build the backtest WebSocket URL from a user-supplied endpoint: a bare
26/// hostname becomes `wss://{host}/backtest`, while an explicit `ws(s)://` URL
27/// keeps its scheme (for local development against plain-`ws://` servers) and
28/// gains the `/backtest` path only if missing. Any other scheme passes
29/// through untouched. Idempotent, so already-normalized URLs survive the
30/// [`crate::BacktestClient`] builder applying this to every `url` input.
31pub fn backtest_ws_url(url: &str) -> String {
32    if url.starts_with("ws://") || url.starts_with("wss://") {
33        let trimmed = url.trim_end_matches('/');
34        if trimmed.ends_with("/backtest") {
35            trimmed.to_string()
36        } else {
37            format!("{trimmed}/backtest")
38        }
39    } else if url.contains("://") {
40        url.to_string()
41    } else {
42        format!("wss://{}/backtest", url.trim_end_matches('/'))
43    }
44}
45
46/// Derive an HTTP base URL from a WebSocket URL.
47/// `wss://host:port/path` → `https://host:port`
48pub fn http_base_from_ws_url(ws_url: &str) -> String {
49    let http = ws_url
50        .replacen("wss://", "https://", 1)
51        .replacen("ws://", "http://", 1);
52    if let Some(start) = http.find("://").map(|i| i + 3)
53        && let Some(slash) = http[start..].find('/')
54    {
55        return http[..start + slash].to_string();
56    }
57    http
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn http_to_ws_converts_http() {
66        assert_eq!(
67            http_to_ws_url("http://localhost:8899").unwrap(),
68            "ws://localhost:8899"
69        );
70    }
71
72    #[test]
73    fn http_to_ws_converts_https() {
74        assert_eq!(
75            http_to_ws_url("https://api.mainnet-beta.solana.com").unwrap(),
76            "wss://api.mainnet-beta.solana.com"
77        );
78    }
79
80    #[test]
81    fn http_to_ws_rejects_other_schemes() {
82        assert!(matches!(
83            http_to_ws_url("ws://example.com"),
84            Err(UrlError::InvalidScheme { .. })
85        ));
86        assert!(matches!(
87            http_to_ws_url("ftp://example.com"),
88            Err(UrlError::InvalidScheme { .. })
89        ));
90        assert!(matches!(
91            http_to_ws_url("example.com"),
92            Err(UrlError::InvalidScheme { .. })
93        ));
94    }
95
96    #[test]
97    fn backtest_ws_url_wraps_bare_hosts() {
98        assert_eq!(
99            backtest_ws_url("simulator.termina.technology"),
100            "wss://simulator.termina.technology/backtest"
101        );
102    }
103
104    #[test]
105    fn backtest_ws_url_keeps_explicit_schemes() {
106        assert_eq!(
107            backtest_ws_url("ws://localhost:8900"),
108            "ws://localhost:8900/backtest"
109        );
110        assert_eq!(
111            backtest_ws_url("wss://staging.simulator.termina.technology/"),
112            "wss://staging.simulator.termina.technology/backtest"
113        );
114    }
115
116    #[test]
117    fn backtest_ws_url_is_idempotent() {
118        for url in [
119            "simulator.termina.technology",
120            "ws://localhost:8900",
121            "wss://staging.simulator.termina.technology/",
122        ] {
123            let normalized = backtest_ws_url(url);
124            assert_eq!(backtest_ws_url(&normalized), normalized);
125        }
126    }
127
128    #[test]
129    fn backtest_ws_url_passes_other_schemes_through() {
130        assert_eq!(
131            backtest_ws_url("http://127.0.0.1:8900"),
132            "http://127.0.0.1:8900"
133        );
134        assert_eq!(
135            backtest_ws_url("https://host/backtest"),
136            "https://host/backtest"
137        );
138    }
139
140    #[test]
141    fn http_base_strips_path() {
142        assert_eq!(
143            http_base_from_ws_url("wss://host:8900/backtest"),
144            "https://host:8900"
145        );
146        assert_eq!(
147            http_base_from_ws_url("ws://localhost:8900/backtest"),
148            "http://localhost:8900"
149        );
150    }
151
152    #[test]
153    fn http_base_no_path() {
154        assert_eq!(
155            http_base_from_ws_url("wss://host:8900"),
156            "https://host:8900"
157        );
158    }
159}