ccxt_exchanges/bybit/
endpoint_router.rs

1//! Bybit-specific endpoint router trait.
2//!
3//! This module provides the `BybitEndpointRouter` trait for routing API requests
4//! to the correct Bybit endpoints based on market category.
5//!
6//! # Bybit API Structure
7//!
8//! Bybit V5 API uses a unified REST domain with category-based WebSocket paths:
9//! - **REST**: `api.bybit.com` - Single unified REST endpoint
10//! - **WebSocket Public**: `stream.bybit.com/v5/public/{category}` - Category-specific paths
11//! - **WebSocket Private**: `stream.bybit.com/v5/private` - Single private endpoint
12//!
13//! # Categories
14//!
15//! Bybit uses the following categories:
16//! - `spot` - Spot trading
17//! - `linear` - USDT-margined perpetuals and futures
18//! - `inverse` - Coin-margined perpetuals and futures
19//! - `option` - Options trading
20//!
21//! # Example
22//!
23//! ```rust,no_run
24//! use ccxt_exchanges::bybit::{Bybit, BybitEndpointRouter};
25//! use ccxt_core::ExchangeConfig;
26//!
27//! let bybit = Bybit::new(ExchangeConfig::default()).unwrap();
28//!
29//! // Get REST endpoint (unified for all categories)
30//! let rest_url = bybit.rest_endpoint();
31//! assert!(rest_url.contains("api.bybit.com"));
32//!
33//! // Get WebSocket endpoint for linear category
34//! let ws_url = bybit.ws_public_endpoint("linear");
35//! assert!(ws_url.contains("/v5/public/linear"));
36//!
37//! // Get private WebSocket endpoint
38//! let ws_private_url = bybit.ws_private_endpoint();
39//! assert!(ws_private_url.contains("/v5/private"));
40//! ```
41
42/// Bybit-specific endpoint router trait.
43///
44/// This trait defines methods for obtaining the correct API endpoints for Bybit.
45/// Unlike Binance which has multiple REST domains, Bybit uses a unified REST
46/// endpoint with category-based WebSocket paths.
47///
48/// # Implementation Notes
49///
50/// - REST API uses a single unified domain for all market types
51/// - WebSocket public endpoints are differentiated by category path suffix
52/// - WebSocket private endpoint is unified for all authenticated streams
53/// - Sandbox/testnet mode switches to testnet domains
54pub trait BybitEndpointRouter {
55    /// Returns the REST API endpoint.
56    ///
57    /// Bybit uses a unified REST domain for all market types. The category
58    /// is specified as a query parameter in API requests, not in the URL.
59    ///
60    /// # Returns
61    ///
62    /// The REST API base URL string.
63    ///
64    /// # Example
65    ///
66    /// ```rust,no_run
67    /// use ccxt_exchanges::bybit::{Bybit, BybitEndpointRouter};
68    /// use ccxt_core::ExchangeConfig;
69    ///
70    /// let bybit = Bybit::new(ExchangeConfig::default()).unwrap();
71    /// let url = bybit.rest_endpoint();
72    /// assert!(url.contains("api.bybit.com"));
73    /// ```
74    fn rest_endpoint(&self) -> &'static str;
75
76    /// Returns the public WebSocket endpoint for a specific category.
77    ///
78    /// Bybit V5 API uses different WebSocket paths for different categories:
79    /// - `spot`: `/v5/public/spot`
80    /// - `linear`: `/v5/public/linear`
81    /// - `inverse`: `/v5/public/inverse`
82    /// - `option`: `/v5/public/option`
83    ///
84    /// # Arguments
85    ///
86    /// * `category` - The market category ("spot", "linear", "inverse", "option")
87    ///
88    /// # Returns
89    ///
90    /// The complete WebSocket URL for the specified category.
91    ///
92    /// # Example
93    ///
94    /// ```rust,no_run
95    /// use ccxt_exchanges::bybit::{Bybit, BybitEndpointRouter};
96    /// use ccxt_core::ExchangeConfig;
97    ///
98    /// let bybit = Bybit::new(ExchangeConfig::default()).unwrap();
99    ///
100    /// // Get WebSocket URL for linear perpetuals
101    /// let ws_url = bybit.ws_public_endpoint("linear");
102    /// assert!(ws_url.contains("/v5/public/linear"));
103    ///
104    /// // Get WebSocket URL for spot trading
105    /// let ws_spot_url = bybit.ws_public_endpoint("spot");
106    /// assert!(ws_spot_url.contains("/v5/public/spot"));
107    /// ```
108    fn ws_public_endpoint(&self, category: &str) -> String;
109
110    /// Returns the private WebSocket endpoint.
111    ///
112    /// Bybit uses a single private WebSocket endpoint for all authenticated
113    /// streams regardless of market category.
114    ///
115    /// # Returns
116    ///
117    /// The private WebSocket URL string.
118    ///
119    /// # Example
120    ///
121    /// ```rust,no_run
122    /// use ccxt_exchanges::bybit::{Bybit, BybitEndpointRouter};
123    /// use ccxt_core::ExchangeConfig;
124    ///
125    /// let bybit = Bybit::new(ExchangeConfig::default()).unwrap();
126    /// let ws_private_url = bybit.ws_private_endpoint();
127    /// assert!(ws_private_url.contains("/v5/private"));
128    /// ```
129    fn ws_private_endpoint(&self) -> &str;
130}
131
132use super::Bybit;
133
134impl BybitEndpointRouter for Bybit {
135    fn rest_endpoint(&self) -> &'static str {
136        // Use the existing urls() method which already handles sandbox mode
137        // We need to return a reference, so we'll use a static approach
138        // based on sandbox mode
139        if self.is_sandbox() {
140            "https://api-testnet.bybit.com"
141        } else {
142            "https://api.bybit.com"
143        }
144    }
145
146    fn ws_public_endpoint(&self, category: &str) -> String {
147        let urls = self.urls();
148        urls.ws_public_for_category(category)
149    }
150
151    fn ws_private_endpoint(&self) -> &str {
152        // Similar to rest_endpoint, return static reference based on sandbox mode
153        if self.is_sandbox() {
154            "wss://stream-testnet.bybit.com/v5/private"
155        } else {
156            "wss://stream.bybit.com/v5/private"
157        }
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use crate::bybit::BybitOptions;
165    use ccxt_core::ExchangeConfig;
166    use ccxt_core::types::default_type::{DefaultSubType, DefaultType};
167
168    fn create_test_bybit() -> Bybit {
169        Bybit::new(ExchangeConfig::default()).unwrap()
170    }
171
172    fn create_sandbox_bybit() -> Bybit {
173        let config = ExchangeConfig {
174            sandbox: true,
175            ..Default::default()
176        };
177        Bybit::new(config).unwrap()
178    }
179
180    // ==================== REST Endpoint Tests ====================
181
182    #[test]
183    fn test_rest_endpoint_production() {
184        let bybit = create_test_bybit();
185        let url = bybit.rest_endpoint();
186        assert!(url.contains("api.bybit.com"));
187        assert!(!url.contains("testnet"));
188    }
189
190    #[test]
191    fn test_rest_endpoint_sandbox() {
192        let bybit = create_sandbox_bybit();
193        let url = bybit.rest_endpoint();
194        assert!(url.contains("api-testnet.bybit.com"));
195    }
196
197    // ==================== WebSocket Public Endpoint Tests ====================
198
199    #[test]
200    fn test_ws_public_endpoint_spot() {
201        let bybit = create_test_bybit();
202        let url = bybit.ws_public_endpoint("spot");
203        assert!(url.contains("stream.bybit.com"));
204        assert!(url.ends_with("/v5/public/spot"));
205    }
206
207    #[test]
208    fn test_ws_public_endpoint_linear() {
209        let bybit = create_test_bybit();
210        let url = bybit.ws_public_endpoint("linear");
211        assert!(url.contains("stream.bybit.com"));
212        assert!(url.ends_with("/v5/public/linear"));
213    }
214
215    #[test]
216    fn test_ws_public_endpoint_inverse() {
217        let bybit = create_test_bybit();
218        let url = bybit.ws_public_endpoint("inverse");
219        assert!(url.contains("stream.bybit.com"));
220        assert!(url.ends_with("/v5/public/inverse"));
221    }
222
223    #[test]
224    fn test_ws_public_endpoint_option() {
225        let bybit = create_test_bybit();
226        let url = bybit.ws_public_endpoint("option");
227        assert!(url.contains("stream.bybit.com"));
228        assert!(url.ends_with("/v5/public/option"));
229    }
230
231    #[test]
232    fn test_ws_public_endpoint_sandbox_spot() {
233        let bybit = create_sandbox_bybit();
234        let url = bybit.ws_public_endpoint("spot");
235        assert!(url.contains("stream-testnet.bybit.com"));
236        assert!(url.ends_with("/v5/public/spot"));
237    }
238
239    #[test]
240    fn test_ws_public_endpoint_sandbox_linear() {
241        let bybit = create_sandbox_bybit();
242        let url = bybit.ws_public_endpoint("linear");
243        assert!(url.contains("stream-testnet.bybit.com"));
244        assert!(url.ends_with("/v5/public/linear"));
245    }
246
247    // ==================== WebSocket Private Endpoint Tests ====================
248
249    #[test]
250    fn test_ws_private_endpoint_production() {
251        let bybit = create_test_bybit();
252        let url = bybit.ws_private_endpoint();
253        assert!(url.contains("stream.bybit.com"));
254        assert!(url.contains("/v5/private"));
255        assert!(!url.contains("testnet"));
256    }
257
258    #[test]
259    fn test_ws_private_endpoint_sandbox() {
260        let bybit = create_sandbox_bybit();
261        let url = bybit.ws_private_endpoint();
262        assert!(url.contains("stream-testnet.bybit.com"));
263        assert!(url.contains("/v5/private"));
264    }
265
266    // ==================== Category Path Construction Tests ====================
267
268    #[test]
269    fn test_ws_public_endpoint_path_format() {
270        let bybit = create_test_bybit();
271
272        // Verify all categories follow the /v5/public/{category} format
273        let categories = ["spot", "linear", "inverse", "option"];
274        for category in categories {
275            let url = bybit.ws_public_endpoint(category);
276            let expected_suffix = format!("/v5/public/{}", category);
277            assert!(
278                url.ends_with(&expected_suffix),
279                "URL {} should end with {}",
280                url,
281                expected_suffix
282            );
283        }
284    }
285
286    // ==================== Testnet Option Tests ====================
287
288    #[test]
289    fn test_rest_endpoint_with_testnet_option() {
290        let config = ExchangeConfig::default();
291        let options = BybitOptions {
292            testnet: true,
293            ..Default::default()
294        };
295        let bybit = Bybit::new_with_options(config, options).unwrap();
296
297        let url = bybit.rest_endpoint();
298        assert!(url.contains("api-testnet.bybit.com"));
299    }
300
301    #[test]
302    fn test_ws_private_endpoint_with_testnet_option() {
303        let config = ExchangeConfig::default();
304        let options = BybitOptions {
305            testnet: true,
306            ..Default::default()
307        };
308        let bybit = Bybit::new_with_options(config, options).unwrap();
309
310        let url = bybit.ws_private_endpoint();
311        assert!(url.contains("stream-testnet.bybit.com"));
312    }
313
314    // ==================== Integration with Default Type Tests ====================
315
316    #[test]
317    fn test_ws_public_endpoint_with_linear_default_type() {
318        let config = ExchangeConfig::default();
319        let options = BybitOptions {
320            default_type: DefaultType::Swap,
321            default_sub_type: Some(DefaultSubType::Linear),
322            ..Default::default()
323        };
324        let bybit = Bybit::new_with_options(config, options).unwrap();
325
326        // Use the category() method to get the correct category
327        let category = bybit.category();
328        assert_eq!(category, "linear");
329
330        let url = bybit.ws_public_endpoint(category);
331        assert!(url.ends_with("/v5/public/linear"));
332    }
333
334    #[test]
335    fn test_ws_public_endpoint_with_inverse_default_type() {
336        let config = ExchangeConfig::default();
337        let options = BybitOptions {
338            default_type: DefaultType::Swap,
339            default_sub_type: Some(DefaultSubType::Inverse),
340            ..Default::default()
341        };
342        let bybit = Bybit::new_with_options(config, options).unwrap();
343
344        let category = bybit.category();
345        assert_eq!(category, "inverse");
346
347        let url = bybit.ws_public_endpoint(category);
348        assert!(url.ends_with("/v5/public/inverse"));
349    }
350}