Skip to main content

nautilus_binance/common/
urls.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! URL resolution helpers for Binance API endpoints.
17
18use super::{
19    consts::{
20        BINANCE_FUTURES_COIN_DEMO_HTTP_URL, BINANCE_FUTURES_COIN_HTTP_URL,
21        BINANCE_FUTURES_COIN_TESTNET_HTTP_URL, BINANCE_FUTURES_COIN_TESTNET_WS_URL,
22        BINANCE_FUTURES_COIN_WS_URL, BINANCE_FUTURES_USD_DEMO_HTTP_URL,
23        BINANCE_FUTURES_USD_HTTP_URL, BINANCE_FUTURES_USD_TESTNET_HTTP_URL,
24        BINANCE_FUTURES_USD_TESTNET_WS_URL, BINANCE_FUTURES_USD_WS_PRIVATE_URL,
25        BINANCE_FUTURES_USD_WS_PUBLIC_URL, BINANCE_FUTURES_USD_WS_URL, BINANCE_OPTIONS_HTTP_URL,
26        BINANCE_OPTIONS_WS_URL, BINANCE_SPOT_DEMO_HTTP_URL, BINANCE_SPOT_DEMO_WS_URL,
27        BINANCE_SPOT_HTTP_URL, BINANCE_SPOT_TESTNET_HTTP_URL, BINANCE_SPOT_TESTNET_WS_URL,
28        BINANCE_SPOT_WS_URL,
29    },
30    enums::{BinanceEnvironment, BinanceProductType},
31};
32
33/// Returns the HTTP base URL for the given product type and environment.
34#[must_use]
35pub fn get_http_base_url(
36    product_type: BinanceProductType,
37    environment: BinanceEnvironment,
38) -> &'static str {
39    match (product_type, environment) {
40        // Mainnet
41        (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Mainnet) => {
42            BINANCE_SPOT_HTTP_URL
43        }
44        (BinanceProductType::UsdM, BinanceEnvironment::Mainnet) => BINANCE_FUTURES_USD_HTTP_URL,
45        (BinanceProductType::CoinM, BinanceEnvironment::Mainnet) => BINANCE_FUTURES_COIN_HTTP_URL,
46        (BinanceProductType::Options, BinanceEnvironment::Mainnet) => BINANCE_OPTIONS_HTTP_URL,
47
48        // Testnet
49        (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Testnet) => {
50            BINANCE_SPOT_TESTNET_HTTP_URL
51        }
52        (BinanceProductType::UsdM, BinanceEnvironment::Testnet) => {
53            BINANCE_FUTURES_USD_TESTNET_HTTP_URL
54        }
55        (BinanceProductType::CoinM, BinanceEnvironment::Testnet) => {
56            BINANCE_FUTURES_COIN_TESTNET_HTTP_URL
57        }
58        (BinanceProductType::Options, BinanceEnvironment::Testnet) => BINANCE_OPTIONS_HTTP_URL,
59
60        // Demo
61        (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Demo) => {
62            BINANCE_SPOT_DEMO_HTTP_URL
63        }
64        (BinanceProductType::UsdM, BinanceEnvironment::Demo) => BINANCE_FUTURES_USD_DEMO_HTTP_URL,
65        (BinanceProductType::CoinM, BinanceEnvironment::Demo) => BINANCE_FUTURES_COIN_DEMO_HTTP_URL,
66        (BinanceProductType::Options, BinanceEnvironment::Demo) => BINANCE_OPTIONS_HTTP_URL,
67    }
68}
69
70/// Returns the WebSocket base URL for the given product type and environment.
71#[must_use]
72pub fn get_ws_base_url(
73    product_type: BinanceProductType,
74    environment: BinanceEnvironment,
75) -> &'static str {
76    match (product_type, environment) {
77        // Mainnet
78        (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Mainnet) => {
79            BINANCE_SPOT_WS_URL
80        }
81        (BinanceProductType::UsdM, BinanceEnvironment::Mainnet) => BINANCE_FUTURES_USD_WS_URL,
82        (BinanceProductType::CoinM, BinanceEnvironment::Mainnet) => BINANCE_FUTURES_COIN_WS_URL,
83        (BinanceProductType::Options, BinanceEnvironment::Mainnet) => BINANCE_OPTIONS_WS_URL,
84
85        // Testnet
86        (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Testnet) => {
87            BINANCE_SPOT_TESTNET_WS_URL
88        }
89        (BinanceProductType::UsdM, BinanceEnvironment::Testnet) => {
90            BINANCE_FUTURES_USD_TESTNET_WS_URL
91        }
92        (BinanceProductType::CoinM, BinanceEnvironment::Testnet) => {
93            BINANCE_FUTURES_COIN_TESTNET_WS_URL
94        }
95        (BinanceProductType::Options, BinanceEnvironment::Testnet) => BINANCE_OPTIONS_WS_URL,
96
97        // Demo (futures demo uses same WS URLs as futures testnet)
98        (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Demo) => {
99            BINANCE_SPOT_DEMO_WS_URL
100        }
101        (BinanceProductType::UsdM, BinanceEnvironment::Demo) => BINANCE_FUTURES_USD_TESTNET_WS_URL,
102        (BinanceProductType::CoinM, BinanceEnvironment::Demo) => {
103            BINANCE_FUTURES_COIN_TESTNET_WS_URL
104        }
105        (BinanceProductType::Options, BinanceEnvironment::Demo) => BINANCE_OPTIONS_WS_URL,
106    }
107}
108
109/// Returns the WebSocket public stream base URL for high-frequency book data.
110///
111/// USD-M mainnet uses the dedicated public endpoint for `@bookTicker` and
112/// `@depth` streams. All other product types and environments fall back to
113/// [`get_ws_base_url`].
114#[must_use]
115pub fn get_ws_public_base_url(
116    product_type: BinanceProductType,
117    environment: BinanceEnvironment,
118) -> &'static str {
119    match (product_type, environment) {
120        (BinanceProductType::UsdM, BinanceEnvironment::Mainnet) => {
121            BINANCE_FUTURES_USD_WS_PUBLIC_URL
122        }
123        _ => get_ws_base_url(product_type, environment),
124    }
125}
126
127/// Returns the WebSocket private stream base URL for user data.
128///
129/// USD-M mainnet uses the dedicated private endpoint. All other
130/// product types and environments fall back to [`get_ws_base_url`].
131#[must_use]
132pub fn get_ws_private_base_url(
133    product_type: BinanceProductType,
134    environment: BinanceEnvironment,
135) -> &'static str {
136    match (product_type, environment) {
137        (BinanceProductType::UsdM, BinanceEnvironment::Mainnet) => {
138            BINANCE_FUTURES_USD_WS_PRIVATE_URL
139        }
140        _ => get_ws_base_url(product_type, environment),
141    }
142}
143
144fn is_usdm_ws_host(base_url: &str) -> bool {
145    // Strip scheme (e.g. `wss://`) and trailing path/port, then match the hostname.
146    // Accepts fstream.binance.com, fstream-mm.binance.com, fstream-auth.binance.com,
147    // and their .us counterparts, without admitting arbitrary substrings.
148    let without_scheme = base_url
149        .split_once("://")
150        .map_or(base_url, |(_, rest)| rest);
151    let host = without_scheme
152        .split(['/', ':'])
153        .next()
154        .unwrap_or(without_scheme);
155    host.starts_with("fstream") && (host.ends_with(".binance.com") || host.ends_with(".binance.us"))
156}
157
158/// Returns a routed USD-M Futures WebSocket URL derived from an override.
159///
160/// Binance now routes USD-M Futures mainnet traffic by category. This helper
161/// accepts either a root override (for example `wss://fstream.binance.com`) or
162/// a routed/transport-specific override such as `/market`, `/public/ws`, or
163/// `/private/stream`, then rebuilds the URL for the requested route.
164///
165/// URLs that do not point at `fstream.binance.com` (for example local test
166/// endpoints) are returned unchanged.
167#[must_use]
168pub(crate) fn get_usdm_ws_route_base_url(base_url: &str, route: &str) -> String {
169    const SUFFIXES: [&str; 11] = [
170        "/market/ws",
171        "/market/stream",
172        "/public/ws",
173        "/public/stream",
174        "/private/ws",
175        "/private/stream",
176        "/market",
177        "/public",
178        "/private",
179        "/ws",
180        "/stream",
181    ];
182
183    assert!(
184        matches!(route, "market" | "public" | "private"),
185        "invalid USD-M WebSocket route: {route}"
186    );
187
188    if !is_usdm_ws_host(base_url) {
189        return base_url.to_string();
190    }
191
192    let mut normalized = base_url.trim_end_matches('/').to_string();
193
194    for suffix in SUFFIXES {
195        if normalized.ends_with(suffix) {
196            normalized.truncate(normalized.len() - suffix.len());
197            break;
198        }
199    }
200
201    format!("{normalized}/{route}/ws")
202}
203
204#[cfg(test)]
205mod tests {
206    use rstest::rstest;
207
208    use super::*;
209
210    #[rstest]
211    fn test_http_url_spot_mainnet() {
212        let url = get_http_base_url(BinanceProductType::Spot, BinanceEnvironment::Mainnet);
213        assert_eq!(url, "https://api.binance.com");
214    }
215
216    #[rstest]
217    fn test_http_url_spot_testnet() {
218        let url = get_http_base_url(BinanceProductType::Spot, BinanceEnvironment::Testnet);
219        assert_eq!(url, "https://testnet.binance.vision");
220    }
221
222    #[rstest]
223    fn test_http_url_spot_demo() {
224        let url = get_http_base_url(BinanceProductType::Spot, BinanceEnvironment::Demo);
225        assert_eq!(url, "https://demo-api.binance.com");
226    }
227
228    #[rstest]
229    fn test_http_url_usdm_mainnet() {
230        let url = get_http_base_url(BinanceProductType::UsdM, BinanceEnvironment::Mainnet);
231        assert_eq!(url, "https://fapi.binance.com");
232    }
233
234    #[rstest]
235    fn test_http_url_usdm_testnet() {
236        let url = get_http_base_url(BinanceProductType::UsdM, BinanceEnvironment::Testnet);
237        assert_eq!(url, "https://demo-fapi.binance.com");
238    }
239
240    #[rstest]
241    fn test_http_url_coinm_mainnet() {
242        let url = get_http_base_url(BinanceProductType::CoinM, BinanceEnvironment::Mainnet);
243        assert_eq!(url, "https://dapi.binance.com");
244    }
245
246    #[rstest]
247    fn test_http_url_usdm_demo() {
248        let url = get_http_base_url(BinanceProductType::UsdM, BinanceEnvironment::Demo);
249        assert_eq!(url, "https://demo-fapi.binance.com");
250    }
251
252    #[rstest]
253    fn test_http_url_coinm_demo() {
254        let url = get_http_base_url(BinanceProductType::CoinM, BinanceEnvironment::Demo);
255        assert_eq!(url, "https://testnet.binancefuture.com");
256    }
257
258    #[rstest]
259    fn test_ws_url_spot_mainnet() {
260        let url = get_ws_base_url(BinanceProductType::Spot, BinanceEnvironment::Mainnet);
261        assert_eq!(url, "wss://stream.binance.com:9443/ws");
262    }
263
264    #[rstest]
265    fn test_ws_url_spot_demo() {
266        let url = get_ws_base_url(BinanceProductType::Spot, BinanceEnvironment::Demo);
267        assert_eq!(url, "wss://demo-stream.binance.com/ws");
268    }
269
270    #[rstest]
271    fn test_ws_url_usdm_mainnet() {
272        let url = get_ws_base_url(BinanceProductType::UsdM, BinanceEnvironment::Mainnet);
273        assert_eq!(url, "wss://fstream.binance.com/market/ws");
274    }
275
276    #[rstest]
277    fn test_ws_url_usdm_testnet() {
278        let url = get_ws_base_url(BinanceProductType::UsdM, BinanceEnvironment::Testnet);
279        assert_eq!(url, "wss://fstream.binancefuture.com/ws");
280    }
281
282    #[rstest]
283    fn test_ws_private_url_usdm_mainnet() {
284        let url = get_ws_private_base_url(BinanceProductType::UsdM, BinanceEnvironment::Mainnet);
285        assert_eq!(url, "wss://fstream.binance.com/private/ws");
286    }
287
288    #[rstest]
289    fn test_ws_private_url_fallback_to_market() {
290        let url = get_ws_private_base_url(BinanceProductType::Spot, BinanceEnvironment::Mainnet);
291        assert_eq!(
292            url,
293            get_ws_base_url(BinanceProductType::Spot, BinanceEnvironment::Mainnet)
294        );
295    }
296
297    #[rstest]
298    fn test_ws_public_url_usdm_mainnet() {
299        let url = get_ws_public_base_url(BinanceProductType::UsdM, BinanceEnvironment::Mainnet);
300        assert_eq!(url, "wss://fstream.binance.com/public/ws");
301    }
302
303    #[rstest]
304    fn test_ws_public_url_fallback_to_market() {
305        let url = get_ws_public_base_url(BinanceProductType::Spot, BinanceEnvironment::Mainnet);
306        assert_eq!(
307            url,
308            get_ws_base_url(BinanceProductType::Spot, BinanceEnvironment::Mainnet)
309        );
310    }
311
312    #[rstest]
313    #[case(
314        "wss://fstream.binance.com",
315        "market",
316        "wss://fstream.binance.com/market/ws"
317    )]
318    #[case(
319        "wss://fstream.binance.com/ws",
320        "public",
321        "wss://fstream.binance.com/public/ws"
322    )]
323    #[case(
324        "wss://fstream.binance.com/market/ws",
325        "private",
326        "wss://fstream.binance.com/private/ws"
327    )]
328    #[case(
329        "wss://fstream-mm.binance.com",
330        "market",
331        "wss://fstream-mm.binance.com/market/ws"
332    )]
333    #[case(
334        "wss://fstream-mm.binance.com/ws",
335        "public",
336        "wss://fstream-mm.binance.com/public/ws"
337    )]
338    #[case(
339        "wss://fstream-auth.binance.com/market/ws",
340        "private",
341        "wss://fstream-auth.binance.com/private/ws"
342    )]
343    #[case(
344        "wss://fstream.binance.us",
345        "market",
346        "wss://fstream.binance.us/market/ws"
347    )]
348    fn test_usdm_ws_route_base_url_normalizes_override(
349        #[case] base_url: &str,
350        #[case] route: &str,
351        #[case] expected: &str,
352    ) {
353        let url = get_usdm_ws_route_base_url(base_url, route);
354        assert_eq!(url, expected);
355    }
356
357    #[rstest]
358    #[case("ws://127.0.0.1:9999/ws", "market")]
359    #[case("wss://other.example.com/private/ws", "private")]
360    #[case("ws://localhost:8080", "public")]
361    #[case("wss://other-fstream.binance.com.example.org/ws", "market")]
362    #[case("wss://fstream.binance.com.example.org/ws", "market")]
363    fn test_usdm_ws_route_base_url_passes_through_non_binance_host(
364        #[case] base_url: &str,
365        #[case] route: &str,
366    ) {
367        let url = get_usdm_ws_route_base_url(base_url, route);
368        assert_eq!(url, base_url);
369    }
370}