1use crate::{
2 domain::{
3 solana::SolanaTransactionValidationError, stellar::StellarTransactionValidationError,
4 },
5 jobs::JobProducerError,
6 models::{SignerError, SignerFactoryError},
7 services::provider::{ProviderError, SolanaProviderError},
8};
9
10use super::{ApiError, RepositoryError, StellarProviderError};
11use eyre::Report;
12use serde::Serialize;
13use soroban_rs::xdr;
14use thiserror::Error;
15
16#[derive(Error, Debug, Serialize)]
17pub enum TransactionError {
18 #[error("Transaction validation error: {0}")]
19 ValidationError(String),
20
21 #[error("Solana transaction validation error: {0}")]
22 SolanaValidation(#[from] SolanaTransactionValidationError),
23
24 #[error("Network configuration error: {0}")]
25 NetworkConfiguration(String),
26
27 #[error("Job producer error: {0}")]
28 JobProducerError(#[from] JobProducerError),
29
30 #[error("Invalid transaction type: {0}")]
31 InvalidType(String),
32
33 #[error("Underlying provider error: {0}")]
34 UnderlyingProvider(#[from] ProviderError),
35
36 #[error("Underlying Solana provider error: {0}")]
37 UnderlyingSolanaProvider(#[from] SolanaProviderError),
38
39 #[error("Stellar validation error: {0}")]
40 StellarTransactionValidationError(#[from] StellarTransactionValidationError),
41
42 #[error("Unexpected error: {0}")]
43 UnexpectedError(String),
44
45 #[error("Concurrent update conflict: {0}")]
46 ConcurrentUpdateConflict(String),
47
48 #[error("Not supported: {0}")]
49 NotSupported(String),
50
51 #[error("Signer error: {0}")]
52 SignerError(String),
53
54 #[error("Insufficient balance: {0}")]
55 InsufficientBalance(String),
56
57 #[error("Stellar transaction simulation failed: {0}")]
58 SimulationFailed(String),
59}
60
61impl TransactionError {
62 pub fn is_transient(&self) -> bool {
80 match self {
81 TransactionError::SolanaValidation(err) => err.is_transient(),
83 TransactionError::UnderlyingSolanaProvider(err) => err.is_transient(),
84 TransactionError::UnderlyingProvider(err) => err.is_transient(),
85
86 TransactionError::UnexpectedError(_) => true,
88 TransactionError::ConcurrentUpdateConflict(_) => true,
89 TransactionError::JobProducerError(_) => true,
90
91 TransactionError::ValidationError(_) => false,
93 TransactionError::InsufficientBalance(_) => false,
94 TransactionError::NetworkConfiguration(_) => false,
95 TransactionError::InvalidType(_) => false,
96 TransactionError::NotSupported(_) => false,
97 TransactionError::SignerError(_) => false,
98 TransactionError::SimulationFailed(_) => false,
99 TransactionError::StellarTransactionValidationError(_) => false,
100 }
101 }
102
103 pub fn is_concurrent_update_conflict(&self) -> bool {
105 matches!(self, TransactionError::ConcurrentUpdateConflict(_))
106 }
107}
108
109impl From<TransactionError> for ApiError {
110 fn from(error: TransactionError) -> Self {
111 match error {
112 TransactionError::ValidationError(msg) => ApiError::BadRequest(msg),
113 TransactionError::StellarTransactionValidationError(err) => {
114 ApiError::BadRequest(err.to_string())
115 }
116 TransactionError::SolanaValidation(err) => ApiError::BadRequest(err.to_string()),
117 TransactionError::NetworkConfiguration(msg) => ApiError::InternalError(msg),
118 TransactionError::JobProducerError(msg) => ApiError::InternalError(msg.to_string()),
119 TransactionError::InvalidType(msg) => ApiError::InternalError(msg),
120 TransactionError::UnderlyingProvider(err) => ApiError::InternalError(err.to_string()),
121 TransactionError::UnderlyingSolanaProvider(err) => {
122 ApiError::InternalError(err.to_string())
123 }
124 TransactionError::NotSupported(msg) => ApiError::BadRequest(msg),
125 TransactionError::UnexpectedError(msg) => ApiError::InternalError(msg),
126 TransactionError::ConcurrentUpdateConflict(msg) => ApiError::InternalError(msg),
127 TransactionError::SignerError(msg) => ApiError::InternalError(msg),
128 TransactionError::InsufficientBalance(msg) => ApiError::BadRequest(msg),
129 TransactionError::SimulationFailed(msg) => ApiError::BadRequest(msg),
130 }
131 }
132}
133
134impl From<RepositoryError> for TransactionError {
135 fn from(error: RepositoryError) -> Self {
136 match error {
137 RepositoryError::NotFound(msg)
138 | RepositoryError::InvalidData(msg)
139 | RepositoryError::ConstraintViolation(msg)
140 | RepositoryError::TransactionValidationFailed(msg) => {
141 TransactionError::ValidationError(msg)
142 }
143 RepositoryError::ConcurrentUpdateConflict(msg) => {
144 TransactionError::ConcurrentUpdateConflict(msg)
145 }
146 RepositoryError::TransactionFailure(msg)
147 | RepositoryError::LockError(msg)
148 | RepositoryError::ConnectionError(msg)
149 | RepositoryError::PermissionDenied(msg)
150 | RepositoryError::Unknown(msg)
151 | RepositoryError::UnexpectedError(msg)
152 | RepositoryError::Other(msg) => TransactionError::UnexpectedError(msg),
153 RepositoryError::NotSupported(msg) => TransactionError::NotSupported(msg),
154 }
155 }
156}
157
158impl From<Report> for TransactionError {
159 fn from(err: Report) -> Self {
160 TransactionError::UnexpectedError(err.to_string())
161 }
162}
163
164impl From<SignerFactoryError> for TransactionError {
165 fn from(error: SignerFactoryError) -> Self {
166 TransactionError::SignerError(error.to_string())
167 }
168}
169
170impl From<SignerError> for TransactionError {
171 fn from(error: SignerError) -> Self {
172 TransactionError::SignerError(error.to_string())
173 }
174}
175
176impl From<StellarProviderError> for TransactionError {
177 fn from(error: StellarProviderError) -> Self {
178 match error {
179 StellarProviderError::SimulationFailed(msg) => TransactionError::SimulationFailed(msg),
180 StellarProviderError::InsufficientBalance(msg) => {
181 TransactionError::InsufficientBalance(msg)
182 }
183 StellarProviderError::BadSeq(msg) => TransactionError::ValidationError(msg),
184 StellarProviderError::RpcError(msg) | StellarProviderError::Unknown(msg) => {
185 TransactionError::UnderlyingProvider(ProviderError::NetworkConfiguration(msg))
186 }
187 }
188 }
189}
190
191impl From<xdr::Error> for TransactionError {
192 fn from(error: xdr::Error) -> Self {
193 TransactionError::ValidationError(format!("XDR error: {error}"))
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_transaction_error_display() {
203 let test_cases = vec![
204 (
205 TransactionError::ValidationError("invalid input".to_string()),
206 "Transaction validation error: invalid input",
207 ),
208 (
209 TransactionError::NetworkConfiguration("wrong network".to_string()),
210 "Network configuration error: wrong network",
211 ),
212 (
213 TransactionError::InvalidType("unknown type".to_string()),
214 "Invalid transaction type: unknown type",
215 ),
216 (
217 TransactionError::UnexpectedError("something went wrong".to_string()),
218 "Unexpected error: something went wrong",
219 ),
220 (
221 TransactionError::NotSupported("feature unavailable".to_string()),
222 "Not supported: feature unavailable",
223 ),
224 (
225 TransactionError::SignerError("key error".to_string()),
226 "Signer error: key error",
227 ),
228 (
229 TransactionError::InsufficientBalance("not enough funds".to_string()),
230 "Insufficient balance: not enough funds",
231 ),
232 (
233 TransactionError::SimulationFailed("sim failed".to_string()),
234 "Stellar transaction simulation failed: sim failed",
235 ),
236 (
237 TransactionError::ConcurrentUpdateConflict("conflict on tx-1".to_string()),
238 "Concurrent update conflict: conflict on tx-1",
239 ),
240 ];
241
242 for (error, expected_message) in test_cases {
243 assert_eq!(error.to_string(), expected_message);
244 }
245 }
246
247 #[test]
248 fn test_transaction_error_to_api_error() {
249 let test_cases = vec![
250 (
251 TransactionError::ValidationError("invalid input".to_string()),
252 ApiError::BadRequest("invalid input".to_string()),
253 ),
254 (
255 TransactionError::NetworkConfiguration("wrong network".to_string()),
256 ApiError::InternalError("wrong network".to_string()),
257 ),
258 (
259 TransactionError::InvalidType("unknown type".to_string()),
260 ApiError::InternalError("unknown type".to_string()),
261 ),
262 (
263 TransactionError::UnexpectedError("something went wrong".to_string()),
264 ApiError::InternalError("something went wrong".to_string()),
265 ),
266 (
267 TransactionError::NotSupported("feature unavailable".to_string()),
268 ApiError::BadRequest("feature unavailable".to_string()),
269 ),
270 (
271 TransactionError::SignerError("key error".to_string()),
272 ApiError::InternalError("key error".to_string()),
273 ),
274 (
275 TransactionError::InsufficientBalance("not enough funds".to_string()),
276 ApiError::BadRequest("not enough funds".to_string()),
277 ),
278 (
279 TransactionError::SimulationFailed("boom".to_string()),
280 ApiError::BadRequest("boom".to_string()),
281 ),
282 (
283 TransactionError::ConcurrentUpdateConflict("conflict".to_string()),
284 ApiError::InternalError("conflict".to_string()),
285 ),
286 ];
287
288 for (tx_error, expected_api_error) in test_cases {
289 let api_error = ApiError::from(tx_error);
290
291 match (&api_error, &expected_api_error) {
292 (ApiError::BadRequest(actual), ApiError::BadRequest(expected)) => {
293 assert_eq!(actual, expected);
294 }
295 (ApiError::InternalError(actual), ApiError::InternalError(expected)) => {
296 assert_eq!(actual, expected);
297 }
298 _ => panic!("Error types don't match: {api_error:?} vs {expected_api_error:?}"),
299 }
300 }
301 }
302
303 #[test]
304 fn test_repository_error_to_transaction_error() {
305 let repo_error = RepositoryError::NotFound("record not found".to_string());
306 let tx_error = TransactionError::from(repo_error);
307
308 match tx_error {
309 TransactionError::ValidationError(msg) => {
310 assert_eq!(msg, "record not found");
311 }
312 _ => panic!("Expected TransactionError::ValidationError"),
313 }
314 }
315
316 #[test]
317 fn test_concurrent_update_conflict_variant() {
318 let error = TransactionError::ConcurrentUpdateConflict("conflict on tx-1".to_string());
319 assert!(error.is_transient());
320 assert!(error.is_concurrent_update_conflict());
321 assert_eq!(
322 error.to_string(),
323 "Concurrent update conflict: conflict on tx-1"
324 );
325
326 let api_error = ApiError::from(error);
327 assert!(matches!(api_error, ApiError::InternalError(_)));
328 }
329
330 #[test]
331 fn test_concurrent_update_conflict_repo_conversion() {
332 let repo_error = RepositoryError::ConcurrentUpdateConflict("conflict on tx-1".to_string());
333 let tx_error = TransactionError::from(repo_error);
334 assert!(matches!(
335 tx_error,
336 TransactionError::ConcurrentUpdateConflict(_)
337 ));
338 assert!(tx_error.is_concurrent_update_conflict());
339 }
340
341 #[test]
342 fn test_non_conflict_errors_are_not_concurrent_update_conflict() {
343 let errors = vec![
344 TransactionError::ValidationError("some error".to_string()),
345 TransactionError::UnexpectedError("some error".to_string()),
346 TransactionError::NetworkConfiguration("some error".to_string()),
347 ];
348 for error in errors {
349 assert!(
350 !error.is_concurrent_update_conflict(),
351 "Error {error:?} should not be a concurrent update conflict"
352 );
353 }
354 }
355
356 #[test]
357 fn test_report_to_transaction_error() {
358 let report = Report::msg("An unexpected error occurred");
359 let tx_error = TransactionError::from(report);
360
361 match tx_error {
362 TransactionError::UnexpectedError(msg) => {
363 assert!(msg.contains("An unexpected error occurred"));
364 }
365 _ => panic!("Expected TransactionError::UnexpectedError"),
366 }
367 }
368
369 #[test]
370 fn test_signer_factory_error_to_transaction_error() {
371 let factory_error = SignerFactoryError::InvalidConfig("missing key".to_string());
372 let tx_error = TransactionError::from(factory_error);
373
374 match tx_error {
375 TransactionError::SignerError(msg) => {
376 assert!(msg.contains("missing key"));
377 }
378 _ => panic!("Expected TransactionError::SignerError"),
379 }
380 }
381
382 #[test]
383 fn test_signer_error_to_transaction_error() {
384 let signer_error = SignerError::KeyError("invalid key format".to_string());
385 let tx_error = TransactionError::from(signer_error);
386
387 match tx_error {
388 TransactionError::SignerError(msg) => {
389 assert!(msg.contains("invalid key format"));
390 }
391 _ => panic!("Expected TransactionError::SignerError"),
392 }
393 }
394
395 #[test]
396 fn test_provider_error_conversion() {
397 let provider_error = ProviderError::NetworkConfiguration("timeout".to_string());
398 let tx_error = TransactionError::from(provider_error);
399
400 match tx_error {
401 TransactionError::UnderlyingProvider(err) => {
402 assert!(err.to_string().contains("timeout"));
403 }
404 _ => panic!("Expected TransactionError::UnderlyingProvider"),
405 }
406 }
407
408 #[test]
409 fn test_solana_provider_error_conversion() {
410 let solana_error = SolanaProviderError::RpcError("invalid response".to_string());
411 let tx_error = TransactionError::from(solana_error);
412
413 match tx_error {
414 TransactionError::UnderlyingSolanaProvider(err) => {
415 assert!(err.to_string().contains("invalid response"));
416 }
417 _ => panic!("Expected TransactionError::UnderlyingSolanaProvider"),
418 }
419 }
420
421 #[test]
422 fn test_job_producer_error_conversion() {
423 let job_error = JobProducerError::QueueError("queue full".to_string());
424 let tx_error = TransactionError::from(job_error);
425
426 match tx_error {
427 TransactionError::JobProducerError(err) => {
428 assert!(err.to_string().contains("queue full"));
429 }
430 _ => panic!("Expected TransactionError::JobProducerError"),
431 }
432 }
433
434 #[test]
435 fn test_xdr_error_conversion() {
436 use soroban_rs::xdr::{Limits, ReadXdr, TransactionEnvelope};
437
438 let xdr_error =
440 TransactionEnvelope::from_xdr_base64("invalid_base64", Limits::none()).unwrap_err();
441
442 let tx_error = TransactionError::from(xdr_error);
443
444 match tx_error {
445 TransactionError::ValidationError(msg) => {
446 assert!(msg.contains("XDR error:"));
447 }
448 _ => panic!("Expected TransactionError::ValidationError"),
449 }
450 }
451
452 #[test]
453 fn test_is_transient_permanent_errors() {
454 let permanent_errors = vec![
456 TransactionError::ValidationError("invalid input".to_string()),
457 TransactionError::InsufficientBalance("not enough funds".to_string()),
458 TransactionError::NetworkConfiguration("wrong network".to_string()),
459 TransactionError::InvalidType("unknown type".to_string()),
460 TransactionError::NotSupported("feature unavailable".to_string()),
461 TransactionError::SignerError("key error".to_string()),
462 TransactionError::SimulationFailed("sim failed".to_string()),
463 ];
464
465 for error in permanent_errors {
466 assert!(!error.is_transient(), "Error {error:?} should be permanent");
467 }
468 }
469
470 #[test]
471 fn test_is_transient_transient_errors() {
472 let transient_errors = vec![
474 TransactionError::UnexpectedError("something went wrong".to_string()),
475 TransactionError::JobProducerError(JobProducerError::QueueError(
476 "queue full".to_string(),
477 )),
478 ];
479
480 for error in transient_errors {
481 assert!(error.is_transient(), "Error {error:?} should be transient");
482 }
483 }
484
485 #[test]
486 fn test_stellar_provider_error_conversion() {
487 let sim_error = StellarProviderError::SimulationFailed("sim failed".to_string());
489 let tx_error = TransactionError::from(sim_error);
490 match tx_error {
491 TransactionError::SimulationFailed(msg) => {
492 assert_eq!(msg, "sim failed");
493 }
494 _ => panic!("Expected TransactionError::SimulationFailed"),
495 }
496
497 let balance_error =
499 StellarProviderError::InsufficientBalance("not enough funds".to_string());
500 let tx_error = TransactionError::from(balance_error);
501 match tx_error {
502 TransactionError::InsufficientBalance(msg) => {
503 assert_eq!(msg, "not enough funds");
504 }
505 _ => panic!("Expected TransactionError::InsufficientBalance"),
506 }
507
508 let seq_error = StellarProviderError::BadSeq("bad sequence".to_string());
510 let tx_error = TransactionError::from(seq_error);
511 match tx_error {
512 TransactionError::ValidationError(msg) => {
513 assert_eq!(msg, "bad sequence");
514 }
515 _ => panic!("Expected TransactionError::ValidationError"),
516 }
517
518 let rpc_error = StellarProviderError::RpcError("rpc failed".to_string());
520 let tx_error = TransactionError::from(rpc_error);
521 match tx_error {
522 TransactionError::UnderlyingProvider(ProviderError::NetworkConfiguration(msg)) => {
523 assert_eq!(msg, "rpc failed");
524 }
525 _ => panic!("Expected TransactionError::UnderlyingProvider"),
526 }
527
528 let unknown_error = StellarProviderError::Unknown("unknown error".to_string());
530 let tx_error = TransactionError::from(unknown_error);
531 match tx_error {
532 TransactionError::UnderlyingProvider(ProviderError::NetworkConfiguration(msg)) => {
533 assert_eq!(msg, "unknown error");
534 }
535 _ => panic!("Expected TransactionError::UnderlyingProvider"),
536 }
537 }
538
539 #[test]
540 fn test_is_transient_delegated_errors() {
541 use crate::domain::solana::SolanaTransactionValidationError;
546 let solana_validation_error =
547 SolanaTransactionValidationError::ValidationError("bad validation".to_string());
548 let tx_error = TransactionError::SolanaValidation(solana_validation_error);
549 let _ = tx_error.is_transient();
552
553 let solana_provider_error = SolanaProviderError::RpcError("rpc failed".to_string());
555 let tx_error = TransactionError::UnderlyingSolanaProvider(solana_provider_error);
556 let _ = tx_error.is_transient();
557
558 let provider_error = ProviderError::NetworkConfiguration("network issue".to_string());
560 let tx_error = TransactionError::UnderlyingProvider(provider_error);
561 let _ = tx_error.is_transient();
562 }
563}