openzeppelin_relayer/models/relayer/
rpc_config.rs

1//! Configuration for RPC endpoints.
2//!
3//! This module provides configuration structures for RPC endpoints,
4//! including URLs and weights for load balancing.
5
6use crate::constants::DEFAULT_RPC_WEIGHT;
7use eyre::{eyre, Result};
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10use utoipa::ToSchema;
11
12#[derive(Debug, Error, PartialEq)]
13pub enum RpcConfigError {
14    #[error("Invalid weight: {value}. Must be between 0 and 100.")]
15    InvalidWeight { value: u8 },
16}
17
18/// Returns the default RPC weight.
19fn default_rpc_weight() -> u8 {
20    DEFAULT_RPC_WEIGHT
21}
22
23/// Configuration for an RPC endpoint.
24#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema)]
25pub struct RpcConfig {
26    /// The RPC endpoint URL.
27    pub url: String,
28    /// The weight of this endpoint in the weighted round-robin selection.
29    /// Defaults to DEFAULT_RPC_WEIGHT (100). Should be between 0 and 100.
30    #[serde(default = "default_rpc_weight")]
31    pub weight: u8,
32}
33
34impl RpcConfig {
35    /// Creates a new RPC configuration with the given URL and default weight (DEFAULT_RPC_WEIGHT).
36    ///
37    /// # Arguments
38    ///
39    /// * `url` - A string slice that holds the URL of the RPC endpoint.
40    pub fn new(url: String) -> Self {
41        Self {
42            url,
43            weight: DEFAULT_RPC_WEIGHT,
44        }
45    }
46
47    /// Creates a new RPC configuration with the given URL and weight.
48    ///
49    /// # Arguments
50    ///
51    /// * `url` - A string that holds the URL of the RPC endpoint.
52    /// * `weight` - A u8 value representing the weight of the endpoint. Must be between 0 and 100 (inclusive).
53    ///
54    /// # Returns
55    ///
56    /// * `Ok(RpcConfig)` if the weight is valid.
57    /// * `Err(RpcConfigError::InvalidWeight)` if the weight is greater than 100.
58    pub fn with_weight(url: String, weight: u8) -> Result<Self, RpcConfigError> {
59        if weight > 100 {
60            return Err(RpcConfigError::InvalidWeight { value: weight });
61        }
62        Ok(Self { url, weight })
63    }
64
65    /// Gets the weight of this RPC endpoint.
66    ///
67    /// # Returns
68    ///
69    /// * `u8` - The weight of the RPC endpoint.
70    pub fn get_weight(&self) -> u8 {
71        self.weight
72    }
73
74    /// Validates that a URL has an HTTP or HTTPS scheme.
75    /// Helper function, hence private.
76    fn validate_url_scheme(url: &str) -> Result<()> {
77        if !url.starts_with("http://") && !url.starts_with("https://") {
78            return Err(eyre!(
79                "Invalid URL scheme for {}: Only HTTP and HTTPS are supported",
80                url
81            ));
82        }
83        Ok(())
84    }
85
86    /// Validates all URLs in a slice of RpcConfig objects.
87    ///
88    /// # Arguments
89    /// * `configs` - A slice of RpcConfig objects
90    ///
91    /// # Returns
92    /// * `Result<()>` - Ok if all URLs have valid schemes, error on first invalid URL
93    ///
94    /// # Examples
95    /// ```rust, ignore
96    /// use crate::models::RpcConfig;
97    ///
98    /// let configs = vec![
99    ///     RpcConfig::new("https://api.example.com".to_string()),
100    ///     RpcConfig::new("http://localhost:8545".to_string()),
101    /// ];
102    /// assert!(RpcConfig::validate_list(&configs).is_ok());
103    /// ```
104    pub fn validate_list(configs: &[RpcConfig]) -> Result<()> {
105        for config in configs {
106            // Call the helper function using Self to refer to the type for associated functions
107            Self::validate_url_scheme(&config.url)?;
108        }
109        Ok(())
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::constants::DEFAULT_RPC_WEIGHT;
117
118    #[test]
119    fn test_new_creates_config_with_default_weight() {
120        let url = "https://example.com".to_string();
121        let config = RpcConfig::new(url.clone());
122
123        assert_eq!(config.url, url);
124        assert_eq!(config.weight, DEFAULT_RPC_WEIGHT);
125    }
126
127    #[test]
128    fn test_with_weight_creates_config_with_custom_weight() {
129        let url = "https://example.com".to_string();
130        let weight: u8 = 5;
131        let result = RpcConfig::with_weight(url.clone(), weight);
132        assert!(result.is_ok());
133
134        let config = result.unwrap();
135        assert_eq!(config.url, url);
136        assert_eq!(config.weight, weight);
137    }
138
139    #[test]
140    fn test_get_weight_returns_weight_value() {
141        let url = "https://example.com".to_string();
142        let weight: u8 = 10;
143        let config = RpcConfig { url, weight };
144
145        assert_eq!(config.get_weight(), weight);
146    }
147
148    #[test]
149    fn test_equality_of_configs() {
150        let url = "https://example.com".to_string();
151        let config1 = RpcConfig::new(url.clone());
152        let config2 = RpcConfig::new(url.clone()); // Same as config1
153        let config3 = RpcConfig::with_weight(url.clone(), 5u8).unwrap(); // Different weight
154        let config4 =
155            RpcConfig::with_weight("https://different.com".to_string(), DEFAULT_RPC_WEIGHT)
156                .unwrap(); // Different URL
157
158        assert_eq!(config1, config2);
159        assert_ne!(config1, config3);
160        assert_ne!(config1, config4);
161    }
162
163    // Tests for URL validation
164    #[test]
165    fn test_validate_url_scheme_with_http() {
166        let result = RpcConfig::validate_url_scheme("http://example.com");
167        assert!(result.is_ok(), "HTTP URL should be valid");
168    }
169
170    #[test]
171    fn test_validate_url_scheme_with_https() {
172        let result = RpcConfig::validate_url_scheme("https://secure.example.com");
173        assert!(result.is_ok(), "HTTPS URL should be valid");
174    }
175
176    #[test]
177    fn test_validate_url_scheme_with_query_params() {
178        let result =
179            RpcConfig::validate_url_scheme("https://example.com/api?param=value&other=123");
180        assert!(result.is_ok(), "URL with query parameters should be valid");
181    }
182
183    #[test]
184    fn test_validate_url_scheme_with_port() {
185        let result = RpcConfig::validate_url_scheme("http://localhost:8545");
186        assert!(result.is_ok(), "URL with port should be valid");
187    }
188
189    #[test]
190    fn test_validate_url_scheme_with_ftp() {
191        let result = RpcConfig::validate_url_scheme("ftp://example.com");
192        assert!(result.is_err(), "FTP URL should be invalid");
193    }
194
195    #[test]
196    fn test_validate_url_scheme_with_invalid_url() {
197        let result = RpcConfig::validate_url_scheme("invalid-url");
198        assert!(result.is_err(), "Invalid URL format should be rejected");
199    }
200
201    #[test]
202    fn test_validate_url_scheme_with_empty_string() {
203        let result = RpcConfig::validate_url_scheme("");
204        assert!(result.is_err(), "Empty string should be rejected");
205    }
206
207    // Tests for validate_list function
208    #[test]
209    fn test_validate_list_with_empty_vec() {
210        let configs: Vec<RpcConfig> = vec![];
211        let result = RpcConfig::validate_list(&configs);
212        assert!(result.is_ok(), "Empty config vector should be valid");
213    }
214
215    #[test]
216    fn test_validate_list_with_valid_urls() {
217        let configs = vec![
218            RpcConfig::new("https://api.example.com".to_string()),
219            RpcConfig::new("http://localhost:8545".to_string()),
220        ];
221        let result = RpcConfig::validate_list(&configs);
222        assert!(result.is_ok(), "All URLs are valid, should return Ok");
223    }
224
225    #[test]
226    fn test_validate_list_with_one_invalid_url() {
227        let configs = vec![
228            RpcConfig::new("https://api.example.com".to_string()),
229            RpcConfig::new("ftp://invalid-scheme.com".to_string()),
230            RpcConfig::new("http://another-valid.com".to_string()),
231        ];
232        let result = RpcConfig::validate_list(&configs);
233        assert!(result.is_err(), "Should fail on first invalid URL");
234    }
235
236    #[test]
237    fn test_validate_list_with_all_invalid_urls() {
238        let configs = vec![
239            RpcConfig::new("ws://websocket.example.com".to_string()),
240            RpcConfig::new("ftp://invalid-scheme.com".to_string()),
241        ];
242        let result = RpcConfig::validate_list(&configs);
243        assert!(result.is_err(), "Should fail with all invalid URLs");
244    }
245}