1use secrecy::{ExposeSecret, SecretString};
4
5pub mod rest_urls {
7 pub const MAINNET: &str = "https://api.bybit.com";
9 pub const MAINNET_BYTICK: &str = "https://api.bytick.com";
11 pub const TESTNET: &str = "https://api-testnet.bybit.com";
13 pub const DEMO: &str = "https://api-demo.bybit.com";
15
16 pub const NL: &str = "https://api.bybit.nl";
18 pub const TK: &str = "https://api.bybit-tr.com";
20 pub const KZ: &str = "https://api.bybit.kz";
22 pub const HK: &str = "https://api.byhkbit.com";
24 pub const GE: &str = "https://api.bybitgeorgia.ge";
26 pub const UAE: &str = "https://api.bybit.ae";
28 pub const EU: &str = "https://api.bybit.eu";
30}
31
32pub mod ws_urls {
34 use super::super::types::Category;
35
36 pub fn public_mainnet(category: Category) -> String {
38 format!("wss://stream.bybit.com/v5/public/{}", category.as_str())
39 }
40
41 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 pub fn public_demo(category: Category) -> String {
51 format!("wss://stream-demo.bybit.com/v5/public/{}", category.as_str())
52 }
53
54 pub const PRIVATE_MAINNET: &str = "wss://stream.bybit.com/v5/private";
56 pub const PRIVATE_TESTNET: &str = "wss://stream-testnet.bybit.com/v5/private";
58 pub const PRIVATE_DEMO: &str = "wss://stream-demo.bybit.com/v5/private";
60
61 pub const TRADE_MAINNET: &str = "wss://stream.bybit.com/v5/trade";
63 pub const TRADE_TESTNET: &str = "wss://stream-testnet.bybit.com/v5/trade";
65 pub const TRADE_DEMO: &str = "wss://stream-demo.bybit.com/v5/trade";
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
71pub enum ApiRegion {
72 #[default]
74 Default,
75 Bytick,
77 NL,
79 TK,
81 KZ,
83 HK,
85 GE,
87 UAE,
89 EU,
91}
92
93impl ApiRegion {
94 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
112pub enum Environment {
113 #[default]
115 Production,
116 Testnet,
118 Demo,
120}
121
122#[derive(Clone)]
124pub struct ClientConfig {
125 pub api_key: Option<String>,
127 api_secret: Option<SecretString>,
129 pub environment: Environment,
131 pub region: ApiRegion,
133 pub base_url: Option<String>,
135 pub recv_window: u32,
137 pub debug: bool,
139 pub timeout_ms: u64,
141 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 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 pub fn public_only() -> Self {
189 Self::default()
190 }
191
192 pub fn environment(mut self, env: Environment) -> Self {
194 self.environment = env;
195 self
196 }
197
198 pub fn testnet(mut self) -> Self {
200 self.environment = Environment::Testnet;
201 self
202 }
203
204 pub fn demo(mut self) -> Self {
206 self.environment = Environment::Demo;
207 self
208 }
209
210 pub fn region(mut self, region: ApiRegion) -> Self {
212 self.region = region;
213 self
214 }
215
216 pub fn base_url(mut self, url: impl Into<String>) -> Self {
218 self.base_url = Some(url.into());
219 self
220 }
221
222 pub fn recv_window(mut self, ms: u32) -> Self {
224 self.recv_window = ms;
225 self
226 }
227
228 pub fn debug(mut self, enabled: bool) -> Self {
230 self.debug = enabled;
231 self
232 }
233
234 pub fn timeout_ms(mut self, ms: u64) -> Self {
236 self.timeout_ms = ms;
237 self
238 }
239
240 pub fn referer(mut self, referer: impl Into<String>) -> Self {
242 self.referer = Some(referer.into());
243 self
244 }
245
246 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 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 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 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 pub fn has_credentials(&self) -> bool {
288 self.api_key.is_some() && self.api_secret.is_some()
289 }
290
291 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}