kaccy_core/
error.rs

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