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}