rusty_ems/exchanges/
bithumb_config.rs

1//! Bithumb exchange configuration and constants
2
3use crate::exchanges::bithumb_errors::{BithumbError, BithumbResult};
4use rusty_common::SmartString;
5use std::time::Duration;
6
7/// Bithumb API constants
8pub mod constants {
9    /// Production API URL
10    pub const API_URL: &str = "https://api.bithumb.com";
11
12    /// Production WebSocket URL
13    pub const WS_URL: &str = "wss://ws-api.bithumb.com/websocket/v1";
14
15    /// Default request timeout
16    pub const DEFAULT_TIMEOUT_MS: u64 = 10000;
17
18    /// Default maximum retries
19    pub const DEFAULT_MAX_RETRIES: usize = 3;
20
21    /// Default ping interval
22    pub const DEFAULT_PING_INTERVAL_MS: u64 = 30000;
23
24    /// Default rate limit window
25    pub const DEFAULT_RATE_LIMIT_WINDOW_MS: u64 = 1000;
26
27    /// Default rate limit per window
28    pub const DEFAULT_RATE_LIMIT_PER_WINDOW: u32 = 100;
29
30    /// Supported currency pairs
31    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    /// Default minimum order amount for all currencies (in KRW)
176    pub const DEFAULT_MIN_ORDER_AMOUNT: u64 = 10000;
177
178    /// Generate minimum order amounts from supported pairs
179    /// All supported pairs have the same minimum order amount
180    #[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/// Bithumb exchange configuration
190#[derive(Debug, Clone)]
191pub struct BithumbConfig {
192    /// API base URL
193    pub api_url: SmartString,
194
195    /// WebSocket URL
196    pub ws_url: SmartString,
197
198    /// Request timeout duration
199    pub timeout: Duration,
200
201    /// Maximum retry attempts
202    pub max_retries: usize,
203
204    /// Ping interval for WebSocket
205    pub ping_interval: Duration,
206
207    /// Enable testnet mode
208    pub enable_testnet: bool,
209
210    /// Rate limit window duration
211    pub rate_limit_window: Duration,
212
213    /// Rate limit per window
214    pub rate_limit_per_window: u32,
215
216    /// User agent string for requests
217    pub user_agent: SmartString,
218
219    /// Connection pool settings
220    pub connection_pool_size: usize,
221
222    /// Keep-alive timeout
223    pub keep_alive_timeout: Duration,
224
225    /// Enable request/response logging
226    pub enable_logging: bool,
227
228    /// Enable performance metrics
229    pub enable_metrics: bool,
230
231    /// WebSocket reconnection settings
232    pub ws_reconnect_attempts: usize,
233    /// Delay between WebSocket reconnection attempts
234    pub ws_reconnect_delay: Duration,
235
236    /// Order validation settings
237    pub validate_orders: bool,
238    /// Enable minimum order amount validation
239    pub min_order_validation: bool,
240}
241
242impl BithumbConfig {
243    /// Create a new configuration with default values
244    #[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    /// Create configuration for testnet
268    ///
269    /// # Note
270    ///
271    /// Bithumb does not support a testnet environment. This method returns
272    /// a production configuration and logs a warning. Use `production()` instead.
273    #[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    /// Create configuration for production
280    #[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    /// Create configuration for high-frequency trading
290    #[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    /// Set API URL
303    pub fn with_api_url(mut self, url: impl Into<SmartString>) -> Self {
304        self.api_url = url.into();
305        self
306    }
307
308    /// Set WebSocket URL
309    pub fn with_ws_url(mut self, url: impl Into<SmartString>) -> Self {
310        self.ws_url = url.into();
311        self
312    }
313
314    /// Set timeout
315    #[must_use]
316    pub const fn with_timeout(mut self, timeout: Duration) -> Self {
317        self.timeout = timeout;
318        self
319    }
320
321    /// Set maximum retries
322    #[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    /// Set ping interval
329    #[must_use]
330    pub const fn with_ping_interval(mut self, interval: Duration) -> Self {
331        self.ping_interval = interval;
332        self
333    }
334
335    /// Enable testnet mode
336    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    /// Set rate limiting
347    #[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    /// Set user agent
355    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    /// Set connection pool size
361    #[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    /// Enable logging
368    #[must_use]
369    pub const fn with_logging(mut self, enable: bool) -> Self {
370        self.enable_logging = enable;
371        self
372    }
373
374    /// Enable metrics
375    #[must_use]
376    pub const fn with_metrics(mut self, enable: bool) -> Self {
377        self.enable_metrics = enable;
378        self
379    }
380
381    /// Set WebSocket reconnection settings
382    #[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    /// Set order validation
390    #[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    /// Validate configuration
398    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
441/// Helper functions for configuration
442impl BithumbConfig {
443    /// Check if symbol is supported
444    #[must_use]
445    pub fn is_symbol_supported(&self, symbol: &str) -> bool {
446        constants::SUPPORTED_PAIRS.contains(&symbol)
447    }
448
449    /// Get minimum order amount for symbol
450    #[must_use]
451    pub fn get_min_order_amount(&self, symbol: &str) -> Option<u64> {
452        // All supported pairs have the same minimum order amount
453        if constants::SUPPORTED_PAIRS.contains(&symbol) {
454            Some(constants::DEFAULT_MIN_ORDER_AMOUNT)
455        } else {
456            None
457        }
458    }
459
460    /// Get effective API URL based on testnet setting
461    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    /// Get effective WebSocket URL based on testnet setting
471    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    /// Get HTTP client timeout
481    #[must_use]
482    pub const fn get_http_timeout(&self) -> Duration {
483        self.timeout
484    }
485
486    /// Get WebSocket ping interval
487    #[must_use]
488    pub const fn get_ws_ping_interval(&self) -> Duration {
489        self.ping_interval
490    }
491
492    /// Get rate limit settings
493    #[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    /// Get connection pool settings
499    #[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    /// Get WebSocket reconnection settings
505    #[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        // testnet() returns production config with a warning
531        let config = BithumbConfig::testnet();
532        // Should return production configuration
533        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}