1use 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#[must_use]
38pub fn get_http_base_url(
39 product_type: BinanceProductType,
40 environment: BinanceEnvironment,
41) -> &'static str {
42 match (product_type, environment) {
43 (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 (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 (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#[must_use]
77pub fn get_ws_base_url(
78 product_type: BinanceProductType,
79 environment: BinanceEnvironment,
80) -> &'static str {
81 match (product_type, environment) {
82 (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 (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 (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#[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#[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 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#[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}