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_DEMO_WS_URL,
21        BINANCE_FUTURES_COIN_HTTP_URL, BINANCE_FUTURES_COIN_TESTNET_HTTP_URL,
22        BINANCE_FUTURES_COIN_TESTNET_WS_URL, BINANCE_FUTURES_COIN_WS_URL,
23        BINANCE_FUTURES_USD_DEMO_HTTP_URL, BINANCE_FUTURES_USD_DEMO_WS_URL,
24        BINANCE_FUTURES_USD_HTTP_URL, BINANCE_FUTURES_USD_TESTNET_HTTP_URL,
25        BINANCE_FUTURES_USD_TESTNET_WS_URL, BINANCE_FUTURES_USD_WS_PRIVATE_URL,
26        BINANCE_FUTURES_USD_WS_PUBLIC_URL, BINANCE_FUTURES_USD_WS_URL, BINANCE_OPTIONS_HTTP_URL,
27        BINANCE_OPTIONS_TESTNET_HTTP_URL, BINANCE_OPTIONS_TESTNET_WS_PRIVATE_URL,
28        BINANCE_OPTIONS_TESTNET_WS_PUBLIC_URL, BINANCE_OPTIONS_TESTNET_WS_URL,
29        BINANCE_OPTIONS_WS_URL, BINANCE_SPOT_DEMO_HTTP_URL, BINANCE_SPOT_DEMO_WS_URL,
30        BINANCE_SPOT_HTTP_URL, BINANCE_SPOT_TESTNET_HTTP_URL, BINANCE_SPOT_TESTNET_WS_URL,
31        BINANCE_SPOT_WS_URL,
32    },
33    enums::{BinanceEnvironment, BinanceProductType},
34};
35
36/// Returns the HTTP base URL for the given product type and environment.
37#[must_use]
38pub fn get_http_base_url(
39    product_type: BinanceProductType,
40    environment: BinanceEnvironment,
41) -> &'static str {
42    match (product_type, environment) {
43        // Live
44        (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Live) => {
45            BINANCE_SPOT_HTTP_URL
46        }
47        (BinanceProductType::UsdM, BinanceEnvironment::Live) => BINANCE_FUTURES_USD_HTTP_URL,
48        (BinanceProductType::CoinM, BinanceEnvironment::Live) => BINANCE_FUTURES_COIN_HTTP_URL,
49        (BinanceProductType::Options, BinanceEnvironment::Live) => BINANCE_OPTIONS_HTTP_URL,
50
51        // Testnet
52        (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Testnet) => {
53            BINANCE_SPOT_TESTNET_HTTP_URL
54        }
55        (BinanceProductType::UsdM, BinanceEnvironment::Testnet) => {
56            BINANCE_FUTURES_USD_TESTNET_HTTP_URL
57        }
58        (BinanceProductType::CoinM, BinanceEnvironment::Testnet) => {
59            BINANCE_FUTURES_COIN_TESTNET_HTTP_URL
60        }
61        (BinanceProductType::Options, BinanceEnvironment::Testnet) => {
62            BINANCE_OPTIONS_TESTNET_HTTP_URL
63        }
64
65        // Demo
66        (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Demo) => {
67            BINANCE_SPOT_DEMO_HTTP_URL
68        }
69        (BinanceProductType::UsdM, BinanceEnvironment::Demo) => BINANCE_FUTURES_USD_DEMO_HTTP_URL,
70        (BinanceProductType::CoinM, BinanceEnvironment::Demo) => BINANCE_FUTURES_COIN_DEMO_HTTP_URL,
71        (BinanceProductType::Options, BinanceEnvironment::Demo) => BINANCE_OPTIONS_TESTNET_HTTP_URL,
72    }
73}
74
75/// Returns the WebSocket base URL for the given product type and environment.
76#[must_use]
77pub fn get_ws_base_url(
78    product_type: BinanceProductType,
79    environment: BinanceEnvironment,
80) -> &'static str {
81    match (product_type, environment) {
82        // Live
83        (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Live) => {
84            BINANCE_SPOT_WS_URL
85        }
86        (BinanceProductType::UsdM, BinanceEnvironment::Live) => BINANCE_FUTURES_USD_WS_URL,
87        (BinanceProductType::CoinM, BinanceEnvironment::Live) => BINANCE_FUTURES_COIN_WS_URL,
88        (BinanceProductType::Options, BinanceEnvironment::Live) => BINANCE_OPTIONS_WS_URL,
89
90        // Testnet
91        (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Testnet) => {
92            BINANCE_SPOT_TESTNET_WS_URL
93        }
94        (BinanceProductType::UsdM, BinanceEnvironment::Testnet) => {
95            BINANCE_FUTURES_USD_TESTNET_WS_URL
96        }
97        (BinanceProductType::CoinM, BinanceEnvironment::Testnet) => {
98            BINANCE_FUTURES_COIN_TESTNET_WS_URL
99        }
100        (BinanceProductType::Options, BinanceEnvironment::Testnet) => {
101            BINANCE_OPTIONS_TESTNET_WS_URL
102        }
103
104        // Demo
105        (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Demo) => {
106            BINANCE_SPOT_DEMO_WS_URL
107        }
108        (BinanceProductType::UsdM, BinanceEnvironment::Demo) => BINANCE_FUTURES_USD_DEMO_WS_URL,
109        (BinanceProductType::CoinM, BinanceEnvironment::Demo) => BINANCE_FUTURES_COIN_DEMO_WS_URL,
110        (BinanceProductType::Options, BinanceEnvironment::Demo) => BINANCE_OPTIONS_TESTNET_WS_URL,
111    }
112}
113
114/// Returns the WebSocket public stream base URL for high-frequency book data.
115///
116/// USD-M live exchange uses the dedicated public endpoint for `@bookTicker` and
117/// `@depth` streams. All other product types and environments fall back to
118/// [`get_ws_base_url`].
119#[must_use]
120pub fn get_ws_public_base_url(
121    product_type: BinanceProductType,
122    environment: BinanceEnvironment,
123) -> &'static str {
124    match (product_type, environment) {
125        (BinanceProductType::UsdM, BinanceEnvironment::Live) => BINANCE_FUTURES_USD_WS_PUBLIC_URL,
126        (BinanceProductType::Options, BinanceEnvironment::Testnet | BinanceEnvironment::Demo) => {
127            BINANCE_OPTIONS_TESTNET_WS_PUBLIC_URL
128        }
129        _ => get_ws_base_url(product_type, environment),
130    }
131}
132
133/// Returns the WebSocket private stream base URL for user data.
134///
135/// USD-M live exchange uses the dedicated private endpoint. All other
136/// product types and environments fall back to [`get_ws_base_url`].
137#[must_use]
138pub fn get_ws_private_base_url(
139    product_type: BinanceProductType,
140    environment: BinanceEnvironment,
141) -> &'static str {
142    match (product_type, environment) {
143        (BinanceProductType::UsdM, BinanceEnvironment::Live) => BINANCE_FUTURES_USD_WS_PRIVATE_URL,
144        (BinanceProductType::Options, BinanceEnvironment::Testnet | BinanceEnvironment::Demo) => {
145            BINANCE_OPTIONS_TESTNET_WS_PRIVATE_URL
146        }
147        _ => get_ws_base_url(product_type, environment),
148    }
149}
150
151fn is_usdm_ws_host(base_url: &str) -> bool {
152    // Strip scheme (e.g. `wss://`) and trailing path/port, then match the hostname.
153    // Accepts fstream.binance.com, fstream-mm.binance.com, fstream-auth.binance.com,
154    // and their .us counterparts, without admitting arbitrary substrings.
155    let without_scheme = base_url
156        .split_once("://")
157        .map_or(base_url, |(_, rest)| rest);
158    let host = without_scheme
159        .split(['/', ':'])
160        .next()
161        .unwrap_or(without_scheme);
162    host.starts_with("fstream") && (host.ends_with(".binance.com") || host.ends_with(".binance.us"))
163}
164
165/// Returns a routed USD-M Futures WebSocket URL derived from an override.
166///
167/// Binance now routes USD-M Futures live traffic by category. This helper
168/// accepts either a root override (for example `wss://fstream.binance.com`) or
169/// a routed/transport-specific override such as `/market`, `/public/ws`, or
170/// `/private/stream`, then rebuilds the URL for the requested route.
171///
172/// URLs that do not point at `fstream.binance.com` (for example local test
173/// endpoints) are returned unchanged.
174#[must_use]
175pub(crate) fn get_usdm_ws_route_base_url(base_url: &str, route: &str) -> String {
176    const SUFFIXES: [&str; 11] = [
177        "/market/ws",
178        "/market/stream",
179        "/public/ws",
180        "/public/stream",
181        "/private/ws",
182        "/private/stream",
183        "/market",
184        "/public",
185        "/private",
186        "/ws",
187        "/stream",
188    ];
189
190    assert!(
191        matches!(route, "market" | "public" | "private"),
192        "invalid USD-M WebSocket route: {route}"
193    );
194
195    if !is_usdm_ws_host(base_url) {
196        return base_url.to_string();
197    }
198
199    let mut normalized = base_url.trim_end_matches('/').to_string();
200
201    for suffix in SUFFIXES {
202        if normalized.ends_with(suffix) {
203            normalized.truncate(normalized.len() - suffix.len());
204            break;
205        }
206    }
207
208    format!("{normalized}/{route}/ws")
209}
210
211#[cfg(test)]
212mod tests {
213    use rstest::rstest;
214
215    use super::*;
216
217    #[rstest]
218    fn test_http_url_spot_live() {
219        let url = get_http_base_url(BinanceProductType::Spot, BinanceEnvironment::Live);
220        assert_eq!(url, "https://api.binance.com");
221    }
222
223    #[rstest]
224    fn test_http_url_spot_testnet() {
225        let url = get_http_base_url(BinanceProductType::Spot, BinanceEnvironment::Testnet);
226        assert_eq!(url, "https://testnet.binance.vision");
227    }
228
229    #[rstest]
230    fn test_http_url_spot_demo() {
231        let url = get_http_base_url(BinanceProductType::Spot, BinanceEnvironment::Demo);
232        assert_eq!(url, "https://demo-api.binance.com");
233    }
234
235    #[rstest]
236    fn test_http_url_usdm_live() {
237        let url = get_http_base_url(BinanceProductType::UsdM, BinanceEnvironment::Live);
238        assert_eq!(url, "https://fapi.binance.com");
239    }
240
241    #[rstest]
242    fn test_http_url_usdm_testnet() {
243        let url = get_http_base_url(BinanceProductType::UsdM, BinanceEnvironment::Testnet);
244        assert_eq!(url, "https://demo-fapi.binance.com");
245    }
246
247    #[rstest]
248    fn test_http_url_coinm_live() {
249        let url = get_http_base_url(BinanceProductType::CoinM, BinanceEnvironment::Live);
250        assert_eq!(url, "https://dapi.binance.com");
251    }
252
253    #[rstest]
254    fn test_http_url_usdm_demo() {
255        let url = get_http_base_url(BinanceProductType::UsdM, BinanceEnvironment::Demo);
256        assert_eq!(url, "https://demo-fapi.binance.com");
257    }
258
259    #[rstest]
260    fn test_http_url_coinm_demo() {
261        let url = get_http_base_url(BinanceProductType::CoinM, BinanceEnvironment::Demo);
262        assert_eq!(url, "https://demo-dapi.binance.com");
263    }
264
265    #[rstest]
266    fn test_http_url_options_testnet() {
267        let url = get_http_base_url(BinanceProductType::Options, BinanceEnvironment::Testnet);
268        assert_eq!(url, "https://testnet.binancefuture.com");
269    }
270
271    #[rstest]
272    fn test_http_url_options_demo() {
273        let url = get_http_base_url(BinanceProductType::Options, BinanceEnvironment::Demo);
274        assert_eq!(url, "https://testnet.binancefuture.com");
275    }
276
277    #[rstest]
278    fn test_ws_url_spot_live() {
279        let url = get_ws_base_url(BinanceProductType::Spot, BinanceEnvironment::Live);
280        assert_eq!(url, "wss://stream.binance.com:9443/ws");
281    }
282
283    #[rstest]
284    fn test_ws_url_spot_demo() {
285        let url = get_ws_base_url(BinanceProductType::Spot, BinanceEnvironment::Demo);
286        assert_eq!(url, "wss://demo-stream.binance.com/ws");
287    }
288
289    #[rstest]
290    fn test_ws_url_usdm_live() {
291        let url = get_ws_base_url(BinanceProductType::UsdM, BinanceEnvironment::Live);
292        assert_eq!(url, "wss://fstream.binance.com/market/ws");
293    }
294
295    #[rstest]
296    fn test_ws_url_usdm_testnet() {
297        let url = get_ws_base_url(BinanceProductType::UsdM, BinanceEnvironment::Testnet);
298        assert_eq!(url, "wss://fstream.binancefuture.com/ws");
299    }
300
301    #[rstest]
302    fn test_ws_url_usdm_demo() {
303        let url = get_ws_base_url(BinanceProductType::UsdM, BinanceEnvironment::Demo);
304        assert_eq!(url, "wss://demo-fstream.binance.com/ws");
305    }
306
307    #[rstest]
308    fn test_ws_url_coinm_demo() {
309        let url = get_ws_base_url(BinanceProductType::CoinM, BinanceEnvironment::Demo);
310        assert_eq!(url, "wss://demo-dstream.binance.com/ws");
311    }
312
313    #[rstest]
314    fn test_ws_url_options_testnet() {
315        let url = get_ws_base_url(BinanceProductType::Options, BinanceEnvironment::Testnet);
316        assert_eq!(url, "wss://fstream.binancefuture.com/market/ws");
317    }
318
319    #[rstest]
320    fn test_ws_url_options_demo() {
321        let url = get_ws_base_url(BinanceProductType::Options, BinanceEnvironment::Demo);
322        assert_eq!(url, "wss://fstream.binancefuture.com/market/ws");
323    }
324
325    #[rstest]
326    fn test_ws_private_url_usdm_live() {
327        let url = get_ws_private_base_url(BinanceProductType::UsdM, BinanceEnvironment::Live);
328        assert_eq!(url, "wss://fstream.binance.com/private/ws");
329    }
330
331    #[rstest]
332    fn test_ws_private_url_fallback_to_market() {
333        let url = get_ws_private_base_url(BinanceProductType::Spot, BinanceEnvironment::Live);
334        assert_eq!(
335            url,
336            get_ws_base_url(BinanceProductType::Spot, BinanceEnvironment::Live)
337        );
338    }
339
340    #[rstest]
341    fn test_ws_public_url_usdm_live() {
342        let url = get_ws_public_base_url(BinanceProductType::UsdM, BinanceEnvironment::Live);
343        assert_eq!(url, "wss://fstream.binance.com/public/ws");
344    }
345
346    #[rstest]
347    fn test_ws_public_url_options_demo() {
348        let url = get_ws_public_base_url(BinanceProductType::Options, BinanceEnvironment::Demo);
349        assert_eq!(url, "wss://fstream.binancefuture.com/public/ws");
350    }
351
352    #[rstest]
353    fn test_ws_private_url_options_demo() {
354        let url = get_ws_private_base_url(BinanceProductType::Options, BinanceEnvironment::Demo);
355        assert_eq!(url, "wss://fstream.binancefuture.com/private/ws");
356    }
357
358    #[rstest]
359    fn test_ws_public_url_fallback_to_market() {
360        let url = get_ws_public_base_url(BinanceProductType::Spot, BinanceEnvironment::Live);
361        assert_eq!(
362            url,
363            get_ws_base_url(BinanceProductType::Spot, BinanceEnvironment::Live)
364        );
365    }
366
367    #[rstest]
368    #[case(
369        "wss://fstream.binance.com",
370        "market",
371        "wss://fstream.binance.com/market/ws"
372    )]
373    #[case(
374        "wss://fstream.binance.com/ws",
375        "public",
376        "wss://fstream.binance.com/public/ws"
377    )]
378    #[case(
379        "wss://fstream.binance.com/market/ws",
380        "private",
381        "wss://fstream.binance.com/private/ws"
382    )]
383    #[case(
384        "wss://fstream-mm.binance.com",
385        "market",
386        "wss://fstream-mm.binance.com/market/ws"
387    )]
388    #[case(
389        "wss://fstream-mm.binance.com/ws",
390        "public",
391        "wss://fstream-mm.binance.com/public/ws"
392    )]
393    #[case(
394        "wss://fstream-auth.binance.com/market/ws",
395        "private",
396        "wss://fstream-auth.binance.com/private/ws"
397    )]
398    #[case(
399        "wss://fstream.binance.us",
400        "market",
401        "wss://fstream.binance.us/market/ws"
402    )]
403    fn test_usdm_ws_route_base_url_normalizes_override(
404        #[case] base_url: &str,
405        #[case] route: &str,
406        #[case] expected: &str,
407    ) {
408        let url = get_usdm_ws_route_base_url(base_url, route);
409        assert_eq!(url, expected);
410    }
411
412    #[rstest]
413    #[case("ws://127.0.0.1:9999/ws", "market")]
414    #[case("wss://other.example.com/private/ws", "private")]
415    #[case("ws://localhost:8080", "public")]
416    #[case("wss://other-fstream.binance.com.example.org/ws", "market")]
417    #[case("wss://fstream.binance.com.example.org/ws", "market")]
418    fn test_usdm_ws_route_base_url_passes_through_non_binance_host(
419        #[case] base_url: &str,
420        #[case] route: &str,
421    ) {
422        let url = get_usdm_ws_route_base_url(base_url, route);
423        assert_eq!(url, base_url);
424    }
425}