openzeppelin_relayer/models/error/
transaction.rs

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