openzeppelin_relayer/models/error/
transaction.rs

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("Not supported: {0}")]
46    NotSupported(String),
47
48    #[error("Signer error: {0}")]
49    SignerError(String),
50
51    #[error("Insufficient balance: {0}")]
52    InsufficientBalance(String),
53
54    #[error("Stellar transaction simulation failed: {0}")]
55    SimulationFailed(String),
56}
57
58impl TransactionError {
59    /// Determines if this error is transient (can retry) or permanent (should fail).
60    ///
61    /// **Transient (can retry):**
62    /// - `SolanaValidation`: Delegates to underlying error's is_transient()
63    /// - `UnderlyingSolanaProvider`: Delegates to underlying error's is_transient()
64    /// - `UnderlyingProvider`: Delegates to underlying error's is_transient()
65    /// - `UnexpectedError`: Unexpected errors may resolve on retry
66    /// - `JobProducerError`: Job queue issues are typically transient
67    ///
68    /// **Permanent (fail immediately):**
69    /// - `ValidationError`: Malformed data, missing fields, invalid state transitions
70    /// - `InsufficientBalance`: Balance issues won't resolve without funding
71    /// - `NetworkConfiguration`: Configuration errors are permanent
72    /// - `InvalidType`: Type mismatches are permanent
73    /// - `NotSupported`: Unsupported operations won't change
74    /// - `SignerError`: Signer issues are typically permanent
75    /// - `SimulationFailed`: Transaction simulation failures are permanent
76    pub fn is_transient(&self) -> bool {
77        match self {
78            // Delegate to underlying error's is_transient() method
79            TransactionError::SolanaValidation(err) => err.is_transient(),
80            TransactionError::UnderlyingSolanaProvider(err) => err.is_transient(),
81            TransactionError::UnderlyingProvider(err) => err.is_transient(),
82
83            // Transient errors - may resolve on retry
84            TransactionError::UnexpectedError(_) => true,
85            TransactionError::JobProducerError(_) => true,
86
87            // Permanent errors - fail immediately
88            TransactionError::ValidationError(_) => false,
89            TransactionError::InsufficientBalance(_) => false,
90            TransactionError::NetworkConfiguration(_) => false,
91            TransactionError::InvalidType(_) => false,
92            TransactionError::NotSupported(_) => false,
93            TransactionError::SignerError(_) => false,
94            TransactionError::SimulationFailed(_) => false,
95            TransactionError::StellarTransactionValidationError(_) => false,
96        }
97    }
98}
99
100impl From<TransactionError> for ApiError {
101    fn from(error: TransactionError) -> Self {
102        match error {
103            TransactionError::ValidationError(msg) => ApiError::BadRequest(msg),
104            TransactionError::StellarTransactionValidationError(err) => {
105                ApiError::BadRequest(err.to_string())
106            }
107            TransactionError::SolanaValidation(err) => ApiError::BadRequest(err.to_string()),
108            TransactionError::NetworkConfiguration(msg) => ApiError::InternalError(msg),
109            TransactionError::JobProducerError(msg) => ApiError::InternalError(msg.to_string()),
110            TransactionError::InvalidType(msg) => ApiError::InternalError(msg),
111            TransactionError::UnderlyingProvider(err) => ApiError::InternalError(err.to_string()),
112            TransactionError::UnderlyingSolanaProvider(err) => {
113                ApiError::InternalError(err.to_string())
114            }
115            TransactionError::NotSupported(msg) => ApiError::BadRequest(msg),
116            TransactionError::UnexpectedError(msg) => ApiError::InternalError(msg),
117            TransactionError::SignerError(msg) => ApiError::InternalError(msg),
118            TransactionError::InsufficientBalance(msg) => ApiError::BadRequest(msg),
119            TransactionError::SimulationFailed(msg) => ApiError::BadRequest(msg),
120        }
121    }
122}
123
124impl From<RepositoryError> for TransactionError {
125    fn from(error: RepositoryError) -> Self {
126        TransactionError::ValidationError(error.to_string())
127    }
128}
129
130impl From<Report> for TransactionError {
131    fn from(err: Report) -> Self {
132        TransactionError::UnexpectedError(err.to_string())
133    }
134}
135
136impl From<SignerFactoryError> for TransactionError {
137    fn from(error: SignerFactoryError) -> Self {
138        TransactionError::SignerError(error.to_string())
139    }
140}
141
142impl From<SignerError> for TransactionError {
143    fn from(error: SignerError) -> Self {
144        TransactionError::SignerError(error.to_string())
145    }
146}
147
148impl From<StellarProviderError> for TransactionError {
149    fn from(error: StellarProviderError) -> Self {
150        match error {
151            StellarProviderError::SimulationFailed(msg) => TransactionError::SimulationFailed(msg),
152            StellarProviderError::InsufficientBalance(msg) => {
153                TransactionError::InsufficientBalance(msg)
154            }
155            StellarProviderError::BadSeq(msg) => TransactionError::ValidationError(msg),
156            StellarProviderError::RpcError(msg) | StellarProviderError::Unknown(msg) => {
157                TransactionError::UnderlyingProvider(ProviderError::NetworkConfiguration(msg))
158            }
159        }
160    }
161}
162
163impl From<xdr::Error> for TransactionError {
164    fn from(error: xdr::Error) -> Self {
165        TransactionError::ValidationError(format!("XDR error: {error}"))
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_transaction_error_display() {
175        let test_cases = vec![
176            (
177                TransactionError::ValidationError("invalid input".to_string()),
178                "Transaction validation error: invalid input",
179            ),
180            (
181                TransactionError::NetworkConfiguration("wrong network".to_string()),
182                "Network configuration error: wrong network",
183            ),
184            (
185                TransactionError::InvalidType("unknown type".to_string()),
186                "Invalid transaction type: unknown type",
187            ),
188            (
189                TransactionError::UnexpectedError("something went wrong".to_string()),
190                "Unexpected error: something went wrong",
191            ),
192            (
193                TransactionError::NotSupported("feature unavailable".to_string()),
194                "Not supported: feature unavailable",
195            ),
196            (
197                TransactionError::SignerError("key error".to_string()),
198                "Signer error: key error",
199            ),
200            (
201                TransactionError::InsufficientBalance("not enough funds".to_string()),
202                "Insufficient balance: not enough funds",
203            ),
204            (
205                TransactionError::SimulationFailed("sim failed".to_string()),
206                "Stellar transaction simulation failed: sim failed",
207            ),
208        ];
209
210        for (error, expected_message) in test_cases {
211            assert_eq!(error.to_string(), expected_message);
212        }
213    }
214
215    #[test]
216    fn test_transaction_error_to_api_error() {
217        let test_cases = vec![
218            (
219                TransactionError::ValidationError("invalid input".to_string()),
220                ApiError::BadRequest("invalid input".to_string()),
221            ),
222            (
223                TransactionError::NetworkConfiguration("wrong network".to_string()),
224                ApiError::InternalError("wrong network".to_string()),
225            ),
226            (
227                TransactionError::InvalidType("unknown type".to_string()),
228                ApiError::InternalError("unknown type".to_string()),
229            ),
230            (
231                TransactionError::UnexpectedError("something went wrong".to_string()),
232                ApiError::InternalError("something went wrong".to_string()),
233            ),
234            (
235                TransactionError::NotSupported("feature unavailable".to_string()),
236                ApiError::BadRequest("feature unavailable".to_string()),
237            ),
238            (
239                TransactionError::SignerError("key error".to_string()),
240                ApiError::InternalError("key error".to_string()),
241            ),
242            (
243                TransactionError::InsufficientBalance("not enough funds".to_string()),
244                ApiError::BadRequest("not enough funds".to_string()),
245            ),
246            (
247                TransactionError::SimulationFailed("boom".to_string()),
248                ApiError::BadRequest("boom".to_string()),
249            ),
250        ];
251
252        for (tx_error, expected_api_error) in test_cases {
253            let api_error = ApiError::from(tx_error);
254
255            match (&api_error, &expected_api_error) {
256                (ApiError::BadRequest(actual), ApiError::BadRequest(expected)) => {
257                    assert_eq!(actual, expected);
258                }
259                (ApiError::InternalError(actual), ApiError::InternalError(expected)) => {
260                    assert_eq!(actual, expected);
261                }
262                _ => panic!(
263                    "Error types don't match: {:?} vs {:?}",
264                    api_error, expected_api_error
265                ),
266            }
267        }
268    }
269
270    #[test]
271    fn test_repository_error_to_transaction_error() {
272        let repo_error = RepositoryError::NotFound("record not found".to_string());
273        let tx_error = TransactionError::from(repo_error);
274
275        match tx_error {
276            TransactionError::ValidationError(msg) => {
277                assert_eq!(msg, "Entity not found: record not found");
278            }
279            _ => panic!("Expected TransactionError::ValidationError"),
280        }
281    }
282
283    #[test]
284    fn test_report_to_transaction_error() {
285        let report = Report::msg("An unexpected error occurred");
286        let tx_error = TransactionError::from(report);
287
288        match tx_error {
289            TransactionError::UnexpectedError(msg) => {
290                assert!(msg.contains("An unexpected error occurred"));
291            }
292            _ => panic!("Expected TransactionError::UnexpectedError"),
293        }
294    }
295
296    #[test]
297    fn test_signer_factory_error_to_transaction_error() {
298        let factory_error = SignerFactoryError::InvalidConfig("missing key".to_string());
299        let tx_error = TransactionError::from(factory_error);
300
301        match tx_error {
302            TransactionError::SignerError(msg) => {
303                assert!(msg.contains("missing key"));
304            }
305            _ => panic!("Expected TransactionError::SignerError"),
306        }
307    }
308
309    #[test]
310    fn test_signer_error_to_transaction_error() {
311        let signer_error = SignerError::KeyError("invalid key format".to_string());
312        let tx_error = TransactionError::from(signer_error);
313
314        match tx_error {
315            TransactionError::SignerError(msg) => {
316                assert!(msg.contains("invalid key format"));
317            }
318            _ => panic!("Expected TransactionError::SignerError"),
319        }
320    }
321
322    #[test]
323    fn test_provider_error_conversion() {
324        let provider_error = ProviderError::NetworkConfiguration("timeout".to_string());
325        let tx_error = TransactionError::from(provider_error);
326
327        match tx_error {
328            TransactionError::UnderlyingProvider(err) => {
329                assert!(err.to_string().contains("timeout"));
330            }
331            _ => panic!("Expected TransactionError::UnderlyingProvider"),
332        }
333    }
334
335    #[test]
336    fn test_solana_provider_error_conversion() {
337        let solana_error = SolanaProviderError::RpcError("invalid response".to_string());
338        let tx_error = TransactionError::from(solana_error);
339
340        match tx_error {
341            TransactionError::UnderlyingSolanaProvider(err) => {
342                assert!(err.to_string().contains("invalid response"));
343            }
344            _ => panic!("Expected TransactionError::UnderlyingSolanaProvider"),
345        }
346    }
347
348    #[test]
349    fn test_job_producer_error_conversion() {
350        let job_error = JobProducerError::QueueError("queue full".to_string());
351        let tx_error = TransactionError::from(job_error);
352
353        match tx_error {
354            TransactionError::JobProducerError(err) => {
355                assert!(err.to_string().contains("queue full"));
356            }
357            _ => panic!("Expected TransactionError::JobProducerError"),
358        }
359    }
360
361    #[test]
362    fn test_xdr_error_conversion() {
363        use soroban_rs::xdr::{Limits, ReadXdr, TransactionEnvelope};
364
365        // Create an XDR error by trying to parse invalid base64
366        let xdr_error =
367            TransactionEnvelope::from_xdr_base64("invalid_base64", Limits::none()).unwrap_err();
368
369        let tx_error = TransactionError::from(xdr_error);
370
371        match tx_error {
372            TransactionError::ValidationError(msg) => {
373                assert!(msg.contains("XDR error:"));
374            }
375            _ => panic!("Expected TransactionError::ValidationError"),
376        }
377    }
378
379    #[test]
380    fn test_is_transient_permanent_errors() {
381        // Test permanent errors that should return false
382        let permanent_errors = vec![
383            TransactionError::ValidationError("invalid input".to_string()),
384            TransactionError::InsufficientBalance("not enough funds".to_string()),
385            TransactionError::NetworkConfiguration("wrong network".to_string()),
386            TransactionError::InvalidType("unknown type".to_string()),
387            TransactionError::NotSupported("feature unavailable".to_string()),
388            TransactionError::SignerError("key error".to_string()),
389            TransactionError::SimulationFailed("sim failed".to_string()),
390        ];
391
392        for error in permanent_errors {
393            assert!(
394                !error.is_transient(),
395                "Error {:?} should be permanent",
396                error
397            );
398        }
399    }
400
401    #[test]
402    fn test_is_transient_transient_errors() {
403        // Test transient errors that should return true
404        let transient_errors = vec![
405            TransactionError::UnexpectedError("something went wrong".to_string()),
406            TransactionError::JobProducerError(JobProducerError::QueueError(
407                "queue full".to_string(),
408            )),
409        ];
410
411        for error in transient_errors {
412            assert!(
413                error.is_transient(),
414                "Error {:?} should be transient",
415                error
416            );
417        }
418    }
419
420    #[test]
421    fn test_stellar_provider_error_conversion() {
422        // Test SimulationFailed
423        let sim_error = StellarProviderError::SimulationFailed("sim failed".to_string());
424        let tx_error = TransactionError::from(sim_error);
425        match tx_error {
426            TransactionError::SimulationFailed(msg) => {
427                assert_eq!(msg, "sim failed");
428            }
429            _ => panic!("Expected TransactionError::SimulationFailed"),
430        }
431
432        // Test InsufficientBalance
433        let balance_error =
434            StellarProviderError::InsufficientBalance("not enough funds".to_string());
435        let tx_error = TransactionError::from(balance_error);
436        match tx_error {
437            TransactionError::InsufficientBalance(msg) => {
438                assert_eq!(msg, "not enough funds");
439            }
440            _ => panic!("Expected TransactionError::InsufficientBalance"),
441        }
442
443        // Test BadSeq
444        let seq_error = StellarProviderError::BadSeq("bad sequence".to_string());
445        let tx_error = TransactionError::from(seq_error);
446        match tx_error {
447            TransactionError::ValidationError(msg) => {
448                assert_eq!(msg, "bad sequence");
449            }
450            _ => panic!("Expected TransactionError::ValidationError"),
451        }
452
453        // Test RpcError
454        let rpc_error = StellarProviderError::RpcError("rpc failed".to_string());
455        let tx_error = TransactionError::from(rpc_error);
456        match tx_error {
457            TransactionError::UnderlyingProvider(ProviderError::NetworkConfiguration(msg)) => {
458                assert_eq!(msg, "rpc failed");
459            }
460            _ => panic!("Expected TransactionError::UnderlyingProvider"),
461        }
462
463        // Test Unknown
464        let unknown_error = StellarProviderError::Unknown("unknown error".to_string());
465        let tx_error = TransactionError::from(unknown_error);
466        match tx_error {
467            TransactionError::UnderlyingProvider(ProviderError::NetworkConfiguration(msg)) => {
468                assert_eq!(msg, "unknown error");
469            }
470            _ => panic!("Expected TransactionError::UnderlyingProvider"),
471        }
472    }
473
474    #[test]
475    fn test_is_transient_delegated_errors() {
476        // Test errors that delegate to underlying error's is_transient() method
477        // We need to create mock errors that have is_transient() methods
478
479        // For SolanaValidation - create a mock error
480        use crate::domain::solana::SolanaTransactionValidationError;
481        let solana_validation_error =
482            SolanaTransactionValidationError::ValidationError("bad validation".to_string());
483        let tx_error = TransactionError::SolanaValidation(solana_validation_error);
484        // This will delegate to the underlying error's is_transient method
485        // We can't easily test the delegation without mocking, so we'll just ensure it doesn't panic
486        let _ = tx_error.is_transient();
487
488        // For UnderlyingSolanaProvider
489        let solana_provider_error = SolanaProviderError::RpcError("rpc failed".to_string());
490        let tx_error = TransactionError::UnderlyingSolanaProvider(solana_provider_error);
491        let _ = tx_error.is_transient();
492
493        // For UnderlyingProvider
494        let provider_error = ProviderError::NetworkConfiguration("network issue".to_string());
495        let tx_error = TransactionError::UnderlyingProvider(provider_error);
496        let _ = tx_error.is_transient();
497    }
498}