openzeppelin_relayer/utils/
error_sanitization.rs

1//! Error sanitization and mapping utilities for provider errors.
2//!
3//! This module provides network-agnostic utilities for sanitizing and mapping
4//! provider errors to JSON-RPC error codes and user-friendly messages.
5//!
6//! These utilities are used by all network relayers (EVM, Stellar, Solana) to
7//! ensure consistent error handling and prevent exposing sensitive information.
8
9use crate::{
10    models::{OpenZeppelinErrorCodes, RpcErrorCodes},
11    services::provider::ProviderError,
12};
13
14/// Maps provider errors to appropriate JSON-RPC error codes and messages.
15///
16/// This function translates internal provider errors into standardized
17/// JSON-RPC error codes and user-friendly messages that can be returned
18/// to clients. It follows JSON-RPC 2.0 specification for standard errors
19/// and uses OpenZeppelin-specific codes for extended functionality.
20///
21/// # Arguments
22///
23/// * `error` - A reference to the provider error to be mapped
24///
25/// # Returns
26///
27/// Returns a tuple containing:
28/// - `i32` - The error code (following JSON-RPC 2.0 and OpenZeppelin conventions)
29/// - `&'static str` - A static string describing the error type
30///
31/// # Error Code Mappings
32///
33/// - `InvalidAddress` → -32602 ("Invalid params")
34/// - `NetworkConfiguration` → -33004 ("Network configuration error")
35/// - `Timeout` → -33000 ("Request timeout")
36/// - `RateLimited` → -33001 ("Rate limited")
37/// - `BadGateway` → -33002 ("Bad gateway")
38/// - `RequestError` → -33003 ("Request error")
39/// - `Other` and unknown errors → -32603 ("Internal error")
40pub fn map_provider_error(error: &ProviderError) -> (i32, &'static str) {
41    match error {
42        ProviderError::InvalidAddress(_) => (RpcErrorCodes::INVALID_PARAMS, "Invalid params"),
43        ProviderError::NetworkConfiguration(_) => (
44            OpenZeppelinErrorCodes::NETWORK_CONFIGURATION,
45            "Network configuration error",
46        ),
47        ProviderError::Timeout => (OpenZeppelinErrorCodes::TIMEOUT, "Request timeout"),
48        ProviderError::RateLimited => (OpenZeppelinErrorCodes::RATE_LIMITED, "Rate limited"),
49        ProviderError::BadGateway => (OpenZeppelinErrorCodes::BAD_GATEWAY, "Bad gateway"),
50        ProviderError::RequestError { .. } => {
51            (OpenZeppelinErrorCodes::REQUEST_ERROR, "Request error")
52        }
53        ProviderError::Other(_) => (RpcErrorCodes::INTERNAL_ERROR, "Internal error"),
54        _ => (RpcErrorCodes::INTERNAL_ERROR, "Internal error"),
55    }
56}
57
58/// Sanitizes provider error descriptions to prevent exposing internal details.
59///
60/// This function creates a safe, user-friendly error description that doesn't
61/// expose sensitive information like API keys, internal URLs, or implementation
62/// details. The full error is logged internally for debugging purposes.
63///
64/// # Arguments
65///
66/// * `error` - A reference to the provider error to sanitize
67///
68/// # Returns
69///
70/// Returns a sanitized error description string that is safe to return to clients.
71pub fn sanitize_error_description(error: &ProviderError) -> String {
72    match error {
73        ProviderError::InvalidAddress(_) => "The provided address is invalid".to_string(),
74        ProviderError::NetworkConfiguration(_) => {
75            "Network configuration error. Please check your network settings".to_string()
76        }
77        ProviderError::Timeout => "The request timed out. Please try again later".to_string(),
78        ProviderError::RateLimited => "Rate limit exceeded. Please try again later".to_string(),
79        ProviderError::BadGateway => {
80            "Service temporarily unavailable. Please try again later".to_string()
81        }
82        ProviderError::RequestError { status_code, .. } => {
83            format!("Request failed with status code {status_code}")
84        }
85        ProviderError::RpcErrorCode { code, .. } => {
86            format!("RPC error occurred (code: {code})")
87        }
88        ProviderError::TransportError(_) => {
89            "Network error occurred. Please try again later".to_string()
90        }
91        ProviderError::SolanaRpcError(_) => {
92            "RPC request failed. Please try again later".to_string()
93        }
94        ProviderError::Other(_) => "An internal error occurred. Please try again later".to_string(),
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::services::provider::{rpc_selector::RpcSelectorError, SolanaProviderError};
102
103    #[test]
104    fn test_map_provider_error_invalid_address() {
105        let error = ProviderError::InvalidAddress("invalid address".to_string());
106        let (code, _message) = map_provider_error(&error);
107
108        assert_eq!(code, RpcErrorCodes::INVALID_PARAMS);
109    }
110
111    #[test]
112    fn test_map_provider_error_invalid_address_empty() {
113        let error = ProviderError::InvalidAddress("".to_string());
114        let (code, _message) = map_provider_error(&error);
115
116        assert_eq!(code, RpcErrorCodes::INVALID_PARAMS);
117    }
118
119    #[test]
120    fn test_map_provider_error_network_configuration() {
121        let error = ProviderError::NetworkConfiguration("network config error".to_string());
122        let (code, _message) = map_provider_error(&error);
123
124        assert_eq!(code, OpenZeppelinErrorCodes::NETWORK_CONFIGURATION);
125    }
126
127    #[test]
128    fn test_map_provider_error_network_configuration_empty() {
129        let error = ProviderError::NetworkConfiguration("".to_string());
130        let (code, _message) = map_provider_error(&error);
131
132        assert_eq!(code, OpenZeppelinErrorCodes::NETWORK_CONFIGURATION);
133    }
134
135    #[test]
136    fn test_map_provider_error_timeout() {
137        let error = ProviderError::Timeout;
138        let (code, _message) = map_provider_error(&error);
139
140        assert_eq!(code, OpenZeppelinErrorCodes::TIMEOUT);
141    }
142
143    #[test]
144    fn test_map_provider_error_rate_limited() {
145        let error = ProviderError::RateLimited;
146        let (code, _message) = map_provider_error(&error);
147
148        assert_eq!(code, OpenZeppelinErrorCodes::RATE_LIMITED);
149    }
150
151    #[test]
152    fn test_map_provider_error_bad_gateway() {
153        let error = ProviderError::BadGateway;
154        let (code, _message) = map_provider_error(&error);
155
156        assert_eq!(code, OpenZeppelinErrorCodes::BAD_GATEWAY);
157    }
158
159    #[test]
160    fn test_map_provider_error_request_error_400() {
161        let error = ProviderError::RequestError {
162            error: "Bad request".to_string(),
163            status_code: 400,
164        };
165        let (code, _message) = map_provider_error(&error);
166
167        assert_eq!(code, OpenZeppelinErrorCodes::REQUEST_ERROR);
168    }
169
170    #[test]
171    fn test_map_provider_error_request_error_500() {
172        let error = ProviderError::RequestError {
173            error: "Internal server error".to_string(),
174            status_code: 500,
175        };
176        let (code, _message) = map_provider_error(&error);
177
178        assert_eq!(code, OpenZeppelinErrorCodes::REQUEST_ERROR);
179    }
180
181    #[test]
182    fn test_map_provider_error_request_error_empty_message() {
183        let error = ProviderError::RequestError {
184            error: "".to_string(),
185            status_code: 404,
186        };
187        let (code, _message) = map_provider_error(&error);
188
189        assert_eq!(code, OpenZeppelinErrorCodes::REQUEST_ERROR);
190    }
191
192    #[test]
193    fn test_map_provider_error_request_error_zero_status() {
194        let error = ProviderError::RequestError {
195            error: "No status".to_string(),
196            status_code: 0,
197        };
198        let (code, _message) = map_provider_error(&error);
199
200        assert_eq!(code, OpenZeppelinErrorCodes::REQUEST_ERROR);
201    }
202
203    #[test]
204    fn test_map_provider_error_other() {
205        let error = ProviderError::Other("some other error".to_string());
206        let (code, _message) = map_provider_error(&error);
207
208        assert_eq!(code, RpcErrorCodes::INTERNAL_ERROR);
209    }
210
211    #[test]
212    fn test_map_provider_error_other_empty() {
213        let error = ProviderError::Other("".to_string());
214        let (code, _message) = map_provider_error(&error);
215
216        assert_eq!(code, RpcErrorCodes::INTERNAL_ERROR);
217    }
218
219    #[test]
220    fn test_map_provider_error_solana_rpc_error() {
221        let solana_error = SolanaProviderError::RpcError("Solana RPC failed".to_string());
222        let error = ProviderError::SolanaRpcError(solana_error);
223        let (code, _message) = map_provider_error(&error);
224
225        // The SolanaRpcError variant should be caught by the wildcard pattern
226        assert_eq!(code, RpcErrorCodes::INTERNAL_ERROR);
227    }
228
229    #[test]
230    fn test_map_provider_error_solana_invalid_address() {
231        let solana_error =
232            SolanaProviderError::InvalidAddress("Invalid Solana address".to_string());
233        let error = ProviderError::SolanaRpcError(solana_error);
234        let (code, _message) = map_provider_error(&error);
235
236        // The SolanaRpcError variant should be caught by the wildcard pattern
237        assert_eq!(code, RpcErrorCodes::INTERNAL_ERROR);
238    }
239
240    #[test]
241    fn test_map_provider_error_solana_selector_error() {
242        let selector_error = RpcSelectorError::NoProviders;
243        let solana_error = SolanaProviderError::SelectorError(selector_error);
244        let error = ProviderError::SolanaRpcError(solana_error);
245        let (code, _message) = map_provider_error(&error);
246
247        // The SolanaRpcError variant should be caught by the wildcard pattern
248        assert_eq!(code, RpcErrorCodes::INTERNAL_ERROR);
249    }
250
251    #[test]
252    fn test_map_provider_error_solana_network_configuration() {
253        let solana_error =
254            SolanaProviderError::NetworkConfiguration("Solana network config error".to_string());
255        let error = ProviderError::SolanaRpcError(solana_error);
256        let (code, _message) = map_provider_error(&error);
257
258        // The SolanaRpcError variant should be caught by the wildcard pattern
259        assert_eq!(code, RpcErrorCodes::INTERNAL_ERROR);
260    }
261
262    #[test]
263    fn test_map_provider_error_wildcard_pattern() {
264        // This test ensures the wildcard pattern works by testing all variations
265        // that should fall through to the default case
266        let test_cases = vec![
267            ProviderError::SolanaRpcError(SolanaProviderError::RpcError("test".to_string())),
268            ProviderError::SolanaRpcError(SolanaProviderError::InvalidAddress("test".to_string())),
269            ProviderError::SolanaRpcError(SolanaProviderError::NetworkConfiguration(
270                "test".to_string(),
271            )),
272            ProviderError::SolanaRpcError(SolanaProviderError::SelectorError(
273                RpcSelectorError::NoProviders,
274            )),
275        ];
276
277        for error in test_cases {
278            let (code, _message) = map_provider_error(&error);
279            assert_eq!(code, RpcErrorCodes::INTERNAL_ERROR);
280        }
281    }
282
283    #[test]
284    fn test_sanitize_error_description_invalid_address() {
285        let error = ProviderError::InvalidAddress("0xinvalid".to_string());
286        let description = sanitize_error_description(&error);
287        assert_eq!(description, "The provided address is invalid");
288        // Ensure no internal details are exposed
289        assert!(!description.contains("0xinvalid"));
290    }
291
292    #[test]
293    fn test_sanitize_error_description_network_configuration() {
294        let error =
295            ProviderError::NetworkConfiguration("RPC selector error: No providers".to_string());
296        let description = sanitize_error_description(&error);
297        assert_eq!(
298            description,
299            "Network configuration error. Please check your network settings"
300        );
301        // Ensure no internal details are exposed
302        assert!(!description.contains("RPC selector"));
303        assert!(!description.contains("No providers"));
304    }
305
306    #[test]
307    fn test_sanitize_error_description_timeout() {
308        let error = ProviderError::Timeout;
309        let description = sanitize_error_description(&error);
310        assert_eq!(description, "The request timed out. Please try again later");
311    }
312
313    #[test]
314    fn test_sanitize_error_description_rate_limited() {
315        let error = ProviderError::RateLimited;
316        let description = sanitize_error_description(&error);
317        assert_eq!(description, "Rate limit exceeded. Please try again later");
318    }
319
320    #[test]
321    fn test_sanitize_error_description_bad_gateway() {
322        let error = ProviderError::BadGateway;
323        let description = sanitize_error_description(&error);
324        assert_eq!(
325            description,
326            "Service temporarily unavailable. Please try again later"
327        );
328    }
329
330    #[test]
331    fn test_sanitize_error_description_request_error() {
332        let error = ProviderError::RequestError {
333            error: "API key invalid: abc123".to_string(),
334            status_code: 401,
335        };
336        let description = sanitize_error_description(&error);
337        assert_eq!(description, "Request failed with status code 401");
338        // Ensure no API key details are exposed
339        assert!(!description.contains("API key"));
340        assert!(!description.contains("abc123"));
341    }
342
343    #[test]
344    fn test_sanitize_error_description_rpc_error_code() {
345        let error = ProviderError::RpcErrorCode {
346            code: -32000,
347            message: "Server error: Invalid API key".to_string(),
348        };
349        let description = sanitize_error_description(&error);
350        assert_eq!(description, "RPC error occurred (code: -32000)");
351        // Ensure no internal message details are exposed
352        assert!(!description.contains("Server error"));
353        assert!(!description.contains("API key"));
354    }
355
356    #[test]
357    fn test_sanitize_error_description_transport_error() {
358        let error = ProviderError::TransportError(
359            "Connection failed: https://rpc.example.com/api?key=secret".to_string(),
360        );
361        let description = sanitize_error_description(&error);
362        assert_eq!(
363            description,
364            "Network error occurred. Please try again later"
365        );
366        // Ensure no URLs or keys are exposed
367        assert!(!description.contains("https://"));
368        assert!(!description.contains("key="));
369        assert!(!description.contains("secret"));
370    }
371
372    #[test]
373    fn test_sanitize_error_description_solana_rpc_error() {
374        let solana_error =
375            SolanaProviderError::RpcError("RPC failed: Invalid API key abc123".to_string());
376        let error = ProviderError::SolanaRpcError(solana_error);
377        let description = sanitize_error_description(&error);
378        assert_eq!(description, "RPC request failed. Please try again later");
379        // Ensure no internal details are exposed
380        assert!(!description.contains("API key"));
381        assert!(!description.contains("abc123"));
382    }
383
384    #[test]
385    fn test_sanitize_error_description_other() {
386        let error = ProviderError::Other(
387            "Internal error: Failed to connect to https://rpc.example.com with key abc123"
388                .to_string(),
389        );
390        let description = sanitize_error_description(&error);
391        assert_eq!(
392            description,
393            "An internal error occurred. Please try again later"
394        );
395        // Ensure no internal details are exposed
396        assert!(!description.contains("Failed to connect"));
397        assert!(!description.contains("https://"));
398        assert!(!description.contains("key"));
399        assert!(!description.contains("abc123"));
400    }
401}