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: {0}")]
64    ConfigError(String),
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
101macro_rules! impl_kora_error_from {
102    ($source:ty => $variant:ident) => {
103        impl From<$source> for KoraError {
104            fn from(e: $source) -> Self {
105                #[cfg(feature = "unsafe-debug")]
106                {
107                    KoraError::$variant(e.to_string())
108                }
109                #[cfg(not(feature = "unsafe-debug"))]
110                {
111                    KoraError::$variant(sanitize_message(&e.to_string()))
112                }
113            }
114        }
115    };
116}
117
118impl_kora_error_from!(SignerError => SigningError);
119impl_kora_error_from!(bincode::Error => SerializationError);
120impl_kora_error_from!(bs58::decode::Error => SerializationError);
121impl_kora_error_from!(bs58::encode::Error => SerializationError);
122impl_kora_error_from!(std::io::Error => InternalServerError);
123impl_kora_error_from!(Box<dyn StdError> => InternalServerError);
124impl_kora_error_from!(Box<dyn StdError + Send + Sync> => InternalServerError);
125impl_kora_error_from!(ProgramError => InvalidTransaction);
126
127impl From<KoraError> for RpcError {
128    fn from(err: KoraError) -> Self {
129        match err {
130            KoraError::AccountNotFound(_)
131            | KoraError::InvalidTransaction(_)
132            | KoraError::ValidationError(_)
133            | KoraError::UnsupportedFeeToken(_)
134            | KoraError::InsufficientFunds(_) => invalid_request(err),
135
136            KoraError::InternalServerError(_) | KoraError::SerializationError(_) => {
137                internal_server_error(err)
138            }
139
140            _ => invalid_request(err),
141        }
142    }
143}
144
145pub fn invalid_request(e: KoraError) -> RpcError {
146    RpcError::Call(CallError::from_std_error(e))
147}
148
149pub fn internal_server_error(e: KoraError) -> RpcError {
150    RpcError::Call(CallError::from_std_error(e))
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct KoraResponse<T> {
155    pub data: Option<T>,
156    pub error: Option<KoraError>,
157}
158
159impl<T> KoraResponse<T> {
160    pub fn ok(data: T) -> Self {
161        Self { data: Some(data), error: None }
162    }
163
164    pub fn err(error: KoraError) -> Self {
165        Self { data: None, error: Some(error) }
166    }
167
168    pub fn from_result(result: Result<T, KoraError>) -> Self {
169        match result {
170            Ok(data) => Self::ok(data),
171            Err(error) => Self::err(error),
172        }
173    }
174}
175
176// Extension trait for Result<T, E> to convert to KoraResponse
177pub trait IntoKoraResponse<T> {
178    fn into_response(self) -> KoraResponse<T>;
179}
180
181impl<T, E: Into<KoraError>> IntoKoraResponse<T> for Result<T, E> {
182    fn into_response(self) -> KoraResponse<T> {
183        match self {
184            Ok(data) => KoraResponse::ok(data),
185            Err(e) => KoraResponse::err(e.into()),
186        }
187    }
188}
189
190impl_kora_error_from!(anyhow::Error => SigningError);
191impl_kora_error_from!(solana_keychain::SignerError => SigningError);
192
193impl From<BundleError> for KoraError {
194    fn from(err: BundleError) -> Self {
195        match err {
196            BundleError::Jito(_) => KoraError::JitoError(err.to_string()),
197            _ => KoraError::InvalidTransaction(err.to_string()),
198        }
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use solana_program::program_error::ProgramError;
206    use std::error::Error as StdError;
207
208    #[test]
209    fn test_kora_response_ok() {
210        let response = KoraResponse::ok(42);
211        assert_eq!(response.data, Some(42));
212        assert_eq!(response.error, None);
213    }
214
215    #[test]
216    fn test_kora_response_err() {
217        let error = KoraError::AccountNotFound("test_account".to_string());
218        let response: KoraResponse<()> = KoraResponse::err(error.clone());
219        assert_eq!(response.data, None);
220        assert_eq!(response.error, Some(error));
221    }
222
223    #[test]
224    fn test_kora_response_from_result() {
225        let ok_response = KoraResponse::from_result(Ok(42));
226        assert_eq!(ok_response.data, Some(42));
227        assert_eq!(ok_response.error, None);
228
229        let error = KoraError::ValidationError("test error".to_string());
230        let err_response: KoraResponse<i32> = KoraResponse::from_result(Err(error.clone()));
231        assert_eq!(err_response.data, None);
232        assert_eq!(err_response.error, Some(error));
233    }
234
235    #[test]
236    fn test_into_kora_response() {
237        let result: Result<i32, KoraError> = Ok(42);
238        let response = result.into_response();
239        assert_eq!(response.data, Some(42));
240        assert_eq!(response.error, None);
241
242        let error = KoraError::SwapError("swap failed".to_string());
243        let result: Result<i32, KoraError> = Err(error.clone());
244        let response = result.into_response();
245        assert_eq!(response.data, None);
246        assert_eq!(response.error, Some(error));
247    }
248
249    #[test]
250    fn test_client_error_conversion() {
251        let client_error = ClientError::from(std::io::Error::other("test"));
252        let kora_error: KoraError = client_error.into();
253        assert!(matches!(kora_error, KoraError::RpcError(_)));
254        // With sanitization, error message context is preserved unless it contains sensitive data
255        if let KoraError::RpcError(msg) = kora_error {
256            assert!(msg.contains("test"));
257        }
258    }
259
260    #[test]
261    fn test_signer_error_conversion() {
262        let signer_error = SignerError::Custom("signing failed".to_string());
263        let kora_error: KoraError = signer_error.into();
264        assert!(matches!(kora_error, KoraError::SigningError(_)));
265        // With sanitization, error message context is preserved unless it contains sensitive data
266        if let KoraError::SigningError(msg) = kora_error {
267            assert!(msg.contains("signing failed"));
268        }
269    }
270
271    #[test]
272    fn test_bincode_error_conversion() {
273        let bincode_error = bincode::Error::from(bincode::ErrorKind::SizeLimit);
274        let kora_error: KoraError = bincode_error.into();
275        assert!(matches!(kora_error, KoraError::SerializationError(_)));
276    }
277
278    #[test]
279    fn test_bs58_decode_error_conversion() {
280        let bs58_error = bs58::decode::Error::InvalidCharacter { character: 'x', index: 0 };
281        let kora_error: KoraError = bs58_error.into();
282        assert!(matches!(kora_error, KoraError::SerializationError(_)));
283    }
284
285    #[test]
286    fn test_bs58_encode_error_conversion() {
287        let buffer_too_small_error = bs58::encode::Error::BufferTooSmall;
288        let kora_error: KoraError = buffer_too_small_error.into();
289        assert!(matches!(kora_error, KoraError::SerializationError(_)));
290    }
291
292    #[test]
293    fn test_io_error_conversion() {
294        let io_error = std::io::Error::other("file not found");
295        let kora_error: KoraError = io_error.into();
296        assert!(matches!(kora_error, KoraError::InternalServerError(_)));
297        // With sanitization, error message context is preserved unless it contains sensitive data
298        if let KoraError::InternalServerError(msg) = kora_error {
299            assert!(msg.contains("file not found"));
300        }
301    }
302
303    #[test]
304    fn test_boxed_error_conversion() {
305        let error: Box<dyn StdError> = Box::new(std::io::Error::other("boxed error"));
306        let kora_error: KoraError = error.into();
307        assert!(matches!(kora_error, KoraError::InternalServerError(_)));
308    }
309
310    #[test]
311    fn test_boxed_error_send_sync_conversion() {
312        let error: Box<dyn StdError + Send + Sync> =
313            Box::new(std::io::Error::other("boxed send sync error"));
314        let kora_error: KoraError = error.into();
315        assert!(matches!(kora_error, KoraError::InternalServerError(_)));
316    }
317
318    #[test]
319    fn test_program_error_conversion() {
320        let program_error = ProgramError::InvalidAccountData;
321        let kora_error: KoraError = program_error.into();
322        assert!(matches!(kora_error, KoraError::InvalidTransaction(_)));
323        if let KoraError::InvalidTransaction(msg) = kora_error {
324            // Just check that the error is converted properly, don't rely on specific formatting
325            assert!(!msg.is_empty());
326        }
327    }
328
329    #[test]
330    fn test_anyhow_error_conversion() {
331        let anyhow_error = anyhow::anyhow!("something went wrong");
332        let kora_error: KoraError = anyhow_error.into();
333        assert!(matches!(kora_error, KoraError::SigningError(_)));
334        // With sanitization, error message context is preserved unless it contains sensitive data
335        if let KoraError::SigningError(msg) = kora_error {
336            assert!(msg.contains("something went wrong"));
337        }
338    }
339
340    #[test]
341    fn test_kora_error_to_rpc_error_invalid_request() {
342        let test_cases = vec![
343            KoraError::AccountNotFound("test".to_string()),
344            KoraError::InvalidTransaction("test".to_string()),
345            KoraError::ValidationError("test".to_string()),
346            KoraError::UnsupportedFeeToken("test".to_string()),
347            KoraError::InsufficientFunds("test".to_string()),
348        ];
349
350        for kora_error in test_cases {
351            let rpc_error: RpcError = kora_error.into();
352            assert!(matches!(rpc_error, RpcError::Call(_)));
353        }
354    }
355
356    #[test]
357    fn test_kora_error_to_rpc_error_internal_server() {
358        let test_cases = vec![
359            KoraError::InternalServerError("test".to_string()),
360            KoraError::SerializationError("test".to_string()),
361        ];
362
363        for kora_error in test_cases {
364            let rpc_error: RpcError = kora_error.into();
365            assert!(matches!(rpc_error, RpcError::Call(_)));
366        }
367    }
368
369    #[test]
370    fn test_kora_error_to_rpc_error_default_case() {
371        let other_errors = vec![
372            KoraError::RpcError("test".to_string()),
373            KoraError::SigningError("test".to_string()),
374            KoraError::TransactionExecutionFailed("test".to_string()),
375            KoraError::FeeEstimationFailed("test".to_string()),
376            KoraError::SwapError("test".to_string()),
377            KoraError::TokenOperationError("test".to_string()),
378            KoraError::InvalidRequest("test".to_string()),
379            KoraError::Unauthorized("test".to_string()),
380            KoraError::RateLimitExceeded,
381        ];
382
383        for kora_error in other_errors {
384            let rpc_error: RpcError = kora_error.into();
385            assert!(matches!(rpc_error, RpcError::Call(_)));
386        }
387    }
388
389    #[test]
390    fn test_invalid_request_function() {
391        let error = KoraError::ValidationError("invalid input".to_string());
392        let rpc_error = invalid_request(error);
393        assert!(matches!(rpc_error, RpcError::Call(_)));
394    }
395
396    #[test]
397    fn test_internal_server_error_function() {
398        let error = KoraError::InternalServerError("server panic".to_string());
399        let rpc_error = internal_server_error(error);
400        assert!(matches!(rpc_error, RpcError::Call(_)));
401    }
402
403    #[test]
404    fn test_into_kora_response_with_different_error_types() {
405        let io_result: Result<String, std::io::Error> = Err(std::io::Error::other("test"));
406        let response = io_result.into_response();
407        assert_eq!(response.data, None);
408        assert!(matches!(response.error, Some(KoraError::InternalServerError(_))));
409
410        let signer_result: Result<String, SignerError> =
411            Err(SignerError::Custom("test".to_string()));
412        let response = signer_result.into_response();
413        assert_eq!(response.data, None);
414        assert!(matches!(response.error, Some(KoraError::SigningError(_))));
415    }
416
417    #[test]
418    fn test_kora_error_display() {
419        let error = KoraError::AccountNotFound("test_account".to_string());
420        let display_string = format!("{error}");
421        assert_eq!(display_string, "Account test_account not found");
422
423        let error = KoraError::RateLimitExceeded;
424        let display_string = format!("{error}");
425        assert_eq!(display_string, "Rate limit exceeded");
426    }
427
428    #[test]
429    fn test_kora_error_debug() {
430        let error = KoraError::ValidationError("test".to_string());
431        let debug_string = format!("{error:?}");
432        assert!(debug_string.contains("ValidationError"));
433    }
434
435    #[test]
436    fn test_kora_error_clone() {
437        let error = KoraError::SwapError("original".to_string());
438        let cloned = error.clone();
439        assert_eq!(error, cloned);
440    }
441
442    #[test]
443    fn test_kora_response_serialization() {
444        let response = KoraResponse::ok("test_data".to_string());
445        let json = serde_json::to_string(&response).unwrap();
446        assert!(json.contains("test_data"));
447
448        let error_response: KoraResponse<String> =
449            KoraResponse::err(KoraError::ValidationError("test".to_string()));
450        let error_json = serde_json::to_string(&error_response).unwrap();
451        assert!(error_json.contains("ValidationError"));
452    }
453}