Skip to main content

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 market = Market {
481            id: "BTC-250328-100000-C".to_string(),
482            symbol: "BTC/USDT:USDT-250328-100000-C".to_string(),
483            market_type: MarketType::Option,
484            ..Default::default()
485        };
486
487        let url = binance.rest_endpoint(&market, EndpointType::Public);
488        assert!(url.contains("eapi.binance.com"));
489    }
490
491    #[test]
492    fn test_rest_endpoint_option_private() {
493        let binance = create_test_binance();
494        let market = Market {
495            id: "BTC-250328-100000-C".to_string(),
496            symbol: "BTC/USDT:USDT-250328-100000-C".to_string(),
497            market_type: MarketType::Option,
498            ..Default::default()
499        };
500
501        let url = binance.rest_endpoint(&market, EndpointType::Private);
502        assert!(url.contains("eapi.binance.com"));
503    }
504
505    // ==================== WebSocket Endpoint Tests ====================
506
507    #[test]
508    fn test_ws_endpoint_spot() {
509        let binance = create_test_binance();
510        let market = Market::new_spot(
511            "BTCUSDT".to_string(),
512            "BTC/USDT".to_string(),
513            "BTC".to_string(),
514            "USDT".to_string(),
515        );
516
517        let url = binance.ws_endpoint(&market);
518        assert!(url.contains("stream.binance.com"));
519    }
520
521    #[test]
522    fn test_ws_endpoint_linear_swap() {
523        let binance = create_test_binance();
524        let market = Market::new_swap(
525            "BTCUSDT".to_string(),
526            "BTC/USDT:USDT".to_string(),
527            "BTC".to_string(),
528            "USDT".to_string(),
529            "USDT".to_string(),
530            dec!(1.0),
531        );
532
533        let url = binance.ws_endpoint(&market);
534        assert!(url.contains("fstream.binance.com"));
535    }
536
537    #[test]
538    fn test_ws_endpoint_inverse_swap() {
539        let binance = create_test_binance();
540        let mut market = Market::new_swap(
541            "BTCUSD_PERP".to_string(),
542            "BTC/USD:BTC".to_string(),
543            "BTC".to_string(),
544            "USD".to_string(),
545            "BTC".to_string(),
546            dec!(100.0),
547        );
548        market.linear = Some(false);
549        market.inverse = Some(true);
550
551        let url = binance.ws_endpoint(&market);
552        assert!(url.contains("dstream.binance.com"));
553    }
554
555    #[test]
556    fn test_ws_endpoint_option() {
557        let binance = create_test_binance();
558        let mut market = Market::default();
559        market.market_type = MarketType::Option;
560
561        let url = binance.ws_endpoint(&market);
562        assert!(url.contains("nbstream.binance.com"));
563    }
564
565    // ==================== Default Endpoint Tests ====================
566
567    #[test]
568    fn test_default_rest_endpoint_spot() {
569        let binance = create_test_binance();
570
571        let url = binance.default_rest_endpoint(EndpointType::Public);
572        assert!(url.contains("api.binance.com"));
573    }
574
575    #[test]
576    fn test_default_ws_endpoint_spot() {
577        let binance = create_test_binance();
578
579        let url = binance.default_ws_endpoint();
580        assert!(url.contains("stream.binance.com"));
581    }
582
583    // ==================== SAPI and PAPI Tests ====================
584
585    #[test]
586    fn test_sapi_endpoint() {
587        let binance = create_test_binance();
588
589        let url = binance.sapi_endpoint();
590        assert!(url.contains("sapi"));
591        assert!(url.contains("api.binance.com"));
592    }
593
594    #[test]
595    fn test_papi_endpoint() {
596        let binance = create_test_binance();
597
598        let url = binance.papi_endpoint();
599        assert!(url.contains("papi"));
600    }
601
602    // ==================== Sandbox Mode Tests ====================
603
604    #[test]
605    fn test_sandbox_rest_endpoint_spot() {
606        let binance = create_sandbox_binance();
607        let market = Market::new_spot(
608            "BTCUSDT".to_string(),
609            "BTC/USDT".to_string(),
610            "BTC".to_string(),
611            "USDT".to_string(),
612        );
613
614        let url = binance.rest_endpoint(&market, EndpointType::Public);
615        assert!(url.contains("testnet"));
616    }
617
618    #[test]
619    fn test_sandbox_ws_endpoint_spot() {
620        let binance = create_sandbox_binance();
621        let market = Market::new_spot(
622            "BTCUSDT".to_string(),
623            "BTC/USDT".to_string(),
624            "BTC".to_string(),
625            "USDT".to_string(),
626        );
627
628        let url = binance.ws_endpoint(&market);
629        assert!(url.contains("testnet"));
630    }
631
632    #[test]
633    fn test_sandbox_rest_endpoint_linear_swap() {
634        let binance = create_sandbox_binance();
635        let market = Market::new_swap(
636            "BTCUSDT".to_string(),
637            "BTC/USDT:USDT".to_string(),
638            "BTC".to_string(),
639            "USDT".to_string(),
640            "USDT".to_string(),
641            dec!(1.0),
642        );
643
644        let url = binance.rest_endpoint(&market, EndpointType::Public);
645        assert!(url.contains("testnet"));
646    }
647
648    // ==================== Edge Case Tests ====================
649
650    #[test]
651    fn test_swap_defaults_to_linear_when_not_specified() {
652        let binance = create_test_binance();
653        let market = Market {
654            market_type: MarketType::Swap,
655            ..Default::default()
656        };
657        // linear and inverse are None
658
659        let url = binance.rest_endpoint(&market, EndpointType::Public);
660        // Should default to linear (fapi)
661        assert!(url.contains("fapi.binance.com"));
662    }
663
664    #[test]
665    fn test_futures_defaults_to_linear_when_not_specified() {
666        let binance = create_test_binance();
667        let market = Market {
668            market_type: MarketType::Futures,
669            ..Default::default()
670        };
671        // linear and inverse are None
672
673        let url = binance.rest_endpoint(&market, EndpointType::Public);
674        // Should default to linear (fapi)
675        assert!(url.contains("fapi.binance.com"));
676    }
677}