openzeppelin_relayer/config/
server_config.rs

1/// Configuration for the server, including network and rate limiting settings.
2use std::env;
3
4use crate::{constants::MINIMUM_SECRET_VALUE_LENGTH, models::SecretString};
5
6#[derive(Debug, Clone)]
7pub struct ServerConfig {
8    /// The host address the server will bind to.
9    pub host: String,
10    /// The port number the server will listen on.
11    pub port: u16,
12    /// The URL for the Redis instance.
13    pub redis_url: String,
14    /// The file path to the server's configuration file.
15    pub config_file_path: String,
16    /// The API key used for authentication.
17    pub api_key: SecretString,
18    /// The number of requests allowed per second.
19    pub rate_limit_requests_per_second: u64,
20    /// The maximum burst size for rate limiting.
21    pub rate_limit_burst_size: u32,
22    /// The port number for exposing metrics.
23    pub metrics_port: u16,
24    /// Enable Swagger UI.
25    pub enable_swagger: bool,
26    /// The number of seconds to wait for a Redis connection.
27    pub redis_connection_timeout_ms: u64,
28    /// The number of milliseconds to wait for an RPC response.
29    pub rpc_timeout_ms: u64,
30    /// Maximum number of retry attempts for provider operations.
31    pub provider_max_retries: u8,
32    /// Base delay between retry attempts (milliseconds).
33    pub provider_retry_base_delay_ms: u64,
34    /// Maximum delay between retry attempts (milliseconds).
35    pub provider_retry_max_delay_ms: u64,
36    /// Maximum number of failovers (switching to different providers).
37    pub provider_max_failovers: u8,
38}
39
40impl ServerConfig {
41    /// Creates a new `ServerConfig` instance from environment variables.
42    ///
43    /// # Panics
44    ///
45    /// This function will panic if the `REDIS_URL` or `API_KEY` environment
46    /// variables are not set, as they are required for the server to function.
47    ///
48    /// # Defaults
49    ///
50    /// - `HOST` defaults to `"0.0.0.0"`.
51    /// - `APP_PORT` defaults to `8080`.
52    /// - `CONFIG_DIR` defaults to `"config/config.json"`.
53    /// - `RATE_LIMIT_REQUESTS_PER_SECOND` defaults to `100`.
54    /// - `RATE_LIMIT_BURST_SIZE` defaults to `300`.
55    /// - `METRICS_PORT` defaults to `8081`.
56    /// - `PROVIDER_MAX_RETRIES` defaults to `3`.
57    /// - `PROVIDER_RETRY_BASE_DELAY_MS` defaults to `100`.
58    /// - `PROVIDER_RETRY_MAX_DELAY_MS` defaults to `2000`.
59    /// - `PROVIDER_MAX_FAILOVERS` defaults to `3`.
60    pub fn from_env() -> Self {
61        let conf_dir = if env::var("IN_DOCKER")
62            .map(|val| val == "true")
63            .unwrap_or(false)
64        {
65            "config/".to_string()
66        } else {
67            env::var("CONFIG_DIR").unwrap_or_else(|_| "./config".to_string())
68        };
69
70        let conf_dir = format!("{}/", conf_dir.trim_end_matches('/'));
71
72        // Get config filename (default: config.json), applies to both local and Docker
73        let config_file_name =
74            env::var("CONFIG_FILE_NAME").unwrap_or_else(|_| "config.json".to_string());
75
76        // Construct full path
77        let config_file_path = format!("{}{}", conf_dir, config_file_name);
78
79        let api_key = SecretString::new(&env::var("API_KEY").expect("API_KEY must be set"));
80
81        if !api_key.has_minimum_length(MINIMUM_SECRET_VALUE_LENGTH) {
82            panic!(
83                "Security error: API_KEY must be at least {} characters long",
84                MINIMUM_SECRET_VALUE_LENGTH
85            );
86        }
87
88        Self {
89            host: env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string()),
90            port: env::var("APP_PORT")
91                .unwrap_or_else(|_| "8080".to_string())
92                .parse()
93                .unwrap_or(8080),
94            redis_url: env::var("REDIS_URL").expect("REDIS_URL must be set"),
95            config_file_path,
96            api_key,
97            rate_limit_requests_per_second: env::var("RATE_LIMIT_REQUESTS_PER_SECOND")
98                .unwrap_or_else(|_| "100".to_string())
99                .parse()
100                .unwrap_or(100),
101            rate_limit_burst_size: env::var("RATE_LIMIT_BURST_SIZE")
102                .unwrap_or_else(|_| "300".to_string())
103                .parse()
104                .unwrap_or(300),
105            metrics_port: env::var("METRICS_PORT")
106                .unwrap_or_else(|_| "8081".to_string())
107                .parse()
108                .unwrap_or(8081),
109            enable_swagger: env::var("ENABLE_SWAGGER")
110                .map(|v| v.to_lowercase() == "true")
111                .unwrap_or(false),
112            redis_connection_timeout_ms: env::var("REDIS_CONNECTION_TIMEOUT_MS")
113                .unwrap_or_else(|_| "10000".to_string())
114                .parse()
115                .unwrap_or(10000),
116            rpc_timeout_ms: env::var("RPC_TIMEOUT_MS")
117                .unwrap_or_else(|_| "10000".to_string())
118                .parse()
119                .unwrap_or(10000),
120            provider_max_retries: env::var("PROVIDER_MAX_RETRIES")
121                .unwrap_or_else(|_| "3".to_string())
122                .parse()
123                .unwrap_or(3),
124            provider_retry_base_delay_ms: env::var("PROVIDER_RETRY_BASE_DELAY_MS")
125                .unwrap_or_else(|_| "100".to_string())
126                .parse()
127                .unwrap_or(100),
128            provider_retry_max_delay_ms: env::var("PROVIDER_RETRY_MAX_DELAY_MS")
129                .unwrap_or_else(|_| "2000".to_string())
130                .parse()
131                .unwrap_or(2000),
132            provider_max_failovers: env::var("PROVIDER_MAX_FAILOVERS")
133                .unwrap_or_else(|_| "3".to_string())
134                .parse()
135                .unwrap_or(3),
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use lazy_static::lazy_static;
144    use std::env;
145    use std::sync::Mutex;
146
147    // Use a mutex to ensure tests don't run in parallel when modifying env vars
148    lazy_static! {
149        static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
150    }
151
152    fn setup() {
153        // Clear all environment variables first
154        env::remove_var("HOST");
155        env::remove_var("APP_PORT");
156        env::remove_var("REDIS_URL");
157        env::remove_var("CONFIG_DIR");
158        env::remove_var("CONFIG_FILE_NAME");
159        env::remove_var("CONFIG_FILE_PATH");
160        env::remove_var("API_KEY");
161        env::remove_var("RATE_LIMIT_REQUESTS_PER_SECOND");
162        env::remove_var("RATE_LIMIT_BURST_SIZE");
163        env::remove_var("METRICS_PORT");
164        env::remove_var("REDIS_CONNECTION_TIMEOUT_MS");
165        env::remove_var("RPC_TIMEOUT_MS");
166        env::remove_var("PROVIDER_MAX_RETRIES");
167        env::remove_var("PROVIDER_RETRY_BASE_DELAY_MS");
168        env::remove_var("PROVIDER_RETRY_MAX_DELAY_MS");
169        env::remove_var("PROVIDER_MAX_FAILOVERS");
170        // Set required variables for most tests
171        env::set_var("REDIS_URL", "redis://localhost:6379");
172        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
173        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "5000");
174    }
175
176    #[test]
177    fn test_default_values() {
178        let _lock = match ENV_MUTEX.lock() {
179            Ok(guard) => guard,
180            Err(poisoned) => poisoned.into_inner(),
181        };
182        setup();
183
184        let config = ServerConfig::from_env();
185
186        assert_eq!(config.host, "0.0.0.0");
187        assert_eq!(config.port, 8080);
188        assert_eq!(config.redis_url, "redis://localhost:6379");
189        assert_eq!(config.config_file_path, "./config/config.json");
190        assert_eq!(
191            config.api_key,
192            SecretString::new("7EF1CB7C-5003-4696-B384-C72AF8C3E15D")
193        );
194        assert_eq!(config.rate_limit_requests_per_second, 100);
195        assert_eq!(config.rate_limit_burst_size, 300);
196        assert_eq!(config.metrics_port, 8081);
197        assert_eq!(config.redis_connection_timeout_ms, 5000);
198        assert_eq!(config.rpc_timeout_ms, 10000);
199        assert_eq!(config.provider_max_retries, 3);
200        assert_eq!(config.provider_retry_base_delay_ms, 100);
201        assert_eq!(config.provider_retry_max_delay_ms, 2000);
202        assert_eq!(config.provider_max_failovers, 3);
203    }
204
205    #[test]
206    fn test_invalid_port_values() {
207        let _lock = match ENV_MUTEX.lock() {
208            Ok(guard) => guard,
209            Err(poisoned) => poisoned.into_inner(),
210        };
211        setup();
212        env::set_var("REDIS_URL", "redis://localhost:6379");
213        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
214        env::set_var("APP_PORT", "not_a_number");
215        env::set_var("METRICS_PORT", "also_not_a_number");
216        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "invalid");
217        env::set_var("RATE_LIMIT_BURST_SIZE", "invalid");
218        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "invalid");
219        env::set_var("RPC_TIMEOUT_MS", "invalid");
220        env::set_var("PROVIDER_MAX_RETRIES", "invalid");
221        env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "invalid");
222        env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "invalid");
223        env::set_var("PROVIDER_MAX_FAILOVERS", "invalid");
224        let config = ServerConfig::from_env();
225
226        // Should fall back to defaults when parsing fails
227        assert_eq!(config.port, 8080);
228        assert_eq!(config.metrics_port, 8081);
229        assert_eq!(config.rate_limit_requests_per_second, 100);
230        assert_eq!(config.rate_limit_burst_size, 300);
231        assert_eq!(config.redis_connection_timeout_ms, 10000);
232        assert_eq!(config.rpc_timeout_ms, 10000);
233        assert_eq!(config.provider_max_retries, 3);
234        assert_eq!(config.provider_retry_base_delay_ms, 100);
235        assert_eq!(config.provider_retry_max_delay_ms, 2000);
236        assert_eq!(config.provider_max_failovers, 3);
237    }
238
239    #[test]
240    fn test_custom_values() {
241        let _lock = match ENV_MUTEX.lock() {
242            Ok(guard) => guard,
243            Err(poisoned) => poisoned.into_inner(),
244        };
245        setup();
246
247        env::set_var("HOST", "127.0.0.1");
248        env::set_var("APP_PORT", "9090");
249        env::set_var("REDIS_URL", "redis://custom:6379");
250        env::set_var("CONFIG_DIR", "custom");
251        env::set_var("CONFIG_FILE_NAME", "path.json");
252        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
253        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "200");
254        env::set_var("RATE_LIMIT_BURST_SIZE", "500");
255        env::set_var("METRICS_PORT", "9091");
256        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "10000");
257        env::set_var("RPC_TIMEOUT_MS", "33333");
258        env::set_var("PROVIDER_MAX_RETRIES", "5");
259        env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "200");
260        env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "3000");
261        env::set_var("PROVIDER_MAX_FAILOVERS", "4");
262        let config = ServerConfig::from_env();
263
264        assert_eq!(config.host, "127.0.0.1");
265        assert_eq!(config.port, 9090);
266        assert_eq!(config.redis_url, "redis://custom:6379");
267        assert_eq!(config.config_file_path, "custom/path.json");
268        assert_eq!(
269            config.api_key,
270            SecretString::new("7EF1CB7C-5003-4696-B384-C72AF8C3E15D")
271        );
272        assert_eq!(config.rate_limit_requests_per_second, 200);
273        assert_eq!(config.rate_limit_burst_size, 500);
274        assert_eq!(config.metrics_port, 9091);
275        assert_eq!(config.redis_connection_timeout_ms, 10000);
276        assert_eq!(config.rpc_timeout_ms, 33333);
277        assert_eq!(config.provider_max_retries, 5);
278        assert_eq!(config.provider_retry_base_delay_ms, 200);
279        assert_eq!(config.provider_retry_max_delay_ms, 3000);
280        assert_eq!(config.provider_max_failovers, 4);
281    }
282
283    #[test]
284    #[should_panic(expected = "Security error: API_KEY must be at least 32 characters long")]
285    fn test_invalid_api_key_length() {
286        let _lock = match ENV_MUTEX.lock() {
287            Ok(guard) => guard,
288            Err(poisoned) => poisoned.into_inner(),
289        };
290        setup();
291        env::set_var("REDIS_URL", "redis://localhost:6379");
292        env::set_var("API_KEY", "insufficient_length");
293        env::set_var("APP_PORT", "8080");
294        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "100");
295        env::set_var("RATE_LIMIT_BURST_SIZE", "300");
296        env::set_var("METRICS_PORT", "9091");
297
298        let _ = ServerConfig::from_env();
299
300        panic!("Test should have panicked before reaching here");
301    }
302}