1use crate::error::EMSError;
4use rusty_common::SmartString;
5use thiserror::Error;
6
7#[derive(Debug, Error)]
9pub enum BithumbError {
10 #[error("Authentication failed: {message}")]
12 Authentication {
13 message: SmartString,
15 },
16
17 #[error("Invalid symbol format: {symbol}")]
19 InvalidSymbol {
20 symbol: SmartString,
22 },
23
24 #[error("Connection failed: {message}")]
26 Connection {
27 message: SmartString,
29 },
30
31 #[error("API error: {code} - {message}")]
33 Api {
34 code: SmartString,
36 message: SmartString,
38 },
39
40 #[error("Order operation failed: {operation} - {reason}")]
42 OrderOperation {
43 operation: SmartString,
45 reason: SmartString,
47 },
48
49 #[error("Invalid order status: {status}")]
51 InvalidOrderStatus {
52 status: SmartString,
54 },
55
56 #[error("Rate limit exceeded: {message}")]
58 RateLimit {
59 message: SmartString,
61 },
62
63 #[error("Insufficient balance: {currency} required: {required}, available: {available}")]
65 InsufficientBalance {
66 currency: SmartString,
68 required: SmartString,
70 available: SmartString,
72 },
73
74 #[error("Invalid market type: {market_type}")]
76 InvalidMarketType {
77 market_type: SmartString,
79 },
80
81 #[error("WebSocket connection error: {message}")]
83 WebSocketConnection {
84 message: SmartString,
86 },
87
88 #[error("JSON parsing error: {message}")]
90 JsonParsing {
91 message: SmartString,
93 },
94
95 #[error("Configuration error: {message}")]
97 Configuration {
98 message: SmartString,
100 },
101
102 #[error("Network timeout: operation took longer than {timeout_ms}ms")]
104 Timeout {
105 timeout_ms: u64,
107 },
108
109 #[error("Internal system error: {message}")]
111 Internal {
112 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
161pub type BithumbResult<T> = Result<T, BithumbError>;
163
164impl BithumbError {
166 pub fn auth(message: impl Into<SmartString>) -> Self {
168 Self::Authentication {
169 message: message.into(),
170 }
171 }
172
173 pub fn invalid_symbol(symbol: impl Into<SmartString>) -> Self {
175 Self::InvalidSymbol {
176 symbol: symbol.into(),
177 }
178 }
179
180 pub fn connection(message: impl Into<SmartString>) -> Self {
182 Self::Connection {
183 message: message.into(),
184 }
185 }
186
187 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 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 pub fn invalid_order_status(status: impl Into<SmartString>) -> Self {
208 Self::InvalidOrderStatus {
209 status: status.into(),
210 }
211 }
212
213 pub fn rate_limit(message: impl Into<SmartString>) -> Self {
215 Self::RateLimit {
216 message: message.into(),
217 }
218 }
219
220 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 pub fn websocket(message: impl Into<SmartString>) -> Self {
235 Self::WebSocketConnection {
236 message: message.into(),
237 }
238 }
239
240 pub fn json_parsing(message: impl Into<SmartString>) -> Self {
242 Self::JsonParsing {
243 message: message.into(),
244 }
245 }
246
247 pub fn configuration(message: impl Into<SmartString>) -> Self {
249 Self::Configuration {
250 message: message.into(),
251 }
252 }
253
254 #[must_use]
256 pub const fn timeout(timeout_ms: u64) -> Self {
257 Self::Timeout { timeout_ms }
258 }
259
260 pub fn internal(message: impl Into<SmartString>) -> Self {
262 Self::Internal {
263 message: message.into(),
264 }
265 }
266}
267
268#[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
286pub 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
299pub 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
311pub 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
324pub fn map_websocket_order_state(state: &str) -> BithumbResult<rusty_model::enums::OrderStatus> {
326 match state {
327 "wait" => Ok(rusty_model::enums::OrderStatus::Open), "trade" => Ok(rusty_model::enums::OrderStatus::PartiallyFilled), "done" => Ok(rusty_model::enums::OrderStatus::Filled), "cancel" => Ok(rusty_model::enums::OrderStatus::Cancelled), _ => Err(BithumbError::invalid_order_status(state)),
332 }
333}
334
335pub 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
346pub 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}