Skip to main content

bybit_client/
config.rs

1//! Configuration for the Bybit client.
2
3use secrecy::{ExposeSecret, SecretString};
4
5/// API base URLs for REST endpoints.
6pub mod rest_urls {
7    /// Default mainnet API URL.
8    pub const MAINNET: &str = "https://api.bybit.com";
9    /// Alternative mainnet (bytick).
10    pub const MAINNET_BYTICK: &str = "https://api.bytick.com";
11    /// Testnet API URL.
12    pub const TESTNET: &str = "https://api-testnet.bybit.com";
13    /// Demo trading API URL.
14    pub const DEMO: &str = "https://api-demo.bybit.com";
15
16    /// Netherlands
17    pub const NL: &str = "https://api.bybit.nl";
18    /// Turkey
19    pub const TK: &str = "https://api.bybit-tr.com";
20    /// Kazakhstan
21    pub const KZ: &str = "https://api.bybit.kz";
22    /// Hong Kong
23    pub const HK: &str = "https://api.byhkbit.com";
24    /// Georgia
25    pub const GE: &str = "https://api.bybitgeorgia.ge";
26    /// UAE
27    pub const UAE: &str = "https://api.bybit.ae";
28    /// EU
29    pub const EU: &str = "https://api.bybit.eu";
30}
31
32/// WebSocket URLs.
33pub mod ws_urls {
34    use super::super::types::Category;
35
36    /// Get public WebSocket URL for mainnet.
37    pub fn public_mainnet(category: Category) -> String {
38        format!("wss://stream.bybit.com/v5/public/{}", category.as_str())
39    }
40
41    /// Get public WebSocket URL for testnet.
42    pub fn public_testnet(category: Category) -> String {
43        format!(
44            "wss://stream-testnet.bybit.com/v5/public/{}",
45            category.as_str()
46        )
47    }
48
49    /// Get public WebSocket URL for demo.
50    pub fn public_demo(category: Category) -> String {
51        format!("wss://stream-demo.bybit.com/v5/public/{}", category.as_str())
52    }
53
54    /// Private WebSocket URL for mainnet.
55    pub const PRIVATE_MAINNET: &str = "wss://stream.bybit.com/v5/private";
56    /// Private WebSocket URL for testnet.
57    pub const PRIVATE_TESTNET: &str = "wss://stream-testnet.bybit.com/v5/private";
58    /// Private WebSocket URL for demo.
59    pub const PRIVATE_DEMO: &str = "wss://stream-demo.bybit.com/v5/private";
60
61    /// Trade WebSocket URL for mainnet.
62    pub const TRADE_MAINNET: &str = "wss://stream.bybit.com/v5/trade";
63    /// Trade WebSocket URL for testnet.
64    pub const TRADE_TESTNET: &str = "wss://stream-testnet.bybit.com/v5/trade";
65    /// Trade WebSocket URL for demo.
66    pub const TRADE_DEMO: &str = "wss://stream-demo.bybit.com/v5/trade";
67}
68
69/// API region selection.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
71pub enum ApiRegion {
72    /// Default (api.bybit.com)
73    #[default]
74    Default,
75    /// Bytick (api.bytick.com)
76    Bytick,
77    /// Netherlands
78    NL,
79    /// Turkey
80    TK,
81    /// Kazakhstan
82    KZ,
83    /// Hong Kong
84    HK,
85    /// Georgia
86    GE,
87    /// UAE
88    UAE,
89    /// EU
90    EU,
91}
92
93impl ApiRegion {
94    /// Get the base URL for this region.
95    pub fn base_url(&self) -> &'static str {
96        match self {
97            ApiRegion::Default => rest_urls::MAINNET,
98            ApiRegion::Bytick => rest_urls::MAINNET_BYTICK,
99            ApiRegion::NL => rest_urls::NL,
100            ApiRegion::TK => rest_urls::TK,
101            ApiRegion::KZ => rest_urls::KZ,
102            ApiRegion::HK => rest_urls::HK,
103            ApiRegion::GE => rest_urls::GE,
104            ApiRegion::UAE => rest_urls::UAE,
105            ApiRegion::EU => rest_urls::EU,
106        }
107    }
108}
109
110/// Environment selection (production, testnet, demo).
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
112pub enum Environment {
113    /// Production environment.
114    #[default]
115    Production,
116    /// Testnet environment.
117    Testnet,
118    /// Demo trading environment.
119    Demo,
120}
121
122/// Client configuration.
123#[derive(Clone)]
124pub struct ClientConfig {
125    /// API key (required for private endpoints).
126    pub api_key: Option<String>,
127    /// API secret (required for private endpoints).
128    api_secret: Option<SecretString>,
129    /// Environment (production, testnet, demo).
130    pub environment: Environment,
131    /// API region for production.
132    pub region: ApiRegion,
133    /// Custom base URL (overrides environment/region).
134    pub base_url: Option<String>,
135    /// Request validity window in milliseconds.
136    pub recv_window: u32,
137    /// Enable request/response logging.
138    pub debug: bool,
139    /// Request timeout in milliseconds.
140    pub timeout_ms: u64,
141    /// Referer header value.
142    pub referer: Option<String>,
143}
144
145impl std::fmt::Debug for ClientConfig {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        f.debug_struct("ClientConfig")
148            .field("api_key", &self.api_key.as_ref().map(|_| "[REDACTED]"))
149            .field("api_secret", &self.api_secret.as_ref().map(|_| "[REDACTED]"))
150            .field("environment", &self.environment)
151            .field("region", &self.region)
152            .field("base_url", &self.base_url)
153            .field("recv_window", &self.recv_window)
154            .field("debug", &self.debug)
155            .field("timeout_ms", &self.timeout_ms)
156            .field("referer", &self.referer)
157            .finish()
158    }
159}
160
161impl Default for ClientConfig {
162    fn default() -> Self {
163        Self {
164            api_key: None,
165            api_secret: None,
166            environment: Environment::Production,
167            region: ApiRegion::Default,
168            base_url: None,
169            recv_window: 5000,
170            debug: false,
171            timeout_ms: 10000,
172            referer: None,
173        }
174    }
175}
176
177impl ClientConfig {
178    /// Create a new configuration with API credentials.
179    pub fn new(api_key: impl Into<String>, api_secret: impl Into<String>) -> Self {
180        Self {
181            api_key: Some(api_key.into()),
182            api_secret: Some(SecretString::from(api_secret.into())),
183            ..Default::default()
184        }
185    }
186
187    /// Create a configuration for public endpoints only (no authentication).
188    pub fn public_only() -> Self {
189        Self::default()
190    }
191
192    /// Set the environment.
193    pub fn environment(mut self, env: Environment) -> Self {
194        self.environment = env;
195        self
196    }
197
198    /// Use testnet environment.
199    pub fn testnet(mut self) -> Self {
200        self.environment = Environment::Testnet;
201        self
202    }
203
204    /// Use demo trading environment.
205    pub fn demo(mut self) -> Self {
206        self.environment = Environment::Demo;
207        self
208    }
209
210    /// Set the API region (for production only).
211    pub fn region(mut self, region: ApiRegion) -> Self {
212        self.region = region;
213        self
214    }
215
216    /// Set a custom base URL.
217    pub fn base_url(mut self, url: impl Into<String>) -> Self {
218        self.base_url = Some(url.into());
219        self
220    }
221
222    /// Set the recv_window value.
223    pub fn recv_window(mut self, ms: u32) -> Self {
224        self.recv_window = ms;
225        self
226    }
227
228    /// Enable debug mode.
229    pub fn debug(mut self, enabled: bool) -> Self {
230        self.debug = enabled;
231        self
232    }
233
234    /// Set request timeout.
235    pub fn timeout_ms(mut self, ms: u64) -> Self {
236        self.timeout_ms = ms;
237        self
238    }
239
240    /// Set referer header.
241    pub fn referer(mut self, referer: impl Into<String>) -> Self {
242        self.referer = Some(referer.into());
243        self
244    }
245
246    /// Get the effective REST API base URL.
247    pub fn get_rest_url(&self) -> &str {
248        if let Some(ref url) = self.base_url {
249            return url;
250        }
251
252        match self.environment {
253            Environment::Production => self.region.base_url(),
254            Environment::Testnet => rest_urls::TESTNET,
255            Environment::Demo => rest_urls::DEMO,
256        }
257    }
258
259    /// Get the WebSocket URL for public streams.
260    pub fn get_ws_public_url(&self, category: crate::types::Category) -> String {
261        match self.environment {
262            Environment::Production => ws_urls::public_mainnet(category),
263            Environment::Testnet => ws_urls::public_testnet(category),
264            Environment::Demo => ws_urls::public_demo(category),
265        }
266    }
267
268    /// Get the WebSocket URL for private streams.
269    pub fn get_ws_private_url(&self) -> &'static str {
270        match self.environment {
271            Environment::Production => ws_urls::PRIVATE_MAINNET,
272            Environment::Testnet => ws_urls::PRIVATE_TESTNET,
273            Environment::Demo => ws_urls::PRIVATE_DEMO,
274        }
275    }
276
277    /// Get the WebSocket URL for trade API.
278    pub fn get_ws_trade_url(&self) -> &'static str {
279        match self.environment {
280            Environment::Production => ws_urls::TRADE_MAINNET,
281            Environment::Testnet => ws_urls::TRADE_TESTNET,
282            Environment::Demo => ws_urls::TRADE_DEMO,
283        }
284    }
285
286    /// Check if authentication is configured.
287    pub fn has_credentials(&self) -> bool {
288        self.api_key.is_some() && self.api_secret.is_some()
289    }
290
291    /// Get the API secret (for signing).
292    pub(crate) fn get_secret(&self) -> Option<&str> {
293        self.api_secret.as_ref().map(|s| s.expose_secret())
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_default_config() {
303        let config = ClientConfig::default();
304        assert_eq!(config.get_rest_url(), rest_urls::MAINNET);
305        assert!(!config.has_credentials());
306    }
307
308    #[test]
309    fn test_testnet_config() {
310        let config = ClientConfig::new("key", "secret").testnet();
311        assert_eq!(config.get_rest_url(), rest_urls::TESTNET);
312        assert!(config.has_credentials());
313    }
314
315    #[test]
316    fn test_regional_config() {
317        let config = ClientConfig::public_only().region(ApiRegion::HK);
318        assert_eq!(config.get_rest_url(), rest_urls::HK);
319    }
320
321    #[test]
322    fn test_custom_url() {
323        let config = ClientConfig::public_only().base_url("https://custom.api.com");
324        assert_eq!(config.get_rest_url(), "https://custom.api.com");
325    }
326
327    #[test]
328    fn test_ws_urls() {
329        use crate::types::Category;
330
331        let config = ClientConfig::default();
332        assert!(config
333            .get_ws_public_url(Category::Linear)
334            .contains("linear"));
335
336        let testnet = ClientConfig::default().testnet();
337        assert!(testnet.get_ws_private_url().contains("testnet"));
338    }
339}