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, BINANCE_US_SPOT_HTTP_URL, BINANCE_US_SPOT_USER_WS_URL,
32 BINANCE_US_SPOT_WS_URL,
33 },
34 enums::{BinanceEnvironment, BinanceProductType},
35};
36
37#[must_use]
39pub fn get_http_base_url(
40 product_type: BinanceProductType,
41 environment: BinanceEnvironment,
42) -> &'static str {
43 match (product_type, environment) {
44 (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Live) => {
46 BINANCE_SPOT_HTTP_URL
47 }
48 (BinanceProductType::UsdM, BinanceEnvironment::Live) => BINANCE_FUTURES_USD_HTTP_URL,
49 (BinanceProductType::CoinM, BinanceEnvironment::Live) => BINANCE_FUTURES_COIN_HTTP_URL,
50 (BinanceProductType::Options, BinanceEnvironment::Live) => BINANCE_OPTIONS_HTTP_URL,
51
52 (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Testnet) => {
54 BINANCE_SPOT_TESTNET_HTTP_URL
55 }
56 (BinanceProductType::UsdM, BinanceEnvironment::Testnet) => {
57 BINANCE_FUTURES_USD_TESTNET_HTTP_URL
58 }
59 (BinanceProductType::CoinM, BinanceEnvironment::Testnet) => {
60 BINANCE_FUTURES_COIN_TESTNET_HTTP_URL
61 }
62 (BinanceProductType::Options, BinanceEnvironment::Testnet) => {
63 BINANCE_OPTIONS_TESTNET_HTTP_URL
64 }
65
66 (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Demo) => {
68 BINANCE_SPOT_DEMO_HTTP_URL
69 }
70 (BinanceProductType::UsdM, BinanceEnvironment::Demo) => BINANCE_FUTURES_USD_DEMO_HTTP_URL,
71 (BinanceProductType::CoinM, BinanceEnvironment::Demo) => BINANCE_FUTURES_COIN_DEMO_HTTP_URL,
72 (BinanceProductType::Options, BinanceEnvironment::Demo) => BINANCE_OPTIONS_TESTNET_HTTP_URL,
73 }
74}
75
76#[must_use]
78pub fn get_http_base_url_with_us(
79 product_type: BinanceProductType,
80 environment: BinanceEnvironment,
81 us: bool,
82) -> &'static str {
83 if us {
84 BINANCE_US_SPOT_HTTP_URL
85 } else {
86 get_http_base_url(product_type, environment)
87 }
88}
89
90#[must_use]
96pub fn get_sapi_base_url(environment: BinanceEnvironment) -> Option<&'static str> {
97 match environment {
98 BinanceEnvironment::Live => Some(BINANCE_SPOT_HTTP_URL),
99 BinanceEnvironment::Testnet | BinanceEnvironment::Demo => None,
100 }
101}
102
103#[must_use]
105pub fn get_ws_base_url(
106 product_type: BinanceProductType,
107 environment: BinanceEnvironment,
108) -> &'static str {
109 match (product_type, environment) {
110 (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Live) => {
112 BINANCE_SPOT_WS_URL
113 }
114 (BinanceProductType::UsdM, BinanceEnvironment::Live) => BINANCE_FUTURES_USD_WS_URL,
115 (BinanceProductType::CoinM, BinanceEnvironment::Live) => BINANCE_FUTURES_COIN_WS_URL,
116 (BinanceProductType::Options, BinanceEnvironment::Live) => BINANCE_OPTIONS_WS_URL,
117
118 (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Testnet) => {
120 BINANCE_SPOT_TESTNET_WS_URL
121 }
122 (BinanceProductType::UsdM, BinanceEnvironment::Testnet) => {
123 BINANCE_FUTURES_USD_TESTNET_WS_URL
124 }
125 (BinanceProductType::CoinM, BinanceEnvironment::Testnet) => {
126 BINANCE_FUTURES_COIN_TESTNET_WS_URL
127 }
128 (BinanceProductType::Options, BinanceEnvironment::Testnet) => {
129 BINANCE_OPTIONS_TESTNET_WS_URL
130 }
131
132 (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Demo) => {
134 BINANCE_SPOT_DEMO_WS_URL
135 }
136 (BinanceProductType::UsdM, BinanceEnvironment::Demo) => BINANCE_FUTURES_USD_DEMO_WS_URL,
137 (BinanceProductType::CoinM, BinanceEnvironment::Demo) => BINANCE_FUTURES_COIN_DEMO_WS_URL,
138 (BinanceProductType::Options, BinanceEnvironment::Demo) => BINANCE_OPTIONS_TESTNET_WS_URL,
139 }
140}
141
142#[must_use]
144pub fn get_ws_base_url_with_us(
145 product_type: BinanceProductType,
146 environment: BinanceEnvironment,
147 us: bool,
148) -> &'static str {
149 if us {
150 BINANCE_US_SPOT_WS_URL
151 } else {
152 get_ws_base_url(product_type, environment)
153 }
154}
155
156#[must_use]
158pub(crate) fn get_spot_user_stream_url(base_url: Option<&str>, listen_key: &str) -> String {
159 let base_url = base_url.unwrap_or(BINANCE_US_SPOT_USER_WS_URL);
160 let normalized = base_url.trim_end_matches('/');
161 if normalized.ends_with("/ws") {
162 format!("{normalized}/{listen_key}")
163 } else {
164 format!("{normalized}/ws/{listen_key}")
165 }
166}
167
168#[must_use]
174pub fn get_ws_public_base_url(
175 product_type: BinanceProductType,
176 environment: BinanceEnvironment,
177) -> &'static str {
178 match (product_type, environment) {
179 (BinanceProductType::UsdM, BinanceEnvironment::Live) => BINANCE_FUTURES_USD_WS_PUBLIC_URL,
180 (BinanceProductType::Options, BinanceEnvironment::Testnet | BinanceEnvironment::Demo) => {
181 BINANCE_OPTIONS_TESTNET_WS_PUBLIC_URL
182 }
183 _ => get_ws_base_url(product_type, environment),
184 }
185}
186
187#[must_use]
192pub fn get_ws_private_base_url(
193 product_type: BinanceProductType,
194 environment: BinanceEnvironment,
195) -> &'static str {
196 match (product_type, environment) {
197 (BinanceProductType::UsdM, BinanceEnvironment::Live) => BINANCE_FUTURES_USD_WS_PRIVATE_URL,
198 (BinanceProductType::Options, BinanceEnvironment::Testnet | BinanceEnvironment::Demo) => {
199 BINANCE_OPTIONS_TESTNET_WS_PRIVATE_URL
200 }
201 _ => get_ws_base_url(product_type, environment),
202 }
203}
204
205#[must_use]
207pub(crate) fn get_futures_user_stream_url(
208 product_type: BinanceProductType,
209 base_url: &str,
210 listen_key: &str,
211) -> String {
212 assert!(
213 matches!(
214 product_type,
215 BinanceProductType::UsdM | BinanceProductType::CoinM
216 ),
217 "Futures user stream requires UsdM or CoinM product type, was {product_type:?}"
218 );
219
220 let mut normalized = base_url.trim_end_matches('/').to_string();
221 let path = normalized
222 .split_once("://")
223 .map_or(normalized.as_str(), |(_, rest)| rest)
224 .split_once('/')
225 .map(|(_, path)| path);
226 if matches!(path, None | Some("" | "private")) {
227 normalized.push_str("/ws");
228 }
229
230 match product_type {
231 BinanceProductType::UsdM => format!("{normalized}?listenKey={listen_key}"),
232 BinanceProductType::CoinM => format!("{normalized}/{listen_key}"),
233 _ => unreachable!(),
234 }
235}
236
237fn is_usdm_ws_host(base_url: &str) -> bool {
238 let without_scheme = base_url
242 .split_once("://")
243 .map_or(base_url, |(_, rest)| rest);
244 let host = without_scheme
245 .split(['/', ':'])
246 .next()
247 .unwrap_or(without_scheme);
248 host.starts_with("fstream") && (host.ends_with(".binance.com") || host.ends_with(".binance.us"))
249}
250
251#[must_use]
261pub(crate) fn get_usdm_ws_route_base_url(base_url: &str, route: &str) -> String {
262 const SUFFIXES: [&str; 11] = [
263 "/market/ws",
264 "/market/stream",
265 "/public/ws",
266 "/public/stream",
267 "/private/ws",
268 "/private/stream",
269 "/market",
270 "/public",
271 "/private",
272 "/ws",
273 "/stream",
274 ];
275
276 assert!(
277 matches!(route, "market" | "public" | "private"),
278 "invalid USD-M WebSocket route: {route}"
279 );
280
281 if !is_usdm_ws_host(base_url) {
282 return base_url.to_string();
283 }
284
285 let mut normalized = base_url.trim_end_matches('/').to_string();
286
287 for suffix in SUFFIXES {
288 if normalized.ends_with(suffix) {
289 normalized.truncate(normalized.len() - suffix.len());
290 break;
291 }
292 }
293
294 format!("{normalized}/{route}/ws")
295}
296
297#[cfg(test)]
298mod tests {
299 use rstest::rstest;
300
301 use super::*;
302
303 #[rstest]
304 fn test_http_url_spot_live() {
305 let url = get_http_base_url(BinanceProductType::Spot, BinanceEnvironment::Live);
306 assert_eq!(url, "https://api.binance.com");
307 }
308
309 #[rstest]
310 fn test_binance_us_spot_urls() {
311 let http =
312 get_http_base_url_with_us(BinanceProductType::Spot, BinanceEnvironment::Live, true);
313 let public_ws =
314 get_ws_base_url_with_us(BinanceProductType::Spot, BinanceEnvironment::Live, true);
315 let user_ws = get_spot_user_stream_url(None, "listen-key");
316
317 assert_eq!(http, "https://api.binance.us");
318 assert_eq!(public_ws, "wss://stream.binance.us:9443/ws");
319 assert_eq!(user_ws, "wss://stream.binance.us:443/ws/listen-key");
320 }
321
322 #[rstest]
323 #[case("wss://custom.example/ws", "wss://custom.example/ws/listen-key")]
324 #[case("wss://custom.example", "wss://custom.example/ws/listen-key")]
325 #[case("wss://custom.example/ws/", "wss://custom.example/ws/listen-key")]
326 fn test_spot_user_stream_url_override(#[case] base: &str, #[case] expected: &str) {
327 assert_eq!(get_spot_user_stream_url(Some(base), "listen-key"), expected);
328 }
329
330 #[rstest]
331 fn test_http_url_spot_testnet() {
332 let url = get_http_base_url(BinanceProductType::Spot, BinanceEnvironment::Testnet);
333 assert_eq!(url, "https://testnet.binance.vision");
334 }
335
336 #[rstest]
337 fn test_http_url_spot_demo() {
338 let url = get_http_base_url(BinanceProductType::Spot, BinanceEnvironment::Demo);
339 assert_eq!(url, "https://demo-api.binance.com");
340 }
341
342 #[rstest]
343 fn test_http_url_usdm_live() {
344 let url = get_http_base_url(BinanceProductType::UsdM, BinanceEnvironment::Live);
345 assert_eq!(url, "https://fapi.binance.com");
346 }
347
348 #[rstest]
349 fn test_urls_usdm_testnet() {
350 let http_url = get_http_base_url(BinanceProductType::UsdM, BinanceEnvironment::Testnet);
351 let ws_url = get_ws_base_url(BinanceProductType::UsdM, BinanceEnvironment::Testnet);
352
353 assert_eq!(http_url, "https://testnet.binancefuture.com");
354 assert_eq!(ws_url, "wss://fstream.binancefuture.com/ws");
355 }
356
357 #[rstest]
358 fn test_http_url_coinm_live() {
359 let url = get_http_base_url(BinanceProductType::CoinM, BinanceEnvironment::Live);
360 assert_eq!(url, "https://dapi.binance.com");
361 }
362
363 #[rstest]
364 fn test_urls_usdm_demo() {
365 let http_url = get_http_base_url(BinanceProductType::UsdM, BinanceEnvironment::Demo);
366 let ws_url = get_ws_base_url(BinanceProductType::UsdM, BinanceEnvironment::Demo);
367
368 assert_eq!(http_url, "https://demo-fapi.binance.com");
369 assert_eq!(ws_url, "wss://demo-fstream.binance.com/ws");
370 }
371
372 #[rstest]
373 fn test_http_url_coinm_demo() {
374 let url = get_http_base_url(BinanceProductType::CoinM, BinanceEnvironment::Demo);
375 assert_eq!(url, "https://demo-dapi.binance.com");
376 }
377
378 #[rstest]
379 fn test_http_url_options_testnet() {
380 let url = get_http_base_url(BinanceProductType::Options, BinanceEnvironment::Testnet);
381 assert_eq!(url, "https://testnet.binancefuture.com");
382 }
383
384 #[rstest]
385 fn test_http_url_options_demo() {
386 let url = get_http_base_url(BinanceProductType::Options, BinanceEnvironment::Demo);
387 assert_eq!(url, "https://testnet.binancefuture.com");
388 }
389
390 #[rstest]
391 fn test_ws_url_spot_live() {
392 let url = get_ws_base_url(BinanceProductType::Spot, BinanceEnvironment::Live);
393 assert_eq!(url, "wss://stream.binance.com:9443/ws");
394 }
395
396 #[rstest]
397 fn test_ws_url_spot_demo() {
398 let url = get_ws_base_url(BinanceProductType::Spot, BinanceEnvironment::Demo);
399 assert_eq!(url, "wss://demo-stream.binance.com/ws");
400 }
401
402 #[rstest]
403 fn test_ws_url_usdm_live() {
404 let url = get_ws_base_url(BinanceProductType::UsdM, BinanceEnvironment::Live);
405 assert_eq!(url, "wss://fstream.binance.com/market/ws");
406 }
407
408 #[rstest]
409 fn test_ws_url_coinm_demo() {
410 let url = get_ws_base_url(BinanceProductType::CoinM, BinanceEnvironment::Demo);
411 assert_eq!(url, "wss://demo-dstream.binance.com/ws");
412 }
413
414 #[rstest]
415 fn test_ws_url_options_testnet() {
416 let url = get_ws_base_url(BinanceProductType::Options, BinanceEnvironment::Testnet);
417 assert_eq!(url, "wss://fstream.binancefuture.com/market/ws");
418 }
419
420 #[rstest]
421 fn test_ws_url_options_demo() {
422 let url = get_ws_base_url(BinanceProductType::Options, BinanceEnvironment::Demo);
423 assert_eq!(url, "wss://fstream.binancefuture.com/market/ws");
424 }
425
426 #[rstest]
427 fn test_ws_private_url_usdm_live() {
428 let url = get_ws_private_base_url(BinanceProductType::UsdM, BinanceEnvironment::Live);
429 assert_eq!(url, "wss://fstream.binance.com/private/ws");
430 }
431
432 #[rstest]
433 fn test_ws_private_url_fallback_to_market() {
434 let url = get_ws_private_base_url(BinanceProductType::Spot, BinanceEnvironment::Live);
435 assert_eq!(
436 url,
437 get_ws_base_url(BinanceProductType::Spot, BinanceEnvironment::Live)
438 );
439 }
440
441 #[rstest]
442 fn test_ws_public_url_usdm_live() {
443 let url = get_ws_public_base_url(BinanceProductType::UsdM, BinanceEnvironment::Live);
444 assert_eq!(url, "wss://fstream.binance.com/public/ws");
445 }
446
447 #[rstest]
448 fn test_ws_public_url_options_demo() {
449 let url = get_ws_public_base_url(BinanceProductType::Options, BinanceEnvironment::Demo);
450 assert_eq!(url, "wss://fstream.binancefuture.com/public/ws");
451 }
452
453 #[rstest]
454 fn test_ws_private_url_options_demo() {
455 let url = get_ws_private_base_url(BinanceProductType::Options, BinanceEnvironment::Demo);
456 assert_eq!(url, "wss://fstream.binancefuture.com/private/ws");
457 }
458
459 #[rstest]
460 #[case(
461 BinanceProductType::UsdM,
462 "wss://fstream.binance.com/private/ws",
463 "wss://fstream.binance.com/private/ws?listenKey=redacted"
464 )]
465 #[case(
466 BinanceProductType::CoinM,
467 "wss://dstream.binance.com/ws",
468 "wss://dstream.binance.com/ws/redacted"
469 )]
470 #[case(
471 BinanceProductType::CoinM,
472 "wss://dstream.binancefuture.com/ws",
473 "wss://dstream.binancefuture.com/ws/redacted"
474 )]
475 #[case(
476 BinanceProductType::CoinM,
477 "wss://demo-dstream.binance.com/ws",
478 "wss://demo-dstream.binance.com/ws/redacted"
479 )]
480 #[case(
481 BinanceProductType::CoinM,
482 "wss://custom.example/ws/",
483 "wss://custom.example/ws/redacted"
484 )]
485 #[case(
486 BinanceProductType::CoinM,
487 "wss://custom.example",
488 "wss://custom.example/ws/redacted"
489 )]
490 #[case(
491 BinanceProductType::UsdM,
492 "ws://127.0.0.1:1234/ws-inject",
493 "ws://127.0.0.1:1234/ws-inject?listenKey=redacted"
494 )]
495 #[case(
496 BinanceProductType::CoinM,
497 "ws://127.0.0.1:1234/ws-inject",
498 "ws://127.0.0.1:1234/ws-inject/redacted"
499 )]
500 fn test_futures_user_stream_url(
501 #[case] product_type: BinanceProductType,
502 #[case] base_url: &str,
503 #[case] expected: &str,
504 ) {
505 let url = get_futures_user_stream_url(product_type, base_url, "redacted");
506 assert_eq!(url, expected);
507 }
508
509 #[rstest]
510 #[should_panic(expected = "Futures user stream requires UsdM or CoinM product type")]
511 fn test_futures_user_stream_url_rejects_non_futures_product() {
512 let _ = get_futures_user_stream_url(
513 BinanceProductType::Spot,
514 "wss://stream.binance.com/ws",
515 "redacted",
516 );
517 }
518
519 #[rstest]
520 fn test_ws_public_url_fallback_to_market() {
521 let url = get_ws_public_base_url(BinanceProductType::Spot, BinanceEnvironment::Live);
522 assert_eq!(
523 url,
524 get_ws_base_url(BinanceProductType::Spot, BinanceEnvironment::Live)
525 );
526 }
527
528 #[rstest]
529 #[case(
530 "wss://fstream.binance.com",
531 "market",
532 "wss://fstream.binance.com/market/ws"
533 )]
534 #[case(
535 "wss://fstream.binance.com/ws",
536 "public",
537 "wss://fstream.binance.com/public/ws"
538 )]
539 #[case(
540 "wss://fstream.binance.com/market/ws",
541 "private",
542 "wss://fstream.binance.com/private/ws"
543 )]
544 #[case(
545 "wss://fstream-mm.binance.com",
546 "market",
547 "wss://fstream-mm.binance.com/market/ws"
548 )]
549 #[case(
550 "wss://fstream-mm.binance.com/ws",
551 "public",
552 "wss://fstream-mm.binance.com/public/ws"
553 )]
554 #[case(
555 "wss://fstream-auth.binance.com/market/ws",
556 "private",
557 "wss://fstream-auth.binance.com/private/ws"
558 )]
559 #[case(
560 "wss://fstream.binance.us",
561 "market",
562 "wss://fstream.binance.us/market/ws"
563 )]
564 fn test_usdm_ws_route_base_url_normalizes_override(
565 #[case] base_url: &str,
566 #[case] route: &str,
567 #[case] expected: &str,
568 ) {
569 let url = get_usdm_ws_route_base_url(base_url, route);
570 assert_eq!(url, expected);
571 }
572
573 #[rstest]
574 #[case("ws://127.0.0.1:9999/ws", "market")]
575 #[case("wss://other.example.com/private/ws", "private")]
576 #[case("ws://localhost:8080", "public")]
577 #[case("wss://other-fstream.binance.com.example.org/ws", "market")]
578 #[case("wss://fstream.binance.com.example.org/ws", "market")]
579 fn test_usdm_ws_route_base_url_passes_through_non_binance_host(
580 #[case] base_url: &str,
581 #[case] route: &str,
582 ) {
583 let url = get_usdm_ws_route_base_url(base_url, route);
584 assert_eq!(url, base_url);
585 }
586
587 #[rstest]
588 #[case(BinanceEnvironment::Live, Some("https://api.binance.com"))]
589 #[case(BinanceEnvironment::Testnet, None)]
590 #[case(BinanceEnvironment::Demo, None)]
591 fn test_sapi_base_url(#[case] environment: BinanceEnvironment, #[case] expected: Option<&str>) {
592 let url = get_sapi_base_url(environment);
593 assert_eq!(url, expected);
594 }
595}