openzeppelin_relayer/models/error/
transaction.rs

1use crate::{
2    jobs::JobProducerError,
3    models::{SignerError, SignerFactoryError},
4    services::{ProviderError, SolanaProviderError},
5};
6
7use super::{ApiError, RepositoryError, StellarProviderError};
8use eyre::Report;
9use serde::Serialize;
10use soroban_rs::xdr;
11use thiserror::Error;
12
13#[derive(Error, Debug, Serialize)]
14pub enum TransactionError {
15    #[error("Transaction validation error: {0}")]
16    ValidationError(String),
17
18    #[error("Network configuration error: {0}")]
19    NetworkConfiguration(String),
20
21    #[error("Job producer error: {0}")]
22    JobProducerError(#[from] JobProducerError),
23
24    #[error("Invalid transaction type: {0}")]
25    InvalidType(String),
26
27    #[error("Underlying provider error: {0}")]
28    UnderlyingProvider(#[from] ProviderError),
29
30    #[error("Underlying Solana provider error: {0}")]
31    UnderlyingSolanaProvider(#[from] SolanaProviderError),
32
33    #[error("Unexpected error: {0}")]
34    UnexpectedError(String),
35
36    #[error("Not supported: {0}")]
37    NotSupported(String),
38
39    #[error("Signer error: {0}")]
40    SignerError(String),
41
42    #[error("Insufficient balance: {0}")]
43    InsufficientBalance(String),
44
45    #[error("Stellar transaction simulation failed: {0}")]
46    SimulationFailed(String),
47}
48
49impl From<TransactionError> for ApiError {
50    fn from(error: TransactionError) -> Self {
51        match error {
52            TransactionError::ValidationError(msg) => ApiError::BadRequest(msg),
53            TransactionError::NetworkConfiguration(msg) => ApiError::InternalError(msg),
54            TransactionError::JobProducerError(msg) => ApiError::InternalError(msg.to_string()),
55            TransactionError::InvalidType(msg) => ApiError::InternalError(msg),
56            TransactionError::UnderlyingProvider(err) => ApiError::InternalError(err.to_string()),
57            TransactionError::UnderlyingSolanaProvider(err) => {
58                ApiError::InternalError(err.to_string())
59            }
60            TransactionError::NotSupported(msg) => ApiError::BadRequest(msg),
61            TransactionError::UnexpectedError(msg) => ApiError::InternalError(msg),
62            TransactionError::SignerError(msg) => ApiError::InternalError(msg),
63            TransactionError::InsufficientBalance(msg) => ApiError::BadRequest(msg),
64            TransactionError::SimulationFailed(msg) => ApiError::BadRequest(msg),
65        }
66    }
67}
68
69impl From<RepositoryError> for TransactionError {
70    fn from(error: RepositoryError) -> Self {
71        TransactionError::ValidationError(error.to_string())
72    }
73}
74
75impl From<Report> for TransactionError {
76    fn from(err: Report) -> Self {
77        TransactionError::UnexpectedError(err.to_string())
78    }
79}
80
81impl From<SignerFactoryError> for TransactionError {
82    fn from(error: SignerFactoryError) -> Self {
83        TransactionError::SignerError(error.to_string())
84    }
85}
86
87impl From<SignerError> for TransactionError {
88    fn from(error: SignerError) -> Self {
89        TransactionError::SignerError(error.to_string())
90    }
91}
92
93impl From<StellarProviderError> for TransactionError {
94    fn from(error: StellarProviderError) -> Self {
95        match error {
96            StellarProviderError::SimulationFailed(msg) => TransactionError::SimulationFailed(msg),
97            StellarProviderError::InsufficientBalance(msg) => {
98                TransactionError::InsufficientBalance(msg)
99            }
100            StellarProviderError::BadSeq(msg) => TransactionError::ValidationError(msg),
101            StellarProviderError::RpcError(msg) | StellarProviderError::Unknown(msg) => {
102                TransactionError::UnderlyingProvider(ProviderError::NetworkConfiguration(msg))
103            }
104        }
105    }
106}
107
108impl From<xdr::Error> for TransactionError {
109    fn from(error: xdr::Error) -> Self {
110        TransactionError::ValidationError(format!("XDR error: {}", error))
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn test_transaction_error_display() {
120        let test_cases = vec![
121            (
122                TransactionError::ValidationError("invalid input".to_string()),
123                "Transaction validation error: invalid input",
124            ),
125            (
126                TransactionError::NetworkConfiguration("wrong network".to_string()),
127                "Network configuration error: wrong network",
128            ),
129            (
130                TransactionError::InvalidType("unknown type".to_string()),
131                "Invalid transaction type: unknown type",
132            ),
133            (
134                TransactionError::UnexpectedError("something went wrong".to_string()),
135                "Unexpected error: something went wrong",
136            ),
137            (
138                TransactionError::NotSupported("feature unavailable".to_string()),
139                "Not supported: feature unavailable",
140            ),
141            (
142                TransactionError::SignerError("key error".to_string()),
143                "Signer error: key error",
144            ),
145            (
146                TransactionError::InsufficientBalance("not enough funds".to_string()),
147                "Insufficient balance: not enough funds",
148            ),
149            (
150                TransactionError::SimulationFailed("sim failed".to_string()),
151                "Stellar transaction simulation failed: sim failed",
152            ),
153        ];
154
155        for (error, expected_message) in test_cases {
156            assert_eq!(error.to_string(), expected_message);
157        }
158    }
159
160    #[test]
161    fn test_transaction_error_to_api_error() {
162        let test_cases = vec![
163            (
164                TransactionError::ValidationError("invalid input".to_string()),
165                ApiError::BadRequest("invalid input".to_string()),
166            ),
167            (
168                TransactionError::NetworkConfiguration("wrong network".to_string()),
169                ApiError::InternalError("wrong network".to_string()),
170            ),
171            (
172                TransactionError::InvalidType("unknown type".to_string()),
173                ApiError::InternalError("unknown type".to_string()),
174            ),
175            (
176                TransactionError::UnexpectedError("something went wrong".to_string()),
177                ApiError::InternalError("something went wrong".to_string()),
178            ),
179            (
180                TransactionError::NotSupported("feature unavailable".to_string()),
181                ApiError::BadRequest("feature unavailable".to_string()),
182            ),
183            (
184                TransactionError::SignerError("key error".to_string()),
185                ApiError::InternalError("key error".to_string()),
186            ),
187            (
188                TransactionError::InsufficientBalance("not enough funds".to_string()),
189                ApiError::BadRequest("not enough funds".to_string()),
190            ),
191            (
192                TransactionError::SimulationFailed("boom".to_string()),
193                ApiError::BadRequest("boom".to_string()),
194            ),
195        ];
196
197        for (tx_error, expected_api_error) in test_cases {
198            let api_error = ApiError::from(tx_error);
199
200            match (&api_error, &expected_api_error) {
201                (ApiError::BadRequest(actual), ApiError::BadRequest(expected)) => {
202                    assert_eq!(actual, expected);
203                }
204                (ApiError::InternalError(actual), ApiError::InternalError(expected)) => {
205                    assert_eq!(actual, expected);
206                }
207                _ => panic!(
208                    "Error types don't match: {:?} vs {:?}",
209                    api_error, expected_api_error
210                ),
211            }
212        }
213    }
214
215    #[test]
216    fn test_repository_error_to_transaction_error() {
217        let repo_error = RepositoryError::NotFound("record not found".to_string());
218        let tx_error = TransactionError::from(repo_error);
219
220        match tx_error {
221            TransactionError::ValidationError(msg) => {
222                assert_eq!(msg, "Entity not found: record not found");
223            }
224            _ => panic!("Expected TransactionError::ValidationError"),
225        }
226    }
227
228    #[test]
229    fn test_report_to_transaction_error() {
230        let report = Report::msg("An unexpected error occurred");
231        let tx_error = TransactionError::from(report);
232
233        match tx_error {
234            TransactionError::UnexpectedError(msg) => {
235                assert!(msg.contains("An unexpected error occurred"));
236            }
237            _ => panic!("Expected TransactionError::UnexpectedError"),
238        }
239    }
240
241    #[test]
242    fn test_signer_factory_error_to_transaction_error() {
243        let factory_error = SignerFactoryError::InvalidConfig("missing key".to_string());
244        let tx_error = TransactionError::from(factory_error);
245
246        match tx_error {
247            TransactionError::SignerError(msg) => {
248                assert!(msg.contains("missing key"));
249            }
250            _ => panic!("Expected TransactionError::SignerError"),
251        }
252    }
253
254    #[test]
255    fn test_signer_error_to_transaction_error() {
256        let signer_error = SignerError::KeyError("invalid key format".to_string());
257        let tx_error = TransactionError::from(signer_error);
258
259        match tx_error {
260            TransactionError::SignerError(msg) => {
261                assert!(msg.contains("invalid key format"));
262            }
263            _ => panic!("Expected TransactionError::SignerError"),
264        }
265    }
266
267    #[test]
268    fn test_provider_error_conversion() {
269        let provider_error = ProviderError::NetworkConfiguration("timeout".to_string());
270        let tx_error = TransactionError::from(provider_error);
271
272        match tx_error {
273            TransactionError::UnderlyingProvider(err) => {
274                assert!(err.to_string().contains("timeout"));
275            }
276            _ => panic!("Expected TransactionError::UnderlyingProvider"),
277        }
278    }
279
280    #[test]
281    fn test_solana_provider_error_conversion() {
282        let solana_error = SolanaProviderError::RpcError("invalid response".to_string());
283        let tx_error = TransactionError::from(solana_error);
284
285        match tx_error {
286            TransactionError::UnderlyingSolanaProvider(err) => {
287                assert!(err.to_string().contains("invalid response"));
288            }
289            _ => panic!("Expected TransactionError::UnderlyingSolanaProvider"),
290        }
291    }
292
293    #[test]
294    fn test_job_producer_error_conversion() {
295        let job_error = JobProducerError::QueueError("queue full".to_string());
296        let tx_error = TransactionError::from(job_error);
297
298        match tx_error {
299            TransactionError::JobProducerError(err) => {
300                assert!(err.to_string().contains("queue full"));
301            }
302            _ => panic!("Expected TransactionError::JobProducerError"),
303        }
304    }
305
306    #[test]
307    fn test_xdr_error_conversion() {
308        use soroban_rs::xdr::{Limits, ReadXdr, TransactionEnvelope};
309
310        // Create an XDR error by trying to parse invalid base64
311        let xdr_error =
312            TransactionEnvelope::from_xdr_base64("invalid_base64", Limits::none()).unwrap_err();
313
314        let tx_error = TransactionError::from(xdr_error);
315
316        match tx_error {
317            TransactionError::ValidationError(msg) => {
318                assert!(msg.contains("XDR error:"));
319            }
320            _ => panic!("Expected TransactionError::ValidationError"),
321        }
322    }
323}