openzeppelin_monitor/services/blockchain/
error.rs

1//! Blockchain service error types and handling.
2//!
3//! Provides a comprehensive error handling system for blockchain operations,
4//! including network connectivity, request processing, and blockchain-specific errors.
5
6use crate::utils::logging::error::{ErrorContext, TraceableError};
7use std::collections::HashMap;
8use thiserror::Error as ThisError;
9use uuid::Uuid;
10
11/// Represents possible errors that can occur during blockchain operations
12#[derive(ThisError, Debug)]
13pub enum BlockChainError {
14	/// Errors related to network connectivity issues
15	#[error("Connection error: {0}")]
16	ConnectionError(ErrorContext),
17
18	/// Errors related to malformed requests or invalid responses
19	#[error("Request error: {0}")]
20	RequestError(ErrorContext),
21
22	/// When a requested block cannot be found on the blockchain
23	#[error("Block not found: {0}")]
24	BlockNotFound(ErrorContext),
25
26	/// Errors related to transaction processing
27	#[error("Transaction error: {0}")]
28	TransactionError(ErrorContext),
29
30	/// Internal errors within the blockchain client
31	#[error("Internal error: {0}")]
32	InternalError(ErrorContext),
33
34	/// Errors related to client pool
35	#[error("Client pool error: {0}")]
36	ClientPoolError(ErrorContext),
37
38	/// Other errors that don't fit into the categories above
39	#[error(transparent)]
40	Other(#[from] anyhow::Error),
41}
42
43impl BlockChainError {
44	// Connection error
45	pub fn connection_error(
46		msg: impl Into<String>,
47		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
48		metadata: Option<HashMap<String, String>>,
49	) -> Self {
50		Self::ConnectionError(ErrorContext::new_with_log(msg, source, metadata))
51	}
52
53	// Request error
54	pub fn request_error(
55		msg: impl Into<String>,
56		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
57		metadata: Option<HashMap<String, String>>,
58	) -> Self {
59		Self::RequestError(ErrorContext::new_with_log(msg, source, metadata))
60	}
61
62	// Block not found
63	pub fn block_not_found(
64		msg: impl Into<String>,
65		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
66		metadata: Option<HashMap<String, String>>,
67	) -> Self {
68		Self::BlockNotFound(ErrorContext::new_with_log(msg, source, metadata))
69	}
70
71	// Transaction error
72	pub fn transaction_error(
73		msg: impl Into<String>,
74		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
75		metadata: Option<HashMap<String, String>>,
76	) -> Self {
77		Self::TransactionError(ErrorContext::new_with_log(msg, source, metadata))
78	}
79
80	// Internal error
81	pub fn internal_error(
82		msg: impl Into<String>,
83		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
84		metadata: Option<HashMap<String, String>>,
85	) -> Self {
86		Self::InternalError(ErrorContext::new_with_log(msg, source, metadata))
87	}
88
89	// Client pool error
90	pub fn client_pool_error(
91		msg: impl Into<String>,
92		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
93		metadata: Option<HashMap<String, String>>,
94	) -> Self {
95		Self::ClientPoolError(ErrorContext::new_with_log(msg, source, metadata))
96	}
97}
98
99impl TraceableError for BlockChainError {
100	fn trace_id(&self) -> String {
101		match self {
102			Self::ConnectionError(ctx) => ctx.trace_id.clone(),
103			Self::RequestError(ctx) => ctx.trace_id.clone(),
104			Self::BlockNotFound(ctx) => ctx.trace_id.clone(),
105			Self::TransactionError(ctx) => ctx.trace_id.clone(),
106			Self::InternalError(ctx) => ctx.trace_id.clone(),
107			Self::ClientPoolError(ctx) => ctx.trace_id.clone(),
108			Self::Other(_) => Uuid::new_v4().to_string(),
109		}
110	}
111}
112
113#[cfg(test)]
114mod tests {
115	use super::*;
116	use std::io::{Error as IoError, ErrorKind};
117
118	#[test]
119	fn test_connection_error_formatting() {
120		let error = BlockChainError::connection_error("test error", None, None);
121		assert_eq!(error.to_string(), "Connection error: test error");
122
123		let source_error = IoError::new(ErrorKind::NotFound, "test source");
124		let error = BlockChainError::connection_error(
125			"test error",
126			Some(Box::new(source_error)),
127			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
128		);
129		assert_eq!(
130			error.to_string(),
131			"Connection error: test error [key1=value1]"
132		);
133	}
134
135	#[test]
136	fn test_request_error_formatting() {
137		let error = BlockChainError::request_error("test error", None, None);
138		assert_eq!(error.to_string(), "Request error: test error");
139
140		let source_error = IoError::new(ErrorKind::NotFound, "test source");
141		let error = BlockChainError::request_error(
142			"test error",
143			Some(Box::new(source_error)),
144			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
145		);
146		assert_eq!(error.to_string(), "Request error: test error [key1=value1]");
147	}
148
149	#[test]
150	fn test_block_not_found_formatting() {
151		let error = BlockChainError::block_not_found("1".to_string(), None, None);
152		assert_eq!(error.to_string(), "Block not found: 1");
153
154		let source_error = IoError::new(ErrorKind::NotFound, "test source");
155		let error = BlockChainError::block_not_found(
156			"1".to_string(),
157			Some(Box::new(source_error)),
158			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
159		);
160		assert_eq!(error.to_string(), "Block not found: 1 [key1=value1]");
161	}
162
163	#[test]
164	fn test_transaction_error_formatting() {
165		let error = BlockChainError::transaction_error("test error", None, None);
166		assert_eq!(error.to_string(), "Transaction error: test error");
167
168		let source_error = IoError::new(ErrorKind::NotFound, "test source");
169		let error = BlockChainError::transaction_error(
170			"test error",
171			Some(Box::new(source_error)),
172			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
173		);
174		assert_eq!(
175			error.to_string(),
176			"Transaction error: test error [key1=value1]"
177		);
178	}
179
180	#[test]
181	fn test_internal_error_formatting() {
182		let error = BlockChainError::internal_error("test error", None, None);
183		assert_eq!(error.to_string(), "Internal error: test error");
184
185		let source_error = IoError::new(ErrorKind::NotFound, "test source");
186		let error = BlockChainError::internal_error(
187			"test error",
188			Some(Box::new(source_error)),
189			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
190		);
191		assert_eq!(
192			error.to_string(),
193			"Internal error: test error [key1=value1]"
194		);
195	}
196
197	#[test]
198	fn test_client_pool_error_formatting() {
199		let error = BlockChainError::client_pool_error("test error", None, None);
200		assert_eq!(error.to_string(), "Client pool error: test error");
201
202		let source_error = IoError::new(ErrorKind::NotFound, "test source");
203		let error = BlockChainError::client_pool_error(
204			"test error",
205			Some(Box::new(source_error)),
206			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
207		);
208		assert_eq!(
209			error.to_string(),
210			"Client pool error: test error [key1=value1]"
211		);
212	}
213
214	#[test]
215	fn test_from_anyhow_error() {
216		let anyhow_error = anyhow::anyhow!("test anyhow error");
217		let block_chain_error: BlockChainError = anyhow_error.into();
218		assert!(matches!(block_chain_error, BlockChainError::Other(_)));
219		assert_eq!(block_chain_error.to_string(), "test anyhow error");
220	}
221
222	#[test]
223	fn test_error_source_chain() {
224		let io_error = std::io::Error::new(std::io::ErrorKind::Other, "while reading config");
225
226		let outer_error =
227			BlockChainError::request_error("Failed to initialize", Some(Box::new(io_error)), None);
228
229		// Just test the string representation instead of the source chain
230		assert!(outer_error.to_string().contains("Failed to initialize"));
231
232		// For BlockChainError::RequestError, we know the implementation details
233		if let BlockChainError::RequestError(ctx) = &outer_error {
234			// Check that the context has the right message
235			assert_eq!(ctx.message, "Failed to initialize");
236
237			// Check that the context has the source error
238			assert!(ctx.source.is_some());
239
240			if let Some(src) = &ctx.source {
241				assert_eq!(src.to_string(), "while reading config");
242			}
243		} else {
244			panic!("Expected RequestError variant");
245		}
246	}
247
248	#[test]
249	fn test_trace_id_propagation() {
250		// Create an error context with a known trace ID
251		let error_context = ErrorContext::new("Inner error", None, None);
252		let original_trace_id = error_context.trace_id.clone();
253
254		// Wrap it in a BlockChainError
255		let block_chain_error = BlockChainError::RequestError(error_context);
256
257		// Verify the trace ID is preserved
258		assert_eq!(block_chain_error.trace_id(), original_trace_id);
259
260		// Test trace ID propagation through error chain
261		let source_error = IoError::new(ErrorKind::Other, "Source error");
262		let error_context = ErrorContext::new("Middle error", Some(Box::new(source_error)), None);
263		let original_trace_id = error_context.trace_id.clone();
264
265		let block_chain_error = BlockChainError::RequestError(error_context);
266		assert_eq!(block_chain_error.trace_id(), original_trace_id);
267
268		// Test Other variant
269		let anyhow_error = anyhow::anyhow!("Test anyhow error");
270		let block_chain_error: BlockChainError = anyhow_error.into();
271
272		// Other variant should generate a new UUID
273		assert!(!block_chain_error.trace_id().is_empty());
274	}
275}