kora_lib/
error.rs

1use crate::{bundle::BundleError, sanitize::sanitize_message};
2use jsonrpsee::{core::Error as RpcError, types::error::CallError};
3use serde::{Deserialize, Serialize};
4use solana_client::client_error::ClientError;
5use solana_program::program_error::ProgramError;
6use solana_sdk::signature::SignerError;
7use std::error::Error as StdError;
8use thiserror::Error;
9
10#[derive(Error, Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
11pub enum KoraError {
12    #[error("Account {0} not found")]
13    AccountNotFound(String),
14
15    #[error("RPC error: {0}")]
16    RpcError(String),
17
18    #[error("Signing error: {0}")]
19    SigningError(String),
20
21    #[error("Invalid transaction: {0}")]
22    InvalidTransaction(String),
23
24    #[error("Transaction execution failed: {0}")]
25    TransactionExecutionFailed(String),
26
27    #[error("Fee estimation failed: {0}")]
28    FeeEstimationFailed(String),
29
30    #[error("Token {0} is not supported for fee payment")]
31    UnsupportedFeeToken(String),
32
33    #[error("Insufficient funds: {0}")]
34    InsufficientFunds(String),
35
36    #[error("Internal error: {0}")]
37    InternalServerError(String),
38
39    #[error("Validation error: {0}")]
40    ValidationError(String),
41
42    #[error("Serialization error: {0}")]
43    SerializationError(String),
44
45    #[error("Swap error: {0}")]
46    SwapError(String),
47
48    #[error("Token operation failed: {0}")]
49    TokenOperationError(String),
50
51    #[error("Invalid request: {0}")]
52    InvalidRequest(String),
53
54    #[error("Unauthorized: {0}")]
55    Unauthorized(String),
56
57    #[error("Rate limit exceeded")]
58    RateLimitExceeded,
59
60    #[error("Usage limit exceeded: {0}")]
61    UsageLimitExceeded(String),
62
63    #[error("Invalid configuration for Kora")]
64    ConfigError,
65
66    #[error("Jito error: {0}")]
67    JitoError(String),
68}
69
70impl From<ClientError> for KoraError {
71    fn from(e: ClientError) -> Self {
72        let error_string = e.to_string();
73        let sanitized_error_string = sanitize_message(&error_string);
74        if error_string.contains("AccountNotFound")
75            || error_string.contains("could not find account")
76        {
77            #[cfg(feature = "unsafe-debug")]
78            {
79                KoraError::AccountNotFound(error_string)
80            }
81            #[cfg(not(feature = "unsafe-debug"))]
82            {
83                KoraError::AccountNotFound(sanitized_error_string)
84            }
85        } else {
86            #[cfg(feature = "unsafe-debug")]
87            {
88                KoraError::RpcError(error_string)
89            }
90            #[cfg(not(feature = "unsafe-debug"))]
91            {
92                KoraError::RpcError(sanitized_error_string)
93            }
94        }
95    }
96}
97
98impl From<SignerError> for KoraError {
99    fn from(_e: SignerError) -> Self {
100        #[cfg(feature = "unsafe-debug")]
101        {
102            KoraError::SigningError(_e.to_string())
103        }
104        #[cfg(not(feature = "unsafe-debug"))]
105        {
106            KoraError::SigningError(sanitize_message(&_e.to_string()))
107        }
108    }
109}
110
111impl From<bincode::Error> for KoraError {
112    fn from(_e: bincode::Error) -> Self {
113        #[cfg(feature = "unsafe-debug")]
114        {
115            KoraError::SerializationError(_e.to_string())
116        }
117        #[cfg(not(feature = "unsafe-debug"))]
118        {
119            KoraError::SerializationError(sanitize_message(&_e.to_string()))
120        }
121    }
122}
123
124impl From<bs58::decode::Error> for KoraError {
125    fn from(_e: bs58::decode::Error) -> Self {
126        #[cfg(feature = "unsafe-debug")]
127        {
128            KoraError::SerializationError(_e.to_string())
129        }
130        #[cfg(not(feature = "unsafe-debug"))]
131        {
132            KoraError::SerializationError(sanitize_message(&_e.to_string()))
133        }
134    }
135}
136
137impl From<bs58::encode::Error> for KoraError {
138    fn from(_e: bs58::encode::Error) -> Self {
139        #[cfg(feature = "unsafe-debug")]
140        {
141            KoraError::SerializationError(_e.to_string())
142        }
143        #[cfg(not(feature = "unsafe-debug"))]
144        {
145            KoraError::SerializationError(sanitize_message(&_e.to_string()))
146        }
147    }
148}
149
150impl From<std::io::Error> for KoraError {
151    fn from(_e: std::io::Error) -> Self {
152        #[cfg(feature = "unsafe-debug")]
153        {
154            KoraError::InternalServerError(_e.to_string())
155        }
156        #[cfg(not(feature = "unsafe-debug"))]
157        {
158            KoraError::InternalServerError(sanitize_message(&_e.to_string()))
159        }
160    }
161}
162
163impl From<Box<dyn StdError>> for KoraError {
164    fn from(_e: Box<dyn StdError>) -> Self {
165        #[cfg(feature = "unsafe-debug")]
166        {
167            KoraError::InternalServerError(_e.to_string())
168        }
169        #[cfg(not(feature = "unsafe-debug"))]
170        {
171            KoraError::InternalServerError(sanitize_message(&_e.to_string()))
172        }
173    }
174}
175
176impl From<Box<dyn StdError + Send + Sync>> for KoraError {
177    fn from(_e: Box<dyn StdError + Send + Sync>) -> Self {
178        #[cfg(feature = "unsafe-debug")]
179        {
180            KoraError::InternalServerError(_e.to_string())
181        }
182        #[cfg(not(feature = "unsafe-debug"))]
183        {
184            KoraError::InternalServerError(sanitize_message(&_e.to_string()))
185        }
186    }
187}
188
189impl From<ProgramError> for KoraError {
190    fn from(_err: ProgramError) -> Self {
191        #[cfg(feature = "unsafe-debug")]
192        {
193            KoraError::InvalidTransaction(_err.to_string())
194        }
195        #[cfg(not(feature = "unsafe-debug"))]
196        {
197            KoraError::InvalidTransaction(sanitize_message(&_err.to_string()))
198        }
199    }
200}
201
202impl From<KoraError> for RpcError {
203    fn from(err: KoraError) -> Self {
204        match err {
205            KoraError::AccountNotFound(_)
206            | KoraError::InvalidTransaction(_)
207            | KoraError::ValidationError(_)
208            | KoraError::UnsupportedFeeToken(_)
209            | KoraError::InsufficientFunds(_) => invalid_request(err),
210
211            KoraError::InternalServerError(_) | KoraError::SerializationError(_) => {
212                internal_server_error(err)
213            }
214
215            _ => invalid_request(err),
216        }
217    }
218}
219
220pub fn invalid_request(e: KoraError) -> RpcError {
221    RpcError::Call(CallError::from_std_error(e))
222}
223
224pub fn internal_server_error(e: KoraError) -> RpcError {
225    RpcError::Call(CallError::from_std_error(e))
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct KoraResponse<T> {
230    pub data: Option<T>,
231    pub error: Option<KoraError>,
232}
233
234impl<T> KoraResponse<T> {
235    pub fn ok(data: T) -> Self {
236        Self { data: Some(data), error: None }
237    }
238
239    pub fn err(error: KoraError) -> Self {
240        Self { data: None, error: Some(error) }
241    }
242
243    pub fn from_result(result: Result<T, KoraError>) -> Self {
244        match result {
245            Ok(data) => Self::ok(data),
246            Err(error) => Self::err(error),
247        }
248    }
249}
250
251// Extension trait for Result<T, E> to convert to KoraResponse
252pub trait IntoKoraResponse<T> {
253    fn into_response(self) -> KoraResponse<T>;
254}
255
256impl<T, E: Into<KoraError>> IntoKoraResponse<T> for Result<T, E> {
257    fn into_response(self) -> KoraResponse<T> {
258        match self {
259            Ok(data) => KoraResponse::ok(data),
260            Err(e) => KoraResponse::err(e.into()),
261        }
262    }
263}
264
265impl From<anyhow::Error> for KoraError {
266    fn from(_err: anyhow::Error) -> Self {
267        #[cfg(feature = "unsafe-debug")]
268        {
269            KoraError::SigningError(_err.to_string())
270        }
271        #[cfg(not(feature = "unsafe-debug"))]
272        {
273            KoraError::SigningError(sanitize_message(&_err.to_string()))
274        }
275    }
276}
277
278impl From<solana_keychain::SignerError> for KoraError {
279    fn from(_err: solana_keychain::SignerError) -> Self {
280        #[cfg(feature = "unsafe-debug")]
281        {
282            KoraError::SigningError(_err.to_string())
283        }
284        #[cfg(not(feature = "unsafe-debug"))]
285        {
286            KoraError::SigningError(sanitize_message(&_err.to_string()))
287        }
288    }
289}
290
291impl From<BundleError> for KoraError {
292    fn from(err: BundleError) -> Self {
293        match err {
294            BundleError::Jito(_) => KoraError::JitoError(err.to_string()),
295            _ => KoraError::InvalidTransaction(err.to_string()),
296        }
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303    use solana_program::program_error::ProgramError;
304    use std::error::Error as StdError;
305
306    #[test]
307    fn test_kora_response_ok() {
308        let response = KoraResponse::ok(42);
309        assert_eq!(response.data, Some(42));
310        assert_eq!(response.error, None);
311    }
312
313    #[test]
314    fn test_kora_response_err() {
315        let error = KoraError::AccountNotFound("test_account".to_string());
316        let response: KoraResponse<()> = KoraResponse::err(error.clone());
317        assert_eq!(response.data, None);
318        assert_eq!(response.error, Some(error));
319    }
320
321    #[test]
322    fn test_kora_response_from_result() {
323        let ok_response = KoraResponse::from_result(Ok(42));
324        assert_eq!(ok_response.data, Some(42));
325        assert_eq!(ok_response.error, None);
326
327        let error = KoraError::ValidationError("test error".to_string());
328        let err_response: KoraResponse<i32> = KoraResponse::from_result(Err(error.clone()));
329        assert_eq!(err_response.data, None);
330        assert_eq!(err_response.error, Some(error));
331    }
332
333    #[test]
334    fn test_into_kora_response() {
335        let result: Result<i32, KoraError> = Ok(42);
336        let response = result.into_response();
337        assert_eq!(response.data, Some(42));
338        assert_eq!(response.error, None);
339
340        let error = KoraError::SwapError("swap failed".to_string());
341        let result: Result<i32, KoraError> = Err(error.clone());
342        let response = result.into_response();
343        assert_eq!(response.data, None);
344        assert_eq!(response.error, Some(error));
345    }
346
347    #[test]
348    fn test_client_error_conversion() {
349        let client_error = ClientError::from(std::io::Error::other("test"));
350        let kora_error: KoraError = client_error.into();
351        assert!(matches!(kora_error, KoraError::RpcError(_)));
352        // With sanitization, error message context is preserved unless it contains sensitive data
353        if let KoraError::RpcError(msg) = kora_error {
354            assert!(msg.contains("test"));
355        }
356    }
357
358    #[test]
359    fn test_signer_error_conversion() {
360        let signer_error = SignerError::Custom("signing failed".to_string());
361        let kora_error: KoraError = signer_error.into();
362        assert!(matches!(kora_error, KoraError::SigningError(_)));
363        // With sanitization, error message context is preserved unless it contains sensitive data
364        if let KoraError::SigningError(msg) = kora_error {
365            assert!(msg.contains("signing failed"));
366        }
367    }
368
369    #[test]
370    fn test_bincode_error_conversion() {
371        let bincode_error = bincode::Error::from(bincode::ErrorKind::SizeLimit);
372        let kora_error: KoraError = bincode_error.into();
373        assert!(matches!(kora_error, KoraError::SerializationError(_)));
374    }
375
376    #[test]
377    fn test_bs58_decode_error_conversion() {
378        let bs58_error = bs58::decode::Error::InvalidCharacter { character: 'x', index: 0 };
379        let kora_error: KoraError = bs58_error.into();
380        assert!(matches!(kora_error, KoraError::SerializationError(_)));
381    }
382
383    #[test]
384    fn test_bs58_encode_error_conversion() {
385        let buffer_too_small_error = bs58::encode::Error::BufferTooSmall;
386        let kora_error: KoraError = buffer_too_small_error.into();
387        assert!(matches!(kora_error, KoraError::SerializationError(_)));
388    }
389
390    #[test]
391    fn test_io_error_conversion() {
392        let io_error = std::io::Error::other("file not found");
393        let kora_error: KoraError = io_error.into();
394        assert!(matches!(kora_error, KoraError::InternalServerError(_)));
395        // With sanitization, error message context is preserved unless it contains sensitive data
396        if let KoraError::InternalServerError(msg) = kora_error {
397            assert!(msg.contains("file not found"));
398        }
399    }
400
401    #[test]
402    fn test_boxed_error_conversion() {
403        let error: Box<dyn StdError> = Box::new(std::io::Error::other("boxed error"));
404        let kora_error: KoraError = error.into();
405        assert!(matches!(kora_error, KoraError::InternalServerError(_)));
406    }
407
408    #[test]
409    fn test_boxed_error_send_sync_conversion() {
410        let error: Box<dyn StdError + Send + Sync> =
411            Box::new(std::io::Error::other("boxed send sync error"));
412        let kora_error: KoraError = error.into();
413        assert!(matches!(kora_error, KoraError::InternalServerError(_)));
414    }
415
416    #[test]
417    fn test_program_error_conversion() {
418        let program_error = ProgramError::InvalidAccountData;
419        let kora_error: KoraError = program_error.into();
420        assert!(matches!(kora_error, KoraError::InvalidTransaction(_)));
421        if let KoraError::InvalidTransaction(msg) = kora_error {
422            // Just check that the error is converted properly, don't rely on specific formatting
423            assert!(!msg.is_empty());
424        }
425    }
426
427    #[test]
428    fn test_anyhow_error_conversion() {
429        let anyhow_error = anyhow::anyhow!("something went wrong");
430        let kora_error: KoraError = anyhow_error.into();
431        assert!(matches!(kora_error, KoraError::SigningError(_)));
432        // With sanitization, error message context is preserved unless it contains sensitive data
433        if let KoraError::SigningError(msg) = kora_error {
434            assert!(msg.contains("something went wrong"));
435        }
436    }
437
438    #[test]
439    fn test_kora_error_to_rpc_error_invalid_request() {
440        let test_cases = vec![
441            KoraError::AccountNotFound("test".to_string()),
442            KoraError::InvalidTransaction("test".to_string()),
443            KoraError::ValidationError("test".to_string()),
444            KoraError::UnsupportedFeeToken("test".to_string()),
445            KoraError::InsufficientFunds("test".to_string()),
446        ];
447
448        for kora_error in test_cases {
449            let rpc_error: RpcError = kora_error.into();
450            assert!(matches!(rpc_error, RpcError::Call(_)));
451        }
452    }
453
454    #[test]
455    fn test_kora_error_to_rpc_error_internal_server() {
456        let test_cases = vec![
457            KoraError::InternalServerError("test".to_string()),
458            KoraError::SerializationError("test".to_string()),
459        ];
460
461        for kora_error in test_cases {
462            let rpc_error: RpcError = kora_error.into();
463            assert!(matches!(rpc_error, RpcError::Call(_)));
464        }
465    }
466
467    #[test]
468    fn test_kora_error_to_rpc_error_default_case() {
469        let other_errors = vec![
470            KoraError::RpcError("test".to_string()),
471            KoraError::SigningError("test".to_string()),
472            KoraError::TransactionExecutionFailed("test".to_string()),
473            KoraError::FeeEstimationFailed("test".to_string()),
474            KoraError::SwapError("test".to_string()),
475            KoraError::TokenOperationError("test".to_string()),
476            KoraError::InvalidRequest("test".to_string()),
477            KoraError::Unauthorized("test".to_string()),
478            KoraError::RateLimitExceeded,
479        ];
480
481        for kora_error in other_errors {
482            let rpc_error: RpcError = kora_error.into();
483            assert!(matches!(rpc_error, RpcError::Call(_)));
484        }
485    }
486
487    #[test]
488    fn test_invalid_request_function() {
489        let error = KoraError::ValidationError("invalid input".to_string());
490        let rpc_error = invalid_request(error);
491        assert!(matches!(rpc_error, RpcError::Call(_)));
492    }
493
494    #[test]
495    fn test_internal_server_error_function() {
496        let error = KoraError::InternalServerError("server panic".to_string());
497        let rpc_error = internal_server_error(error);
498        assert!(matches!(rpc_error, RpcError::Call(_)));
499    }
500
501    #[test]
502    fn test_into_kora_response_with_different_error_types() {
503        let io_result: Result<String, std::io::Error> = Err(std::io::Error::other("test"));
504        let response = io_result.into_response();
505        assert_eq!(response.data, None);
506        assert!(matches!(response.error, Some(KoraError::InternalServerError(_))));
507
508        let signer_result: Result<String, SignerError> =
509            Err(SignerError::Custom("test".to_string()));
510        let response = signer_result.into_response();
511        assert_eq!(response.data, None);
512        assert!(matches!(response.error, Some(KoraError::SigningError(_))));
513    }
514
515    #[test]
516    fn test_kora_error_display() {
517        let error = KoraError::AccountNotFound("test_account".to_string());
518        let display_string = format!("{error}");
519        assert_eq!(display_string, "Account test_account not found");
520
521        let error = KoraError::RateLimitExceeded;
522        let display_string = format!("{error}");
523        assert_eq!(display_string, "Rate limit exceeded");
524    }
525
526    #[test]
527    fn test_kora_error_debug() {
528        let error = KoraError::ValidationError("test".to_string());
529        let debug_string = format!("{error:?}");
530        assert!(debug_string.contains("ValidationError"));
531    }
532
533    #[test]
534    fn test_kora_error_clone() {
535        let error = KoraError::SwapError("original".to_string());
536        let cloned = error.clone();
537        assert_eq!(error, cloned);
538    }
539
540    #[test]
541    fn test_kora_response_serialization() {
542        let response = KoraResponse::ok("test_data".to_string());
543        let json = serde_json::to_string(&response).unwrap();
544        assert!(json.contains("test_data"));
545
546        let error_response: KoraResponse<String> =
547            KoraResponse::err(KoraError::ValidationError("test".to_string()));
548        let error_json = serde_json::to_string(&error_response).unwrap();
549        assert!(error_json.contains("ValidationError"));
550    }
551}