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
248pub 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 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 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 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 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 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}