1use 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#[must_use]
35pub fn get_http_base_url(
36 product_type: BinanceProductType,
37 environment: BinanceEnvironment,
38) -> &'static str {
39 match (product_type, environment) {
40 (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 (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 (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#[must_use]
72pub fn get_ws_base_url(
73 product_type: BinanceProductType,
74 environment: BinanceEnvironment,
75) -> &'static str {
76 match (product_type, environment) {
77 (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 (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 (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#[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#[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 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#[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}