openzeppelin_relayer/models/error/
transaction.rs1use 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 pub fn is_transient(&self) -> bool {
77 match self {
78 TransactionError::SolanaValidation(err) => err.is_transient(),
80 TransactionError::UnderlyingSolanaProvider(err) => err.is_transient(),
81 TransactionError::UnderlyingProvider(err) => err.is_transient(),
82
83 TransactionError::UnexpectedError(_) => true,
85 TransactionError::JobProducerError(_) => true,
86
87 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 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 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 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 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 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 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 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 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 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 let _ = tx_error.is_transient();
476
477 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 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}