Skip to main content

kaccy_core/
error.rs

1//! Core error types
2
3use rust_decimal::Decimal;
4use thiserror::Error;
5use uuid::Uuid;
6
7/// Core error type for all kaccy-core operations
8#[derive(Error, Debug)]
9pub enum CoreError {
10    /// A database operation failed
11    #[error("Database error: {0}")]
12    Database(String),
13
14    /// Input validation failed
15    #[error("Validation error: {0}")]
16    Validation(String),
17
18    /// Requested resource was not found
19    #[error("Not found: {0}")]
20    NotFound(String),
21
22    /// Account balance is insufficient for the operation
23    #[error("Insufficient balance: required {required}, available {available}")]
24    InsufficientBalance {
25        /// Required amount
26        required: Decimal,
27        /// Currently available amount
28        available: Decimal,
29    },
30
31    /// A token with this identifier already exists
32    #[error("Token already exists: {0}")]
33    TokenExists(String),
34
35    /// Bonding curve parameters are invalid
36    #[error("Invalid bonding curve parameters")]
37    InvalidCurveParams,
38
39    /// A circuit breaker has been triggered for the token
40    #[error("Circuit breaker triggered")]
41    CircuitBreakerTriggered,
42
43    /// Trade slippage exceeded the configured tolerance
44    #[error("Slippage exceeded: expected {expected}, actual {actual}")]
45    SlippageExceeded {
46        /// Expected slippage
47        expected: Decimal,
48        /// Actual slippage
49        actual: Decimal,
50    },
51
52    /// Order has expired and can no longer be filled
53    #[error("Order expired")]
54    OrderExpired,
55
56    /// Token trading is currently paused
57    #[error("Token paused")]
58    TokenPaused,
59
60    /// User reputation is below the required threshold
61    #[error("Reputation too low: required {required}, current {current}")]
62    ReputationTooLow {
63        /// Required reputation score
64        required: Decimal,
65        /// Current reputation score
66        current: Decimal,
67    },
68
69    /// Token maximum supply has been reached
70    #[error("Max supply reached")]
71    MaxSupplyReached,
72
73    /// Caller is not authorised to perform this action
74    #[error("Unauthorized")]
75    Unauthorized,
76
77    /// Serialization or deserialization failed
78    #[error("Serialization error: {0}")]
79    Serialization(String),
80
81    /// A numeric calculation failed
82    #[error("Calculation error: {0}")]
83    Calculation(String),
84
85    // Trading-specific errors
86    /// The specified order was not found
87    #[error("Order not found: {0}")]
88    OrderNotFound(Uuid),
89
90    /// Order quantity is invalid
91    #[error("Invalid order quantity: {0}")]
92    InvalidOrderQuantity(String),
93
94    /// Order price is invalid
95    #[error("Invalid price: {0}")]
96    InvalidPrice(String),
97
98    /// Order has already been filled
99    #[error("Order already filled")]
100    OrderAlreadyFilled,
101
102    /// Order has already been cancelled
103    #[error("Order already cancelled")]
104    OrderAlreadyCancelled,
105
106    /// Insufficient market liquidity for the trade
107    #[error("Insufficient liquidity: {0}")]
108    InsufficientLiquidity(String),
109
110    /// Trade price impact exceeds the configured maximum
111    #[error("Price impact too high: {impact}% exceeds maximum {max_impact}%")]
112    PriceImpactTooHigh {
113        /// Actual price impact
114        impact: Decimal,
115        /// Configured maximum allowed impact
116        max_impact: Decimal,
117    },
118
119    /// Market is outside of its trading hours
120    #[error("Market is closed")]
121    MarketClosed,
122
123    /// Trading for this token has been halted
124    #[error("Trading halted for token {0}")]
125    TradingHalted(Uuid),
126
127    // Risk management errors
128    /// Position size exceeds the configured limit
129    #[error("Position limit exceeded: current {current}, maximum {maximum}")]
130    PositionLimitExceeded {
131        /// Current position size
132        current: Decimal,
133        /// Configured maximum
134        maximum: Decimal,
135    },
136
137    /// Daily trading volume limit exceeded
138    #[error("Daily trading limit exceeded: traded {traded}, limit {limit}")]
139    DailyTradingLimitExceeded {
140        /// Volume traded today
141        traded: Decimal,
142        /// Configured daily limit
143        limit: Decimal,
144    },
145
146    /// Leverage ratio is too high
147    #[error("Leverage ratio too high: {ratio} exceeds maximum {max_ratio}")]
148    ExcessiveLeverage {
149        /// Current leverage ratio
150        ratio: Decimal,
151        /// Configured maximum ratio
152        max_ratio: Decimal,
153    },
154
155    /// Margin call: equity has fallen below maintenance margin
156    #[error("Margin call: equity {equity} below maintenance margin {maintenance}")]
157    MarginCall {
158        /// Current equity
159        equity: Decimal,
160        /// Required maintenance margin
161        maintenance: Decimal,
162    },
163
164    // Payment-specific errors
165    /// The specified payment was not found
166    #[error("Payment not found: {0}")]
167    PaymentNotFound(Uuid),
168
169    /// Payment has already been confirmed and cannot be reprocessed
170    #[error("Payment already confirmed")]
171    PaymentAlreadyConfirmed,
172
173    /// Payment has expired
174    #[error("Payment expired")]
175    PaymentExpired,
176
177    /// Not enough blockchain confirmations yet
178    #[error("Insufficient confirmations: required {required}, current {current}")]
179    InsufficientConfirmations {
180        /// Required confirmation count
181        required: u32,
182        /// Current confirmation count
183        current: u32,
184    },
185
186    // Reputation/KYC errors
187    /// KYC verification is required before this action
188    #[error("KYC verification required")]
189    KycRequired,
190
191    /// KYC verification is in progress
192    #[error("KYC verification pending")]
193    KycPending,
194
195    /// KYC verification was rejected
196    #[error("KYC verification rejected: {0}")]
197    KycRejected(String),
198
199    /// The specified commitment was not found
200    #[error("Commitment not found: {0}")]
201    CommitmentNotFound(Uuid),
202
203    /// Commitment has already been verified
204    #[error("Commitment already verified")]
205    CommitmentAlreadyVerified,
206
207    /// Commitment deadline has passed
208    #[error("Commitment deadline passed")]
209    CommitmentDeadlinePassed,
210
211    // Rate limiting errors
212    /// Rate limit exceeded; retry after the given number of seconds
213    #[error("Rate limit exceeded: retry after {retry_after} seconds")]
214    RateLimitExceeded {
215        /// Seconds to wait before retrying
216        retry_after: u64,
217    },
218
219    /// Too many requests from this user
220    #[error("Too many requests from user {0}")]
221    TooManyRequests(Uuid),
222
223    // Concurrency errors
224    /// A resource is currently locked by another operation
225    #[error("Resource locked: {0}")]
226    ResourceLocked(String),
227
228    /// Optimistic locking failed because the resource was concurrently modified
229    #[error("Optimistic lock failed: resource was modified")]
230    OptimisticLockFailed,
231
232    /// A database deadlock was detected
233    #[error("Deadlock detected")]
234    DeadlockDetected,
235
236    // Configuration errors
237    /// System configuration is invalid
238    #[error("Configuration error: {0}")]
239    Configuration(String),
240
241    /// A feature flag is not enabled
242    #[error("Feature not enabled: {0}")]
243    FeatureNotEnabled(String),
244
245    // State errors
246    /// Entity is in an invalid state for this operation
247    #[error("Invalid state: {0}")]
248    InvalidState(String),
249
250    /// Entity already exists
251    #[error("Already exists: {0}")]
252    AlreadyExists(String),
253
254    // Bridge-specific errors
255    /// Provided amount is invalid
256    #[error("Invalid amount")]
257    InvalidAmount,
258
259    /// Specified bridge route is invalid
260    #[error("Invalid bridge route")]
261    InvalidBridgeRoute,
262
263    /// Bridge protocol is not supported
264    #[error("Bridge not supported")]
265    BridgeNotSupported,
266}
267
268impl From<sqlx::Error> for CoreError {
269    fn from(err: sqlx::Error) -> Self {
270        CoreError::Database(err.to_string())
271    }
272}
273
274impl From<serde_json::Error> for CoreError {
275    fn from(err: serde_json::Error) -> Self {
276        CoreError::Serialization(err.to_string())
277    }
278}
279
280impl CoreError {
281    /// Check if error is retryable (e.g., for retry logic)
282    pub fn is_retryable(&self) -> bool {
283        matches!(
284            self,
285            CoreError::Database(_)
286                | CoreError::DeadlockDetected
287                | CoreError::ResourceLocked(_)
288                | CoreError::OptimisticLockFailed
289        )
290    }
291
292    /// Check if error is a client error (4xx)
293    pub fn is_client_error(&self) -> bool {
294        matches!(
295            self,
296            CoreError::Validation(_)
297                | CoreError::NotFound(_)
298                | CoreError::Unauthorized
299                | CoreError::OrderNotFound(_)
300                | CoreError::InvalidOrderQuantity(_)
301                | CoreError::InvalidPrice(_)
302                | CoreError::OrderExpired
303                | CoreError::OrderAlreadyFilled
304                | CoreError::OrderAlreadyCancelled
305                | CoreError::InsufficientBalance { .. }
306                | CoreError::TokenExists(_)
307                | CoreError::TokenPaused
308                | CoreError::ReputationTooLow { .. }
309                | CoreError::MaxSupplyReached
310                | CoreError::SlippageExceeded { .. }
311                | CoreError::InsufficientLiquidity { .. }
312                | CoreError::PriceImpactTooHigh { .. }
313                | CoreError::PositionLimitExceeded { .. }
314                | CoreError::DailyTradingLimitExceeded { .. }
315                | CoreError::ExcessiveLeverage { .. }
316                | CoreError::PaymentNotFound(_)
317                | CoreError::PaymentExpired
318                | CoreError::InsufficientConfirmations { .. }
319                | CoreError::KycRequired
320                | CoreError::KycPending
321                | CoreError::KycRejected(_)
322                | CoreError::CommitmentNotFound(_)
323                | CoreError::CommitmentDeadlinePassed
324                | CoreError::RateLimitExceeded { .. }
325                | CoreError::TooManyRequests(_)
326                | CoreError::FeatureNotEnabled(_)
327                | CoreError::InvalidState(_)
328                | CoreError::InvalidAmount
329                | CoreError::InvalidBridgeRoute
330                | CoreError::BridgeNotSupported
331        )
332    }
333
334    /// Check if error is a server error (5xx)
335    pub fn is_server_error(&self) -> bool {
336        matches!(
337            self,
338            CoreError::Database(_)
339                | CoreError::Serialization(_)
340                | CoreError::Calculation(_)
341                | CoreError::DeadlockDetected
342                | CoreError::ResourceLocked(_)
343                | CoreError::OptimisticLockFailed
344                | CoreError::Configuration(_)
345        )
346    }
347
348    /// Get HTTP status code for this error
349    pub fn status_code(&self) -> u16 {
350        match self {
351            CoreError::NotFound(_)
352            | CoreError::OrderNotFound(_)
353            | CoreError::PaymentNotFound(_)
354            | CoreError::CommitmentNotFound(_) => 404,
355
356            CoreError::Unauthorized => 401,
357
358            CoreError::Validation(_)
359            | CoreError::InvalidOrderQuantity(_)
360            | CoreError::InvalidPrice(_)
361            | CoreError::OrderExpired
362            | CoreError::OrderAlreadyFilled
363            | CoreError::OrderAlreadyCancelled
364            | CoreError::InsufficientBalance { .. }
365            | CoreError::TokenExists(_)
366            | CoreError::InvalidCurveParams
367            | CoreError::TokenPaused
368            | CoreError::ReputationTooLow { .. }
369            | CoreError::MaxSupplyReached
370            | CoreError::SlippageExceeded { .. }
371            | CoreError::InsufficientLiquidity(_)
372            | CoreError::PriceImpactTooHigh { .. }
373            | CoreError::MarketClosed
374            | CoreError::TradingHalted(_)
375            | CoreError::PositionLimitExceeded { .. }
376            | CoreError::DailyTradingLimitExceeded { .. }
377            | CoreError::ExcessiveLeverage { .. }
378            | CoreError::MarginCall { .. }
379            | CoreError::PaymentExpired
380            | CoreError::InsufficientConfirmations { .. }
381            | CoreError::KycRequired
382            | CoreError::KycRejected(_)
383            | CoreError::CommitmentDeadlinePassed
384            | CoreError::FeatureNotEnabled(_)
385            | CoreError::InvalidState(_)
386            | CoreError::InvalidAmount
387            | CoreError::InvalidBridgeRoute
388            | CoreError::BridgeNotSupported => 400,
389
390            CoreError::PaymentAlreadyConfirmed
391            | CoreError::CommitmentAlreadyVerified
392            | CoreError::KycPending
393            | CoreError::AlreadyExists(_) => 409,
394
395            CoreError::RateLimitExceeded { .. } | CoreError::TooManyRequests(_) => 429,
396
397            CoreError::CircuitBreakerTriggered => 503,
398
399            CoreError::Database(_)
400            | CoreError::Serialization(_)
401            | CoreError::Calculation(_)
402            | CoreError::ResourceLocked(_)
403            | CoreError::OptimisticLockFailed
404            | CoreError::DeadlockDetected
405            | CoreError::Configuration(_) => 500,
406        }
407    }
408
409    /// Get error category for logging and metrics
410    pub fn category(&self) -> &'static str {
411        match self {
412            CoreError::Database(_) => "database",
413            CoreError::Validation(_)
414            | CoreError::InvalidOrderQuantity(_)
415            | CoreError::InvalidPrice(_) => "validation",
416            CoreError::NotFound(_)
417            | CoreError::OrderNotFound(_)
418            | CoreError::PaymentNotFound(_)
419            | CoreError::CommitmentNotFound(_) => "not_found",
420            CoreError::Unauthorized => "authorization",
421            CoreError::InsufficientBalance { .. } | CoreError::InsufficientLiquidity(_) => {
422                "insufficient_funds"
423            }
424            CoreError::OrderExpired
425            | CoreError::OrderAlreadyFilled
426            | CoreError::OrderAlreadyCancelled => "order_state",
427            CoreError::TokenExists(_) | CoreError::TokenPaused | CoreError::TradingHalted(_) => {
428                "token_state"
429            }
430            CoreError::InvalidCurveParams => "bonding_curve",
431            CoreError::CircuitBreakerTriggered => "circuit_breaker",
432            CoreError::SlippageExceeded { .. } | CoreError::PriceImpactTooHigh { .. } => {
433                "price_protection"
434            }
435            CoreError::ReputationTooLow { .. } => "reputation",
436            CoreError::MaxSupplyReached => "supply_limit",
437            CoreError::MarketClosed => "market_hours",
438            CoreError::PositionLimitExceeded { .. }
439            | CoreError::DailyTradingLimitExceeded { .. }
440            | CoreError::ExcessiveLeverage { .. }
441            | CoreError::MarginCall { .. } => "risk_management",
442            CoreError::PaymentAlreadyConfirmed
443            | CoreError::PaymentExpired
444            | CoreError::InsufficientConfirmations { .. } => "payment",
445            CoreError::KycRequired | CoreError::KycPending | CoreError::KycRejected(_) => "kyc",
446            CoreError::CommitmentAlreadyVerified | CoreError::CommitmentDeadlinePassed => {
447                "commitment"
448            }
449            CoreError::RateLimitExceeded { .. } | CoreError::TooManyRequests(_) => "rate_limiting",
450            CoreError::ResourceLocked(_)
451            | CoreError::OptimisticLockFailed
452            | CoreError::DeadlockDetected => "concurrency",
453            CoreError::Configuration(_) | CoreError::FeatureNotEnabled(_) => "configuration",
454            CoreError::Serialization(_) => "serialization",
455            CoreError::Calculation(_) => "calculation",
456            CoreError::InvalidState(_) => "state",
457            CoreError::AlreadyExists(_) => "duplicate",
458            CoreError::InvalidAmount
459            | CoreError::InvalidBridgeRoute
460            | CoreError::BridgeNotSupported => "bridge",
461        }
462    }
463}
464
465/// Convenience type alias for results that may produce a [`CoreError`].
466pub type Result<T> = std::result::Result<T, CoreError>;
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471    use rust_decimal_macros::dec;
472
473    #[test]
474    fn test_error_retryable() {
475        let retryable_error = CoreError::Database("Connection failed".to_string());
476        assert!(retryable_error.is_retryable());
477
478        let non_retryable_error = CoreError::Validation("Invalid input".to_string());
479        assert!(!non_retryable_error.is_retryable());
480    }
481
482    #[test]
483    fn test_error_categorization() {
484        let client_error = CoreError::Validation("Invalid input".to_string());
485        assert!(client_error.is_client_error());
486        assert!(!client_error.is_server_error());
487
488        let server_error = CoreError::Database("Connection failed".to_string());
489        assert!(!server_error.is_client_error());
490        assert!(server_error.is_server_error());
491    }
492
493    #[test]
494    fn test_error_status_codes() {
495        assert_eq!(
496            CoreError::NotFound("Resource".to_string()).status_code(),
497            404
498        );
499        assert_eq!(CoreError::Unauthorized.status_code(), 401);
500        assert_eq!(
501            CoreError::Validation("Invalid".to_string()).status_code(),
502            400
503        );
504        assert_eq!(CoreError::Database("Error".to_string()).status_code(), 500);
505        assert_eq!(
506            CoreError::RateLimitExceeded { retry_after: 60 }.status_code(),
507            429
508        );
509    }
510
511    #[test]
512    fn test_error_categories() {
513        assert_eq!(
514            CoreError::Database("Error".to_string()).category(),
515            "database"
516        );
517        assert_eq!(
518            CoreError::Validation("Error".to_string()).category(),
519            "validation"
520        );
521        assert_eq!(
522            CoreError::InsufficientBalance {
523                required: dec!(100),
524                available: dec!(50)
525            }
526            .category(),
527            "insufficient_funds"
528        );
529        assert_eq!(
530            CoreError::CircuitBreakerTriggered.category(),
531            "circuit_breaker"
532        );
533    }
534
535    #[test]
536    fn test_serde_json_error_conversion() {
537        let json_err = serde_json::from_str::<serde_json::Value>("invalid json");
538        assert!(json_err.is_err());
539
540        let core_err: CoreError = json_err.unwrap_err().into();
541        matches!(core_err, CoreError::Serialization(_));
542    }
543}