ccxt_exchanges/binance/endpoint_router.rs
1//! Binance-specific endpoint router trait.
2//!
3//! This module provides the `BinanceEndpointRouter` trait for routing API requests
4//! to the correct Binance domain based on market characteristics.
5//!
6//! # Binance API Domains
7//!
8//! Binance has a complex multi-domain structure:
9//! - **Spot**: `api.binance.com` - Spot trading and margin
10//! - **Linear Futures (FAPI)**: `fapi.binance.com` - USDT-margined perpetuals/futures
11//! - **Inverse Futures (DAPI)**: `dapi.binance.com` - Coin-margined perpetuals/futures
12//! - **Options (EAPI)**: `eapi.binance.com` - Options trading
13//! - **Portfolio Margin (PAPI)**: `papi.binance.com` - Portfolio margin API
14//!
15//! # Example
16//!
17//! ```rust,no_run
18//! use ccxt_exchanges::binance::{Binance, BinanceEndpointRouter};
19//! use ccxt_core::types::{EndpointType, Market, MarketType};
20//! use ccxt_core::ExchangeConfig;
21//!
22//! let binance = Binance::new(ExchangeConfig::default()).unwrap();
23//!
24//! // Get REST endpoint for a spot market
25//! let spot_market = Market::new_spot(
26//! "BTCUSDT".to_string(),
27//! "BTC/USDT".to_string(),
28//! "BTC".to_string(),
29//! "USDT".to_string(),
30//! );
31//! let url = binance.rest_endpoint(&spot_market, EndpointType::Public);
32//! assert!(url.contains("api.binance.com"));
33//!
34//! // Get default REST endpoint based on exchange options
35//! let default_url = binance.default_rest_endpoint(EndpointType::Public);
36//! ```
37
38use ccxt_core::types::{EndpointType, Market, MarketType};
39
40/// Binance-specific endpoint router trait.
41///
42/// This trait defines methods for obtaining the correct API endpoints based on
43/// market characteristics. Binance has multiple API domains for different market
44/// types (spot, linear futures, inverse futures, options).
45///
46/// # Implementation Notes
47///
48/// The routing logic follows these rules:
49/// - **Spot markets**: Use `api.binance.com` endpoints
50/// - **Linear Swap/Futures**: Use `fapi.binance.com` endpoints (USDT-margined)
51/// - **Inverse Swap/Futures**: Use `dapi.binance.com` endpoints (Coin-margined)
52/// - **Option markets**: Use `eapi.binance.com` endpoints
53///
54/// When sandbox mode is enabled, testnet URLs are returned instead.
55pub trait BinanceEndpointRouter {
56 /// Returns the REST API endpoint for a specific market.
57 ///
58 /// This method routes to the correct Binance domain based on the market's
59 /// `market_type`, `linear`, and `inverse` fields.
60 ///
61 /// # Arguments
62 ///
63 /// * `market` - Reference to the market object containing type information
64 /// * `endpoint_type` - Whether this is a public or private endpoint
65 ///
66 /// # Returns
67 ///
68 /// The REST API base URL string for the given market.
69 ///
70 /// # Routing Logic
71 ///
72 /// | Market Type | Linear | Inverse | Domain |
73 /// |-------------|--------|---------|--------|
74 /// | Spot | - | - | api.binance.com |
75 /// | Swap/Futures | true | false | fapi.binance.com |
76 /// | Swap/Futures | false | true | dapi.binance.com |
77 /// | Option | - | - | eapi.binance.com |
78 ///
79 /// # Example
80 ///
81 /// ```rust,no_run
82 /// use ccxt_exchanges::binance::{Binance, BinanceEndpointRouter};
83 /// use ccxt_core::types::{EndpointType, Market, MarketType};
84 /// use ccxt_core::ExchangeConfig;
85 /// use rust_decimal_macros::dec;
86 ///
87 /// let binance = Binance::new(ExchangeConfig::default()).unwrap();
88 ///
89 /// // Linear futures market
90 /// let linear_market = Market::new_swap(
91 /// "BTCUSDT".to_string(),
92 /// "BTC/USDT:USDT".to_string(),
93 /// "BTC".to_string(),
94 /// "USDT".to_string(),
95 /// "USDT".to_string(),
96 /// dec!(1.0),
97 /// );
98 /// let url = binance.rest_endpoint(&linear_market, EndpointType::Public);
99 /// assert!(url.contains("fapi.binance.com"));
100 /// ```
101 fn rest_endpoint(&self, market: &Market, endpoint_type: EndpointType) -> String;
102
103 /// Returns the WebSocket endpoint for a specific market.
104 ///
105 /// This method routes to the correct Binance WebSocket domain based on
106 /// the market's type and settlement characteristics.
107 ///
108 /// # Arguments
109 ///
110 /// * `market` - Reference to the market object containing type information
111 ///
112 /// # Returns
113 ///
114 /// The WebSocket URL string for the given market.
115 ///
116 /// # Routing Logic
117 ///
118 /// | Market Type | Linear | Inverse | WebSocket Domain |
119 /// |-------------|--------|---------|------------------|
120 /// | Spot | - | - | stream.binance.com |
121 /// | Swap/Futures | true | false | fstream.binance.com |
122 /// | Swap/Futures | false | true | dstream.binance.com |
123 /// | Option | - | - | nbstream.binance.com |
124 ///
125 /// # Example
126 ///
127 /// ```rust,no_run
128 /// use ccxt_exchanges::binance::{Binance, BinanceEndpointRouter};
129 /// use ccxt_core::types::Market;
130 /// use ccxt_core::ExchangeConfig;
131 ///
132 /// let binance = Binance::new(ExchangeConfig::default()).unwrap();
133 ///
134 /// let spot_market = Market::new_spot(
135 /// "BTCUSDT".to_string(),
136 /// "BTC/USDT".to_string(),
137 /// "BTC".to_string(),
138 /// "USDT".to_string(),
139 /// );
140 /// let ws_url = binance.ws_endpoint(&spot_market);
141 /// assert!(ws_url.contains("stream.binance.com"));
142 /// ```
143 fn ws_endpoint(&self, market: &Market) -> String;
144
145 /// Returns the default REST endpoint when no specific market is provided.
146 ///
147 /// This method uses the exchange's `default_type` and `default_sub_type`
148 /// options to determine which endpoint to return.
149 ///
150 /// # Arguments
151 ///
152 /// * `endpoint_type` - Whether this is a public or private endpoint
153 ///
154 /// # Returns
155 ///
156 /// The default REST API base URL string.
157 ///
158 /// # Example
159 ///
160 /// ```rust,no_run
161 /// use ccxt_exchanges::binance::{Binance, BinanceEndpointRouter, BinanceOptions};
162 /// use ccxt_core::types::EndpointType;
163 /// use ccxt_core::types::default_type::{DefaultType, DefaultSubType};
164 /// use ccxt_core::ExchangeConfig;
165 ///
166 /// // Create a futures-focused Binance instance
167 /// let options = BinanceOptions {
168 /// default_type: DefaultType::Swap,
169 /// default_sub_type: Some(DefaultSubType::Linear),
170 /// ..Default::default()
171 /// };
172 /// let binance = Binance::new_with_options(ExchangeConfig::default(), options).unwrap();
173 ///
174 /// let url = binance.default_rest_endpoint(EndpointType::Public);
175 /// assert!(url.contains("fapi.binance.com"));
176 /// ```
177 fn default_rest_endpoint(&self, endpoint_type: EndpointType) -> String;
178
179 /// Returns the default WebSocket endpoint when no specific market is provided.
180 ///
181 /// This method uses the exchange's `default_type` and `default_sub_type`
182 /// options to determine which WebSocket endpoint to return.
183 ///
184 /// # Returns
185 ///
186 /// The default WebSocket URL string.
187 ///
188 /// # Example
189 ///
190 /// ```rust,no_run
191 /// use ccxt_exchanges::binance::{Binance, BinanceEndpointRouter};
192 /// use ccxt_core::ExchangeConfig;
193 ///
194 /// let binance = Binance::new(ExchangeConfig::default()).unwrap();
195 /// let ws_url = binance.default_ws_endpoint();
196 /// // Default is spot, so should be stream.binance.com
197 /// assert!(ws_url.contains("stream.binance.com"));
198 /// ```
199 fn default_ws_endpoint(&self) -> String;
200
201 /// Returns the SAPI (Spot API) endpoint.
202 ///
203 /// SAPI is used for Binance-specific spot trading features like:
204 /// - Margin trading operations
205 /// - Savings and staking
206 /// - Sub-account management
207 /// - Asset transfers
208 ///
209 /// # Returns
210 ///
211 /// The SAPI base URL string.
212 ///
213 /// # Example
214 ///
215 /// ```rust,no_run
216 /// use ccxt_exchanges::binance::{Binance, BinanceEndpointRouter};
217 /// use ccxt_core::ExchangeConfig;
218 ///
219 /// let binance = Binance::new(ExchangeConfig::default()).unwrap();
220 /// let sapi_url = binance.sapi_endpoint();
221 /// assert!(sapi_url.contains("sapi"));
222 /// ```
223 fn sapi_endpoint(&self) -> String;
224
225 /// Returns the Portfolio Margin API (PAPI) endpoint.
226 ///
227 /// PAPI is used for portfolio margin trading which allows cross-margining
228 /// across spot, futures, and options positions.
229 ///
230 /// # Returns
231 ///
232 /// The PAPI base URL string.
233 ///
234 /// # Example
235 ///
236 /// ```rust,no_run
237 /// use ccxt_exchanges::binance::{Binance, BinanceEndpointRouter};
238 /// use ccxt_core::ExchangeConfig;
239 ///
240 /// let binance = Binance::new(ExchangeConfig::default()).unwrap();
241 /// let papi_url = binance.papi_endpoint();
242 /// assert!(papi_url.contains("papi"));
243 /// ```
244 fn papi_endpoint(&self) -> String;
245}
246
247use super::Binance;
248
249use ccxt_core::types::default_type::{DefaultSubType, DefaultType};
250
251impl BinanceEndpointRouter for Binance {
252 fn rest_endpoint(&self, market: &Market, endpoint_type: EndpointType) -> String {
253 let urls = self.urls();
254
255 match market.market_type {
256 MarketType::Spot => match endpoint_type {
257 EndpointType::Public => urls.public.clone(),
258 EndpointType::Private => urls.private.clone(),
259 },
260 MarketType::Swap | MarketType::Futures => {
261 // Determine linear/inverse from market fields
262 // Default to linear (true) if not specified
263 let is_linear = market.linear.unwrap_or(true);
264
265 if is_linear {
266 match endpoint_type {
267 EndpointType::Public => urls.fapi_public.clone(),
268 EndpointType::Private => urls.fapi_private.clone(),
269 }
270 } else {
271 match endpoint_type {
272 EndpointType::Public => urls.dapi_public.clone(),
273 EndpointType::Private => urls.dapi_private.clone(),
274 }
275 }
276 }
277 MarketType::Option => match endpoint_type {
278 EndpointType::Public => urls.eapi_public.clone(),
279 EndpointType::Private => urls.eapi_private.clone(),
280 },
281 }
282 }
283
284 fn ws_endpoint(&self, market: &Market) -> String {
285 let urls = self.urls();
286
287 match market.market_type {
288 MarketType::Spot => urls.ws.clone(),
289 MarketType::Swap | MarketType::Futures => {
290 // Determine linear/inverse from market fields
291 // Default to linear (true) if not specified
292 let is_linear = market.linear.unwrap_or(true);
293
294 if is_linear {
295 urls.ws_fapi.clone()
296 } else {
297 urls.ws_dapi.clone()
298 }
299 }
300 MarketType::Option => urls.ws_eapi.clone(),
301 }
302 }
303
304 fn default_rest_endpoint(&self, endpoint_type: EndpointType) -> String {
305 let urls = self.urls();
306 let options = self.options();
307
308 match options.default_type {
309 DefaultType::Spot => match endpoint_type {
310 EndpointType::Public => urls.public.clone(),
311 EndpointType::Private => urls.private.clone(),
312 },
313 DefaultType::Margin => urls.sapi.clone(),
314 DefaultType::Swap | DefaultType::Futures => {
315 match options.default_sub_type {
316 Some(DefaultSubType::Inverse) => match endpoint_type {
317 EndpointType::Public => urls.dapi_public.clone(),
318 EndpointType::Private => urls.dapi_private.clone(),
319 },
320 _ => match endpoint_type {
321 // Default to FAPI (Linear)
322 EndpointType::Public => urls.fapi_public.clone(),
323 EndpointType::Private => urls.fapi_private.clone(),
324 },
325 }
326 }
327 DefaultType::Option => match endpoint_type {
328 EndpointType::Public => urls.eapi_public.clone(),
329 EndpointType::Private => urls.eapi_private.clone(),
330 },
331 }
332 }
333
334 fn default_ws_endpoint(&self) -> String {
335 let urls = self.urls();
336 let options = self.options();
337
338 match options.default_type {
339 DefaultType::Swap | DefaultType::Futures => {
340 // Check sub-type for FAPI vs DAPI selection
341 match options.default_sub_type {
342 Some(DefaultSubType::Inverse) => urls.ws_dapi.clone(),
343 _ => urls.ws_fapi.clone(), // Default to FAPI (Linear) if not specified
344 }
345 }
346 DefaultType::Option => urls.ws_eapi.clone(),
347 _ => urls.ws.clone(), // Spot and Margin use standard WebSocket
348 }
349 }
350
351 fn sapi_endpoint(&self) -> String {
352 self.urls().sapi.clone()
353 }
354
355 fn papi_endpoint(&self) -> String {
356 self.urls().papi.clone()
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use ccxt_core::ExchangeConfig;
364 use rust_decimal_macros::dec;
365
366 fn create_test_binance() -> Binance {
367 Binance::new(ExchangeConfig::default()).unwrap()
368 }
369
370 fn create_sandbox_binance() -> Binance {
371 let config = ExchangeConfig {
372 sandbox: true,
373 ..Default::default()
374 };
375 Binance::new(config).unwrap()
376 }
377
378 // ==================== REST Endpoint Tests ====================
379
380 #[test]
381 fn test_rest_endpoint_spot_public() {
382 let binance = create_test_binance();
383 let market = Market::new_spot(
384 "BTCUSDT".to_string(),
385 "BTC/USDT".to_string(),
386 "BTC".to_string(),
387 "USDT".to_string(),
388 );
389
390 let url = binance.rest_endpoint(&market, EndpointType::Public);
391 assert!(url.contains("api.binance.com"));
392 }
393
394 #[test]
395 fn test_rest_endpoint_spot_private() {
396 let binance = create_test_binance();
397 let market = Market::new_spot(
398 "BTCUSDT".to_string(),
399 "BTC/USDT".to_string(),
400 "BTC".to_string(),
401 "USDT".to_string(),
402 );
403
404 let url = binance.rest_endpoint(&market, EndpointType::Private);
405 assert!(url.contains("api.binance.com"));
406 }
407
408 #[test]
409 fn test_rest_endpoint_linear_swap_public() {
410 let binance = create_test_binance();
411 let market = Market::new_swap(
412 "BTCUSDT".to_string(),
413 "BTC/USDT:USDT".to_string(),
414 "BTC".to_string(),
415 "USDT".to_string(),
416 "USDT".to_string(),
417 dec!(1.0),
418 );
419
420 let url = binance.rest_endpoint(&market, EndpointType::Public);
421 assert!(url.contains("fapi.binance.com"));
422 }
423
424 #[test]
425 fn test_rest_endpoint_linear_swap_private() {
426 let binance = create_test_binance();
427 let market = Market::new_swap(
428 "BTCUSDT".to_string(),
429 "BTC/USDT:USDT".to_string(),
430 "BTC".to_string(),
431 "USDT".to_string(),
432 "USDT".to_string(),
433 dec!(1.0),
434 );
435
436 let url = binance.rest_endpoint(&market, EndpointType::Private);
437 assert!(url.contains("fapi.binance.com"));
438 }
439
440 #[test]
441 fn test_rest_endpoint_inverse_swap_public() {
442 let binance = create_test_binance();
443 let mut market = Market::new_swap(
444 "BTCUSD_PERP".to_string(),
445 "BTC/USD:BTC".to_string(),
446 "BTC".to_string(),
447 "USD".to_string(),
448 "BTC".to_string(),
449 dec!(100.0),
450 );
451 // Ensure inverse is set correctly
452 market.linear = Some(false);
453 market.inverse = Some(true);
454
455 let url = binance.rest_endpoint(&market, EndpointType::Public);
456 assert!(url.contains("dapi.binance.com"));
457 }
458
459 #[test]
460 fn test_rest_endpoint_inverse_swap_private() {
461 let binance = create_test_binance();
462 let mut market = Market::new_swap(
463 "BTCUSD_PERP".to_string(),
464 "BTC/USD:BTC".to_string(),
465 "BTC".to_string(),
466 "USD".to_string(),
467 "BTC".to_string(),
468 dec!(100.0),
469 );
470 market.linear = Some(false);
471 market.inverse = Some(true);
472
473 let url = binance.rest_endpoint(&market, EndpointType::Private);
474 assert!(url.contains("dapi.binance.com"));
475 }
476
477 #[test]
478 fn test_rest_endpoint_option_public() {
479 let binance = create_test_binance();
480 let mut market = Market::default();
481 market.id = "BTC-250328-100000-C".to_string();
482 market.symbol = "BTC/USDT:USDT-250328-100000-C".to_string();
483 market.market_type = MarketType::Option;
484
485 let url = binance.rest_endpoint(&market, EndpointType::Public);
486 assert!(url.contains("eapi.binance.com"));
487 }
488
489 #[test]
490 fn test_rest_endpoint_option_private() {
491 let binance = create_test_binance();
492 let mut market = Market::default();
493 market.id = "BTC-250328-100000-C".to_string();
494 market.symbol = "BTC/USDT:USDT-250328-100000-C".to_string();
495 market.market_type = MarketType::Option;
496
497 let url = binance.rest_endpoint(&market, EndpointType::Private);
498 assert!(url.contains("eapi.binance.com"));
499 }
500
501 // ==================== WebSocket Endpoint Tests ====================
502
503 #[test]
504 fn test_ws_endpoint_spot() {
505 let binance = create_test_binance();
506 let market = Market::new_spot(
507 "BTCUSDT".to_string(),
508 "BTC/USDT".to_string(),
509 "BTC".to_string(),
510 "USDT".to_string(),
511 );
512
513 let url = binance.ws_endpoint(&market);
514 assert!(url.contains("stream.binance.com"));
515 }
516
517 #[test]
518 fn test_ws_endpoint_linear_swap() {
519 let binance = create_test_binance();
520 let market = Market::new_swap(
521 "BTCUSDT".to_string(),
522 "BTC/USDT:USDT".to_string(),
523 "BTC".to_string(),
524 "USDT".to_string(),
525 "USDT".to_string(),
526 dec!(1.0),
527 );
528
529 let url = binance.ws_endpoint(&market);
530 assert!(url.contains("fstream.binance.com"));
531 }
532
533 #[test]
534 fn test_ws_endpoint_inverse_swap() {
535 let binance = create_test_binance();
536 let mut market = Market::new_swap(
537 "BTCUSD_PERP".to_string(),
538 "BTC/USD:BTC".to_string(),
539 "BTC".to_string(),
540 "USD".to_string(),
541 "BTC".to_string(),
542 dec!(100.0),
543 );
544 market.linear = Some(false);
545 market.inverse = Some(true);
546
547 let url = binance.ws_endpoint(&market);
548 assert!(url.contains("dstream.binance.com"));
549 }
550
551 #[test]
552 fn test_ws_endpoint_option() {
553 let binance = create_test_binance();
554 let mut market = Market::default();
555 market.market_type = MarketType::Option;
556
557 let url = binance.ws_endpoint(&market);
558 assert!(url.contains("nbstream.binance.com"));
559 }
560
561 // ==================== Default Endpoint Tests ====================
562
563 #[test]
564 fn test_default_rest_endpoint_spot() {
565 let binance = create_test_binance();
566
567 let url = binance.default_rest_endpoint(EndpointType::Public);
568 assert!(url.contains("api.binance.com"));
569 }
570
571 #[test]
572 fn test_default_ws_endpoint_spot() {
573 let binance = create_test_binance();
574
575 let url = binance.default_ws_endpoint();
576 assert!(url.contains("stream.binance.com"));
577 }
578
579 // ==================== SAPI and PAPI Tests ====================
580
581 #[test]
582 fn test_sapi_endpoint() {
583 let binance = create_test_binance();
584
585 let url = binance.sapi_endpoint();
586 assert!(url.contains("sapi"));
587 assert!(url.contains("api.binance.com"));
588 }
589
590 #[test]
591 fn test_papi_endpoint() {
592 let binance = create_test_binance();
593
594 let url = binance.papi_endpoint();
595 assert!(url.contains("papi"));
596 }
597
598 // ==================== Sandbox Mode Tests ====================
599
600 #[test]
601 fn test_sandbox_rest_endpoint_spot() {
602 let binance = create_sandbox_binance();
603 let market = Market::new_spot(
604 "BTCUSDT".to_string(),
605 "BTC/USDT".to_string(),
606 "BTC".to_string(),
607 "USDT".to_string(),
608 );
609
610 let url = binance.rest_endpoint(&market, EndpointType::Public);
611 assert!(url.contains("testnet"));
612 }
613
614 #[test]
615 fn test_sandbox_ws_endpoint_spot() {
616 let binance = create_sandbox_binance();
617 let market = Market::new_spot(
618 "BTCUSDT".to_string(),
619 "BTC/USDT".to_string(),
620 "BTC".to_string(),
621 "USDT".to_string(),
622 );
623
624 let url = binance.ws_endpoint(&market);
625 assert!(url.contains("testnet"));
626 }
627
628 #[test]
629 fn test_sandbox_rest_endpoint_linear_swap() {
630 let binance = create_sandbox_binance();
631 let market = Market::new_swap(
632 "BTCUSDT".to_string(),
633 "BTC/USDT:USDT".to_string(),
634 "BTC".to_string(),
635 "USDT".to_string(),
636 "USDT".to_string(),
637 dec!(1.0),
638 );
639
640 let url = binance.rest_endpoint(&market, EndpointType::Public);
641 assert!(url.contains("testnet"));
642 }
643
644 // ==================== Edge Case Tests ====================
645
646 #[test]
647 fn test_swap_defaults_to_linear_when_not_specified() {
648 let binance = create_test_binance();
649 let mut market = Market::default();
650 market.market_type = MarketType::Swap;
651 // linear and inverse are None
652
653 let url = binance.rest_endpoint(&market, EndpointType::Public);
654 // Should default to linear (fapi)
655 assert!(url.contains("fapi.binance.com"));
656 }
657
658 #[test]
659 fn test_futures_defaults_to_linear_when_not_specified() {
660 let binance = create_test_binance();
661 let mut market = Market::default();
662 market.market_type = MarketType::Futures;
663 // linear and inverse are None
664
665 let url = binance.rest_endpoint(&market, EndpointType::Public);
666 // Should default to linear (fapi)
667 assert!(url.contains("fapi.binance.com"));
668 }
669}