1use crate::exchanges::bithumb_errors::{BithumbError, BithumbResult};
4use rusty_common::SmartString;
5use std::time::Duration;
6
7pub mod constants {
9 pub const API_URL: &str = "https://api.bithumb.com";
11
12 pub const WS_URL: &str = "wss://ws-api.bithumb.com/websocket/v1";
14
15 pub const DEFAULT_TIMEOUT_MS: u64 = 10000;
17
18 pub const DEFAULT_MAX_RETRIES: usize = 3;
20
21 pub const DEFAULT_PING_INTERVAL_MS: u64 = 30000;
23
24 pub const DEFAULT_RATE_LIMIT_WINDOW_MS: u64 = 1000;
26
27 pub const DEFAULT_RATE_LIMIT_PER_WINDOW: u32 = 100;
29
30 pub const SUPPORTED_PAIRS: &[&str] = &[
32 "BTC_KRW",
33 "ETH_KRW",
34 "XRP_KRW",
35 "BCH_KRW",
36 "EOS_KRW",
37 "LTC_KRW",
38 "TRX_KRW",
39 "ETC_KRW",
40 "LINK_KRW",
41 "DOT_KRW",
42 "ADA_KRW",
43 "SOL_KRW",
44 "DOGE_KRW",
45 "AVAX_KRW",
46 "SHIB_KRW",
47 "MATIC_KRW",
48 "ATOM_KRW",
49 "NEAR_KRW",
50 "AAVE_KRW",
51 "AXS_KRW",
52 "SAND_KRW",
53 "MANA_KRW",
54 "CHZ_KRW",
55 "FLOW_KRW",
56 "KLAY_KRW",
57 "1INCH_KRW",
58 "CRV_KRW",
59 "ENJ_KRW",
60 "VET_KRW",
61 "THETA_KRW",
62 "COMP_KRW",
63 "SRM_KRW",
64 "ALICE_KRW",
65 "ONG_KRW",
66 "JST_KRW",
67 "CTC_KRW",
68 "MED_KRW",
69 "WEMIX_KRW",
70 "SOC_KRW",
71 "TEMCO_KRW",
72 "HIBS_KRW",
73 "BURGER_KRW",
74 "DODO_KRW",
75 "CAKE_KRW",
76 "MILK_KRW",
77 "BAT_KRW",
78 "WICC_KRW",
79 "WOM_KRW",
80 "FLOKI_KRW",
81 "USDT_KRW",
82 "USDC_KRW",
83 "BUSD_KRW",
84 "XLM_KRW",
85 "ALGO_KRW",
86 "QTUM_KRW",
87 "ICX_KRW",
88 "ONT_KRW",
89 "ZIL_KRW",
90 "ANKR_KRW",
91 "STORJ_KRW",
92 "POLY_KRW",
93 "RSR_KRW",
94 "BORA_KRW",
95 "STPT_KRW",
96 "HUNT_KRW",
97 "PLA_KRW",
98 "ORBS_KRW",
99 "MVL_KRW",
100 "STRAX_KRW",
101 "AQT_KRW",
102 "GLM_KRW",
103 "PUNDIX_KRW",
104 "LOOM_KRW",
105 "CELR_KRW",
106 "HIVE_KRW",
107 "KAVA_KRW",
108 "AHT_KRW",
109 "BLUR_KRW",
110 "APT_KRW",
111 "SUI_KRW",
112 "PEPE_KRW",
113 "ARB_KRW",
114 "MASK_KRW",
115 "AGIX_KRW",
116 "FET_KRW",
117 "RNDR_KRW",
118 "GRT_KRW",
119 "IMX_KRW",
120 "MINA_KRW",
121 "CKB_KRW",
122 "WAXP_KRW",
123 "HBAR_KRW",
124 "UPP_KRW",
125 "BAL_KRW",
126 "ARDR_KRW",
127 "MSB_KRW",
128 "TFUEL_KRW",
129 "AERGO_KRW",
130 "CTSI_KRW",
131 "BOA_KRW",
132 "JASMY_KRW",
133 "BIOT_KRW",
134 "ASTR_KRW",
135 "GMT_KRW",
136 "STEPN_KRW",
137 "APE_KRW",
138 "T_KRW",
139 "BIGTIME_KRW",
140 "TIA_KRW",
141 "ZETA_KRW",
142 "JTO_KRW",
143 "PYTH_KRW",
144 "BONK_KRW",
145 "JUP_KRW",
146 "STRK_KRW",
147 "WIF_KRW",
148 "ETHFI_KRW",
149 "BOME_KRW",
150 "ENA_KRW",
151 "REZ_KRW",
152 "OMNI_KRW",
153 "BB_KRW",
154 "IO_KRW",
155 "ZK_KRW",
156 "LISTA_KRW",
157 "ZRO_KRW",
158 "BANANA_KRW",
159 "RENDER_KRW",
160 "PEOPLE_KRW",
161 "NOT_KRW",
162 "TON_KRW",
163 "DOGS_KRW",
164 "HAMSTER_KRW",
165 "CATI_KRW",
166 "HMSTR_KRW",
167 "NEIRO_KRW",
168 "TURBO_KRW",
169 "MOODENG_KRW",
170 "GOAT_KRW",
171 "PNUT_KRW",
172 "ACT_KRW",
173 ];
174
175 pub const DEFAULT_MIN_ORDER_AMOUNT: u64 = 10000;
177
178 #[must_use]
181 pub fn generate_min_order_amounts() -> Vec<(&'static str, u64)> {
182 SUPPORTED_PAIRS
183 .iter()
184 .map(|&pair| (pair, DEFAULT_MIN_ORDER_AMOUNT))
185 .collect()
186 }
187}
188
189#[derive(Debug, Clone)]
191pub struct BithumbConfig {
192 pub api_url: SmartString,
194
195 pub ws_url: SmartString,
197
198 pub timeout: Duration,
200
201 pub max_retries: usize,
203
204 pub ping_interval: Duration,
206
207 pub enable_testnet: bool,
209
210 pub rate_limit_window: Duration,
212
213 pub rate_limit_per_window: u32,
215
216 pub user_agent: SmartString,
218
219 pub connection_pool_size: usize,
221
222 pub keep_alive_timeout: Duration,
224
225 pub enable_logging: bool,
227
228 pub enable_metrics: bool,
230
231 pub ws_reconnect_attempts: usize,
233 pub ws_reconnect_delay: Duration,
235
236 pub validate_orders: bool,
238 pub min_order_validation: bool,
240}
241
242impl BithumbConfig {
243 #[must_use]
245 pub fn new() -> Self {
246 Self {
247 api_url: constants::API_URL.into(),
248 ws_url: constants::WS_URL.into(),
249 timeout: Duration::from_millis(constants::DEFAULT_TIMEOUT_MS),
250 max_retries: constants::DEFAULT_MAX_RETRIES,
251 ping_interval: Duration::from_millis(constants::DEFAULT_PING_INTERVAL_MS),
252 enable_testnet: false,
253 rate_limit_window: Duration::from_millis(constants::DEFAULT_RATE_LIMIT_WINDOW_MS),
254 rate_limit_per_window: constants::DEFAULT_RATE_LIMIT_PER_WINDOW,
255 user_agent: "rusty-ems/1.0".into(),
256 connection_pool_size: 10,
257 keep_alive_timeout: Duration::from_secs(30),
258 enable_logging: false,
259 enable_metrics: true,
260 ws_reconnect_attempts: 5,
261 ws_reconnect_delay: Duration::from_secs(5),
262 validate_orders: true,
263 min_order_validation: true,
264 }
265 }
266
267 #[must_use]
274 pub fn testnet() -> Self {
275 log::warn!("Bithumb does not support testnet - returning production configuration instead");
276 Self::production()
277 }
278
279 #[must_use]
281 pub fn production() -> Self {
282 let mut config = Self::new();
283 config.enable_testnet = false;
284 config.enable_logging = false;
285 config.enable_metrics = true;
286 config
287 }
288
289 #[must_use]
291 pub fn high_frequency() -> Self {
292 let mut config = Self::new();
293 config.timeout = Duration::from_millis(1000);
294 config.ping_interval = Duration::from_millis(10000);
295 config.rate_limit_per_window = 1000;
296 config.connection_pool_size = 50;
297 config.keep_alive_timeout = Duration::from_secs(10);
298 config.ws_reconnect_delay = Duration::from_millis(500);
299 config
300 }
301
302 pub fn with_api_url(mut self, url: impl Into<SmartString>) -> Self {
304 self.api_url = url.into();
305 self
306 }
307
308 pub fn with_ws_url(mut self, url: impl Into<SmartString>) -> Self {
310 self.ws_url = url.into();
311 self
312 }
313
314 #[must_use]
316 pub const fn with_timeout(mut self, timeout: Duration) -> Self {
317 self.timeout = timeout;
318 self
319 }
320
321 #[must_use]
323 pub const fn with_max_retries(mut self, max_retries: usize) -> Self {
324 self.max_retries = max_retries;
325 self
326 }
327
328 #[must_use]
330 pub const fn with_ping_interval(mut self, interval: Duration) -> Self {
331 self.ping_interval = interval;
332 self
333 }
334
335 pub fn with_testnet(mut self, enable: bool) -> BithumbResult<Self> {
337 if enable {
338 return Err(BithumbError::configuration(
339 "Bithumb does not support testnet - testnet cannot be enabled",
340 ));
341 }
342 self.enable_testnet = enable;
343 Ok(self)
344 }
345
346 #[must_use]
348 pub const fn with_rate_limit(mut self, window: Duration, per_window: u32) -> Self {
349 self.rate_limit_window = window;
350 self.rate_limit_per_window = per_window;
351 self
352 }
353
354 pub fn with_user_agent(mut self, user_agent: impl Into<SmartString>) -> Self {
356 self.user_agent = user_agent.into();
357 self
358 }
359
360 #[must_use]
362 pub const fn with_connection_pool_size(mut self, size: usize) -> Self {
363 self.connection_pool_size = size;
364 self
365 }
366
367 #[must_use]
369 pub const fn with_logging(mut self, enable: bool) -> Self {
370 self.enable_logging = enable;
371 self
372 }
373
374 #[must_use]
376 pub const fn with_metrics(mut self, enable: bool) -> Self {
377 self.enable_metrics = enable;
378 self
379 }
380
381 #[must_use]
383 pub const fn with_ws_reconnect(mut self, attempts: usize, delay: Duration) -> Self {
384 self.ws_reconnect_attempts = attempts;
385 self.ws_reconnect_delay = delay;
386 self
387 }
388
389 #[must_use]
391 pub const fn with_order_validation(mut self, validate: bool, min_validate: bool) -> Self {
392 self.validate_orders = validate;
393 self.min_order_validation = min_validate;
394 self
395 }
396
397 pub fn validate(&self) -> Result<(), String> {
399 if self.api_url.is_empty() {
400 return Err("API URL cannot be empty".to_string());
401 }
402
403 if self.ws_url.is_empty() {
404 return Err("WebSocket URL cannot be empty".to_string());
405 }
406
407 if self.timeout.as_millis() < 100 {
408 return Err("Timeout must be at least 100ms".to_string());
409 }
410
411 if self.max_retries == 0 {
412 return Err("Max retries must be at least 1".to_string());
413 }
414
415 if self.ping_interval.as_millis() < 1000 {
416 return Err("Ping interval must be at least 1000ms".to_string());
417 }
418
419 if self.rate_limit_per_window == 0 {
420 return Err("Rate limit per window must be at least 1".to_string());
421 }
422
423 if self.connection_pool_size == 0 {
424 return Err("Connection pool size must be at least 1".to_string());
425 }
426
427 if self.ws_reconnect_attempts == 0 {
428 return Err("WebSocket reconnect attempts must be at least 1".to_string());
429 }
430
431 Ok(())
432 }
433}
434
435impl Default for BithumbConfig {
436 fn default() -> Self {
437 Self::new()
438 }
439}
440
441impl BithumbConfig {
443 #[must_use]
445 pub fn is_symbol_supported(&self, symbol: &str) -> bool {
446 constants::SUPPORTED_PAIRS.contains(&symbol)
447 }
448
449 #[must_use]
451 pub fn get_min_order_amount(&self, symbol: &str) -> Option<u64> {
452 if constants::SUPPORTED_PAIRS.contains(&symbol) {
454 Some(constants::DEFAULT_MIN_ORDER_AMOUNT)
455 } else {
456 None
457 }
458 }
459
460 pub fn get_api_url(&self) -> BithumbResult<&SmartString> {
462 if self.enable_testnet {
463 return Err(BithumbError::configuration(
464 "Bithumb does not support testnet - testnet functionality is not available",
465 ));
466 }
467 Ok(&self.api_url)
468 }
469
470 pub fn get_ws_url(&self) -> BithumbResult<&SmartString> {
472 if self.enable_testnet {
473 return Err(BithumbError::configuration(
474 "Bithumb does not support testnet - testnet functionality is not available",
475 ));
476 }
477 Ok(&self.ws_url)
478 }
479
480 #[must_use]
482 pub const fn get_http_timeout(&self) -> Duration {
483 self.timeout
484 }
485
486 #[must_use]
488 pub const fn get_ws_ping_interval(&self) -> Duration {
489 self.ping_interval
490 }
491
492 #[must_use]
494 pub const fn get_rate_limit_settings(&self) -> (Duration, u32) {
495 (self.rate_limit_window, self.rate_limit_per_window)
496 }
497
498 #[must_use]
500 pub const fn get_connection_pool_settings(&self) -> (usize, Duration) {
501 (self.connection_pool_size, self.keep_alive_timeout)
502 }
503
504 #[must_use]
506 pub const fn get_ws_reconnect_settings(&self) -> (usize, Duration) {
507 (self.ws_reconnect_attempts, self.ws_reconnect_delay)
508 }
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514
515 #[test]
516 fn test_default_config() {
517 let config = BithumbConfig::default();
518 assert_eq!(config.api_url, constants::API_URL);
519 assert_eq!(config.ws_url, constants::WS_URL);
520 assert_eq!(
521 config.timeout,
522 Duration::from_millis(constants::DEFAULT_TIMEOUT_MS)
523 );
524 assert_eq!(config.max_retries, constants::DEFAULT_MAX_RETRIES);
525 assert!(!config.enable_testnet);
526 }
527
528 #[test]
529 fn test_testnet_config() {
530 let config = BithumbConfig::testnet();
532 assert!(!config.enable_testnet);
534 assert_eq!(config.api_url, constants::API_URL);
535 }
536
537 #[test]
538 fn test_production_config() {
539 let config = BithumbConfig::production();
540 assert!(!config.enable_testnet);
541 assert!(!config.enable_logging);
542 assert!(config.enable_metrics);
543 }
544
545 #[test]
546 fn test_high_frequency_config() {
547 let config = BithumbConfig::high_frequency();
548 assert_eq!(config.timeout, Duration::from_millis(1000));
549 assert_eq!(config.ping_interval, Duration::from_millis(10000));
550 assert_eq!(config.rate_limit_per_window, 1000);
551 assert_eq!(config.connection_pool_size, 50);
552 }
553
554 #[test]
555 fn test_builder_pattern_with_testnet() {
556 let result = BithumbConfig::new()
557 .with_api_url("https://test.api.com")
558 .with_timeout(Duration::from_millis(5000))
559 .with_max_retries(5)
560 .with_testnet(true);
561
562 assert!(result.is_err());
563 let err = result.unwrap_err();
564 assert!(matches!(err, BithumbError::Configuration { .. }));
565 assert!(err.to_string().contains("Bithumb does not support testnet"));
566 }
567
568 #[test]
569 fn test_builder_pattern() {
570 let config = BithumbConfig::new()
571 .with_api_url("https://test.api.com")
572 .with_timeout(Duration::from_millis(5000))
573 .with_max_retries(5)
574 .with_testnet(false)
575 .expect("Should create config with testnet=false")
576 .with_logging(true);
577
578 assert_eq!(config.api_url, "https://test.api.com");
579 assert_eq!(config.timeout, Duration::from_millis(5000));
580 assert_eq!(config.max_retries, 5);
581 assert!(!config.enable_testnet);
582 assert!(config.enable_logging);
583 }
584
585 #[test]
586 fn test_config_validation() {
587 let mut config = BithumbConfig::new();
588 assert!(config.validate().is_ok());
589
590 config.api_url = "".into();
591 assert!(config.validate().is_err());
592
593 config.api_url = "https://api.test.com".into();
594 config.timeout = Duration::from_millis(50);
595 assert!(config.validate().is_err());
596 }
597
598 #[test]
599 fn test_symbol_support() {
600 let config = BithumbConfig::new();
601 assert!(config.is_symbol_supported("BTC_KRW"));
602 assert!(config.is_symbol_supported("ETH_KRW"));
603 assert!(!config.is_symbol_supported("INVALID_PAIR"));
604 }
605
606 #[test]
607 fn test_min_order_amount() {
608 let config = BithumbConfig::new();
609 assert_eq!(config.get_min_order_amount("BTC_KRW"), Some(10000));
610 assert_eq!(config.get_min_order_amount("ETH_KRW"), Some(10000));
611 assert_eq!(config.get_min_order_amount("INVALID_PAIR"), None);
612 }
613}