rusty_ems/exchanges/
bithumb_errors.rs

1//! Bithumb-specific error types for better error handling
2
3use crate::error::EMSError;
4use rusty_common::SmartString;
5use thiserror::Error;
6
7/// Bithumb-specific error types
8#[derive(Debug, Error)]
9pub enum BithumbError {
10    /// Authentication failed
11    #[error("Authentication failed: {message}")]
12    Authentication {
13        /// Error message describing the authentication failure
14        message: SmartString,
15    },
16
17    /// Invalid symbol format
18    #[error("Invalid symbol format: {symbol}")]
19    InvalidSymbol {
20        /// The invalid symbol that was provided
21        symbol: SmartString,
22    },
23
24    /// Connection failed
25    #[error("Connection failed: {message}")]
26    Connection {
27        /// Error message describing the connection failure
28        message: SmartString,
29    },
30
31    /// API error from Bithumb
32    #[error("API error: {code} - {message}")]
33    Api {
34        /// Error code returned by the API
35        code: SmartString,
36        /// Error message returned by the API
37        message: SmartString,
38    },
39
40    /// Order operation failed
41    #[error("Order operation failed: {operation} - {reason}")]
42    OrderOperation {
43        /// The operation that failed (e.g., place_order, cancel_order)
44        operation: SmartString,
45        /// The reason for the failure
46        reason: SmartString,
47    },
48
49    /// Invalid order status
50    #[error("Invalid order status: {status}")]
51    InvalidOrderStatus {
52        /// The invalid order status received
53        status: SmartString,
54    },
55
56    /// Rate limit exceeded
57    #[error("Rate limit exceeded: {message}")]
58    RateLimit {
59        /// Message describing the rate limit violation
60        message: SmartString,
61    },
62
63    /// Insufficient balance
64    #[error("Insufficient balance: {currency} required: {required}, available: {available}")]
65    InsufficientBalance {
66        /// The currency for which balance is insufficient
67        currency: SmartString,
68        /// The required amount
69        required: SmartString,
70        /// The available amount
71        available: SmartString,
72    },
73
74    /// Invalid market type
75    #[error("Invalid market type: {market_type}")]
76    InvalidMarketType {
77        /// The invalid market type provided
78        market_type: SmartString,
79    },
80
81    /// WebSocket connection error
82    #[error("WebSocket connection error: {message}")]
83    WebSocketConnection {
84        /// Error message describing the WebSocket connection issue
85        message: SmartString,
86    },
87
88    /// JSON parsing error
89    #[error("JSON parsing error: {message}")]
90    JsonParsing {
91        /// Error message describing the JSON parsing issue
92        message: SmartString,
93    },
94
95    /// Configuration error
96    #[error("Configuration error: {message}")]
97    Configuration {
98        /// Error message describing the configuration issue
99        message: SmartString,
100    },
101
102    /// Network timeout
103    #[error("Network timeout: operation took longer than {timeout_ms}ms")]
104    Timeout {
105        /// The timeout duration in milliseconds
106        timeout_ms: u64,
107    },
108
109    /// Internal system error
110    #[error("Internal system error: {message}")]
111    Internal {
112        /// Error message describing the internal system issue
113        message: SmartString,
114    },
115}
116
117impl From<BithumbError> for EMSError {
118    fn from(err: BithumbError) -> Self {
119        match err {
120            BithumbError::Authentication { message } => Self::auth(message),
121            BithumbError::InvalidSymbol { symbol } => {
122                Self::invalid_params(format!("Invalid symbol: {symbol}"))
123            }
124            BithumbError::Connection { message } => Self::connection(message),
125            BithumbError::Api { code, message } => Self::exchange_api(
126                "Bithumb",
127                code.parse().unwrap_or(0),
128                message,
129                None::<String>,
130            ),
131            BithumbError::OrderOperation { operation, reason } => {
132                Self::order_submission(format!("{operation}: {reason}"))
133            }
134            BithumbError::InvalidOrderStatus { status } => {
135                Self::invalid_params(format!("Invalid order status: {status}"))
136            }
137            BithumbError::RateLimit { message } => Self::rate_limit(message, None),
138            BithumbError::InsufficientBalance {
139                currency,
140                required,
141                available,
142            } => Self::order_submission(format!(
143                "Insufficient {currency} balance: required {required}, available {available}"
144            )),
145            BithumbError::InvalidMarketType { market_type } => {
146                Self::invalid_params(format!("Invalid market type: {market_type}"))
147            }
148            BithumbError::WebSocketConnection { message } => Self::websocket(message),
149            BithumbError::JsonParsing { message } => Self::json_parse("Bithumb response", message),
150            BithumbError::Configuration { message } => {
151                Self::invalid_params(format!("Configuration error: {message}"))
152            }
153            BithumbError::Timeout { timeout_ms } => {
154                Self::timeout(timeout_ms, "Bithumb operation timeout")
155            }
156            BithumbError::Internal { message } => Self::internal(message),
157        }
158    }
159}
160
161/// Bithumb-specific result type
162pub type BithumbResult<T> = Result<T, BithumbError>;
163
164/// Helper functions for creating common errors
165impl BithumbError {
166    /// Create authentication error
167    pub fn auth(message: impl Into<SmartString>) -> Self {
168        Self::Authentication {
169            message: message.into(),
170        }
171    }
172
173    /// Create invalid symbol error
174    pub fn invalid_symbol(symbol: impl Into<SmartString>) -> Self {
175        Self::InvalidSymbol {
176            symbol: symbol.into(),
177        }
178    }
179
180    /// Create connection error
181    pub fn connection(message: impl Into<SmartString>) -> Self {
182        Self::Connection {
183            message: message.into(),
184        }
185    }
186
187    /// Create API error
188    pub fn api(code: impl Into<SmartString>, message: impl Into<SmartString>) -> Self {
189        Self::Api {
190            code: code.into(),
191            message: message.into(),
192        }
193    }
194
195    /// Create order operation error
196    pub fn order_operation(
197        operation: impl Into<SmartString>,
198        reason: impl Into<SmartString>,
199    ) -> Self {
200        Self::OrderOperation {
201            operation: operation.into(),
202            reason: reason.into(),
203        }
204    }
205
206    /// Create invalid order status error
207    pub fn invalid_order_status(status: impl Into<SmartString>) -> Self {
208        Self::InvalidOrderStatus {
209            status: status.into(),
210        }
211    }
212
213    /// Create rate limit error
214    pub fn rate_limit(message: impl Into<SmartString>) -> Self {
215        Self::RateLimit {
216            message: message.into(),
217        }
218    }
219
220    /// Create insufficient balance error
221    pub fn insufficient_balance(
222        currency: impl Into<SmartString>,
223        required: impl Into<SmartString>,
224        available: impl Into<SmartString>,
225    ) -> Self {
226        Self::InsufficientBalance {
227            currency: currency.into(),
228            required: required.into(),
229            available: available.into(),
230        }
231    }
232
233    /// Create WebSocket connection error
234    pub fn websocket(message: impl Into<SmartString>) -> Self {
235        Self::WebSocketConnection {
236            message: message.into(),
237        }
238    }
239
240    /// Create JSON parsing error
241    pub fn json_parsing(message: impl Into<SmartString>) -> Self {
242        Self::JsonParsing {
243            message: message.into(),
244        }
245    }
246
247    /// Create configuration error
248    pub fn configuration(message: impl Into<SmartString>) -> Self {
249        Self::Configuration {
250            message: message.into(),
251        }
252    }
253
254    /// Create timeout error
255    #[must_use]
256    pub const fn timeout(timeout_ms: u64) -> Self {
257        Self::Timeout { timeout_ms }
258    }
259
260    /// Create internal error
261    pub fn internal(message: impl Into<SmartString>) -> Self {
262        Self::Internal {
263            message: message.into(),
264        }
265    }
266}
267
268/// Parse Bithumb API error response
269#[must_use]
270pub fn parse_bithumb_api_error(response: &simd_json::value::owned::Value) -> Option<BithumbError> {
271    use simd_json::prelude::*;
272
273    if let Some(status) = response.get("status")
274        && let Some(status_str) = status.as_str()
275        && status_str != "0000"
276    {
277        let message = response
278            .get("message")
279            .and_then(|m| m.as_str())
280            .unwrap_or("Unknown error");
281        return Some(BithumbError::api(status_str, message));
282    }
283    None
284}
285
286/// Validate Bithumb symbol format
287pub fn validate_symbol(symbol: &str) -> BithumbResult<(SmartString, SmartString)> {
288    if let Some((base, quote)) = symbol.split_once('_') {
289        if !base.is_empty() && !quote.is_empty() {
290            Ok((base.into(), quote.into()))
291        } else {
292            Err(BithumbError::invalid_symbol(symbol))
293        }
294    } else {
295        Err(BithumbError::invalid_symbol(symbol))
296    }
297}
298
299/// Map Bithumb numeric order status codes to internal representation
300pub fn map_order_status(status: &str) -> BithumbResult<rusty_model::enums::OrderStatus> {
301    match status {
302        "0" => Ok(rusty_model::enums::OrderStatus::New),
303        "1" => Ok(rusty_model::enums::OrderStatus::PartiallyFilled),
304        "2" => Ok(rusty_model::enums::OrderStatus::Filled),
305        "3" => Ok(rusty_model::enums::OrderStatus::Cancelled),
306        "4" => Ok(rusty_model::enums::OrderStatus::Rejected),
307        _ => Err(BithumbError::invalid_order_status(status)),
308    }
309}
310
311/// Map Bithumb WebSocket order status strings to internal representation
312pub fn map_websocket_order_status(status: &str) -> BithumbResult<rusty_model::enums::OrderStatus> {
313    match status {
314        "placed" => Ok(rusty_model::enums::OrderStatus::New),
315        "pending" => Ok(rusty_model::enums::OrderStatus::Open),
316        "partial" => Ok(rusty_model::enums::OrderStatus::PartiallyFilled),
317        "completed" => Ok(rusty_model::enums::OrderStatus::Filled),
318        "cancelled" => Ok(rusty_model::enums::OrderStatus::Cancelled),
319        "rejected" => Ok(rusty_model::enums::OrderStatus::Rejected),
320        _ => Err(BithumbError::invalid_order_status(status)),
321    }
322}
323
324/// Map Bithumb WebSocket order state field to internal representation
325pub fn map_websocket_order_state(state: &str) -> BithumbResult<rusty_model::enums::OrderStatus> {
326    match state {
327        "wait" => Ok(rusty_model::enums::OrderStatus::Open), // Waiting for execution
328        "trade" => Ok(rusty_model::enums::OrderStatus::PartiallyFilled), // Trade occurred
329        "done" => Ok(rusty_model::enums::OrderStatus::Filled), // Fully filled
330        "cancel" => Ok(rusty_model::enums::OrderStatus::Cancelled), // Cancelled
331        _ => Err(BithumbError::invalid_order_status(state)),
332    }
333}
334
335/// Validate Bithumb order side
336pub fn validate_order_side(side: &str) -> BithumbResult<rusty_model::enums::OrderSide> {
337    match side.to_lowercase().as_str() {
338        "buy" | "bid" => Ok(rusty_model::enums::OrderSide::Buy),
339        "sell" | "ask" => Ok(rusty_model::enums::OrderSide::Sell),
340        _ => Err(BithumbError::invalid_order_status(format!(
341            "Invalid order side: {side}"
342        ))),
343    }
344}
345
346/// Validate Bithumb order type
347pub fn validate_order_type(order_type: &str) -> BithumbResult<rusty_model::enums::OrderType> {
348    match order_type.to_lowercase().as_str() {
349        "limit" => Ok(rusty_model::enums::OrderType::Limit),
350        "market" => Ok(rusty_model::enums::OrderType::Market),
351        _ => Err(BithumbError::invalid_order_status(format!(
352            "Invalid order type: {order_type}"
353        ))),
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360
361    #[test]
362    fn test_validate_symbol_success() {
363        let result = validate_symbol("BTC_KRW");
364        assert!(result.is_ok());
365        let (base, quote) = result.unwrap();
366        assert_eq!(base, "BTC");
367        assert_eq!(quote, "KRW");
368    }
369
370    #[test]
371    fn test_validate_symbol_failure() {
372        assert!(validate_symbol("INVALID").is_err());
373        assert!(validate_symbol("").is_err());
374        assert!(validate_symbol("BTC_").is_err());
375        assert!(validate_symbol("_KRW").is_err());
376    }
377
378    #[test]
379    fn test_map_order_status() {
380        assert_eq!(
381            map_order_status("0").unwrap(),
382            rusty_model::enums::OrderStatus::New
383        );
384        assert_eq!(
385            map_order_status("1").unwrap(),
386            rusty_model::enums::OrderStatus::PartiallyFilled
387        );
388        assert_eq!(
389            map_order_status("2").unwrap(),
390            rusty_model::enums::OrderStatus::Filled
391        );
392        assert_eq!(
393            map_order_status("3").unwrap(),
394            rusty_model::enums::OrderStatus::Cancelled
395        );
396        assert_eq!(
397            map_order_status("4").unwrap(),
398            rusty_model::enums::OrderStatus::Rejected
399        );
400
401        assert!(map_order_status("999").is_err());
402    }
403
404    #[test]
405    fn test_validate_order_side() {
406        assert_eq!(
407            validate_order_side("buy").unwrap(),
408            rusty_model::enums::OrderSide::Buy
409        );
410        assert_eq!(
411            validate_order_side("BUY").unwrap(),
412            rusty_model::enums::OrderSide::Buy
413        );
414        assert_eq!(
415            validate_order_side("sell").unwrap(),
416            rusty_model::enums::OrderSide::Sell
417        );
418        assert_eq!(
419            validate_order_side("SELL").unwrap(),
420            rusty_model::enums::OrderSide::Sell
421        );
422
423        assert!(validate_order_side("invalid").is_err());
424    }
425
426    #[test]
427    fn test_validate_order_type() {
428        assert_eq!(
429            validate_order_type("limit").unwrap(),
430            rusty_model::enums::OrderType::Limit
431        );
432        assert_eq!(
433            validate_order_type("LIMIT").unwrap(),
434            rusty_model::enums::OrderType::Limit
435        );
436        assert_eq!(
437            validate_order_type("market").unwrap(),
438            rusty_model::enums::OrderType::Market
439        );
440        assert_eq!(
441            validate_order_type("MARKET").unwrap(),
442            rusty_model::enums::OrderType::Market
443        );
444
445        assert!(validate_order_type("invalid").is_err());
446    }
447}