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!("Error types don't match: {api_error:?} vs {expected_api_error:?}"),
263            }
264        }
265    }
266
267    #[test]
268    fn test_repository_error_to_transaction_error() {
269        let repo_error = RepositoryError::NotFound("record not found".to_string());
270        let tx_error = TransactionError::from(repo_error);
271
272        match tx_error {
273            TransactionError::ValidationError(msg) => {
274                assert_eq!(msg, "Entity not found: record not found");
275            }
276            _ => panic!("Expected TransactionError::ValidationError"),
277        }
278    }
279
280    #[test]
281    fn test_report_to_transaction_error() {
282        let report = Report::msg("An unexpected error occurred");
283        let tx_error = TransactionError::from(report);
284
285        match tx_error {
286            TransactionError::UnexpectedError(msg) => {
287                assert!(msg.contains("An unexpected error occurred"));
288            }
289            _ => panic!("Expected TransactionError::UnexpectedError"),
290        }
291    }
292
293    #[test]
294    fn test_signer_factory_error_to_transaction_error() {
295        let factory_error = SignerFactoryError::InvalidConfig("missing key".to_string());
296        let tx_error = TransactionError::from(factory_error);
297
298        match tx_error {
299            TransactionError::SignerError(msg) => {
300                assert!(msg.contains("missing key"));
301            }
302            _ => panic!("Expected TransactionError::SignerError"),
303        }
304    }
305
306    #[test]
307    fn test_signer_error_to_transaction_error() {
308        let signer_error = SignerError::KeyError("invalid key format".to_string());
309        let tx_error = TransactionError::from(signer_error);
310
311        match tx_error {
312            TransactionError::SignerError(msg) => {
313                assert!(msg.contains("invalid key format"));
314            }
315            _ => panic!("Expected TransactionError::SignerError"),
316        }
317    }
318
319    #[test]
320    fn test_provider_error_conversion() {
321        let provider_error = ProviderError::NetworkConfiguration("timeout".to_string());
322        let tx_error = TransactionError::from(provider_error);
323
324        match tx_error {
325            TransactionError::UnderlyingProvider(err) => {
326                assert!(err.to_string().contains("timeout"));
327            }
328            _ => panic!("Expected TransactionError::UnderlyingProvider"),
329        }
330    }
331
332    #[test]
333    fn test_solana_provider_error_conversion() {
334        let solana_error = SolanaProviderError::RpcError("invalid response".to_string());
335        let tx_error = TransactionError::from(solana_error);
336
337        match tx_error {
338            TransactionError::UnderlyingSolanaProvider(err) => {
339                assert!(err.to_string().contains("invalid response"));
340            }
341            _ => panic!("Expected TransactionError::UnderlyingSolanaProvider"),
342        }
343    }
344
345    #[test]
346    fn test_job_producer_error_conversion() {
347        let job_error = JobProducerError::QueueError("queue full".to_string());
348        let tx_error = TransactionError::from(job_error);
349
350        match tx_error {
351            TransactionError::JobProducerError(err) => {
352                assert!(err.to_string().contains("queue full"));
353            }
354            _ => panic!("Expected TransactionError::JobProducerError"),
355        }
356    }
357
358    #[test]
359    fn test_xdr_error_conversion() {
360        use soroban_rs::xdr::{Limits, ReadXdr, TransactionEnvelope};
361
362        // Create an XDR error by trying to parse invalid base64
363        let xdr_error =
364            TransactionEnvelope::from_xdr_base64("invalid_base64", Limits::none()).unwrap_err();
365
366        let tx_error = TransactionError::from(xdr_error);
367
368        match tx_error {
369            TransactionError::ValidationError(msg) => {
370                assert!(msg.contains("XDR error:"));
371            }
372            _ => panic!("Expected TransactionError::ValidationError"),
373        }
374    }
375
376    #[test]
377    fn test_is_transient_permanent_errors() {
378        // Test permanent errors that should return false
379        let permanent_errors = vec![
380            TransactionError::ValidationError("invalid input".to_string()),
381            TransactionError::InsufficientBalance("not enough funds".to_string()),
382            TransactionError::NetworkConfiguration("wrong network".to_string()),
383            TransactionError::InvalidType("unknown type".to_string()),
384            TransactionError::NotSupported("feature unavailable".to_string()),
385            TransactionError::SignerError("key error".to_string()),
386            TransactionError::SimulationFailed("sim failed".to_string()),
387        ];
388
389        for error in permanent_errors {
390            assert!(!error.is_transient(), "Error {error:?} should be permanent");
391        }
392    }
393
394    #[test]
395    fn test_is_transient_transient_errors() {
396        // Test transient errors that should return true
397        let transient_errors = vec![
398            TransactionError::UnexpectedError("something went wrong".to_string()),
399            TransactionError::JobProducerError(JobProducerError::QueueError(
400                "queue full".to_string(),
401            )),
402        ];
403
404        for error in transient_errors {
405            assert!(error.is_transient(), "Error {error:?} should be transient");
406        }
407    }
408
409    #[test]
410    fn test_stellar_provider_error_conversion() {
411        // Test SimulationFailed
412        let sim_error = StellarProviderError::SimulationFailed("sim failed".to_string());
413        let tx_error = TransactionError::from(sim_error);
414        match tx_error {
415            TransactionError::SimulationFailed(msg) => {
416                assert_eq!(msg, "sim failed");
417            }
418            _ => panic!("Expected TransactionError::SimulationFailed"),
419        }
420
421        // Test InsufficientBalance
422        let balance_error =
423            StellarProviderError::InsufficientBalance("not enough funds".to_string());
424        let tx_error = TransactionError::from(balance_error);
425        match tx_error {
426            TransactionError::InsufficientBalance(msg) => {
427                assert_eq!(msg, "not enough funds");
428            }
429            _ => panic!("Expected TransactionError::InsufficientBalance"),
430        }
431
432        // Test BadSeq
433        let seq_error = StellarProviderError::BadSeq("bad sequence".to_string());
434        let tx_error = TransactionError::from(seq_error);
435        match tx_error {
436            TransactionError::ValidationError(msg) => {
437                assert_eq!(msg, "bad sequence");
438            }
439            _ => panic!("Expected TransactionError::ValidationError"),
440        }
441
442        // Test RpcError
443        let rpc_error = StellarProviderError::RpcError("rpc failed".to_string());
444        let tx_error = TransactionError::from(rpc_error);
445        match tx_error {
446            TransactionError::UnderlyingProvider(ProviderError::NetworkConfiguration(msg)) => {
447                assert_eq!(msg, "rpc failed");
448            }
449            _ => panic!("Expected TransactionError::UnderlyingProvider"),
450        }
451
452        // Test Unknown
453        let unknown_error = StellarProviderError::Unknown("unknown error".to_string());
454        let tx_error = TransactionError::from(unknown_error);
455        match tx_error {
456            TransactionError::UnderlyingProvider(ProviderError::NetworkConfiguration(msg)) => {
457                assert_eq!(msg, "unknown error");
458            }
459            _ => panic!("Expected TransactionError::UnderlyingProvider"),
460        }
461    }
462
463    #[test]
464    fn test_is_transient_delegated_errors() {
465        // Test errors that delegate to underlying error's is_transient() method
466        // We need to create mock errors that have is_transient() methods
467
468        // For SolanaValidation - create a mock error
469        use crate::domain::solana::SolanaTransactionValidationError;
470        let solana_validation_error =
471            SolanaTransactionValidationError::ValidationError("bad validation".to_string());
472        let tx_error = TransactionError::SolanaValidation(solana_validation_error);
473        // This will delegate to the underlying error's is_transient method
474        // We can't easily test the delegation without mocking, so we'll just ensure it doesn't panic
475        let _ = tx_error.is_transient();
476
477        // For UnderlyingSolanaProvider
478        let solana_provider_error = SolanaProviderError::RpcError("rpc failed".to_string());
479        let tx_error = TransactionError::UnderlyingSolanaProvider(solana_provider_error);
480        let _ = tx_error.is_transient();
481
482        // For UnderlyingProvider
483        let provider_error = ProviderError::NetworkConfiguration("network issue".to_string());
484        let tx_error = TransactionError::UnderlyingProvider(provider_error);
485        let _ = tx_error.is_transient();
486    }
487}