Skip to main content

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