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