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