openzeppelin_relayer/models/error/
transaction.rs1use 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 pub fn is_transient(&self) -> bool {
72 match self {
73 TransactionError::SolanaValidation(err) => err.is_transient(),
75 TransactionError::UnderlyingSolanaProvider(err) => err.is_transient(),
76 TransactionError::UnderlyingProvider(err) => err.is_transient(),
77
78 TransactionError::UnexpectedError(_) => true,
80 TransactionError::JobProducerError(_) => true,
81
82 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 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 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 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 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 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 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 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 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 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 let _ = tx_error.is_transient();
478
479 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 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}