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