Skip to main content

kora_lib/
error.rs

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