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!(
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 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 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 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 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 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 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 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 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 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 let _ = tx_error.is_transient();
487
488 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 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}