Skip to main content

electrum_client/
config.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use std::sync::Arc;
4use std::time::Duration;
5
6/// A function that provides authorization tokens dynamically (e.g., for JWT refresh)
7pub type AuthProvider = Arc<dyn Fn() -> Option<String> + Send + Sync>;
8
9/// Configuration for an electrum client
10///
11/// Refer to [`Client::from_config`] and [`ClientType::from_config`].
12///
13/// [`Client::from_config`]: crate::Client::from_config
14/// [`ClientType::from_config`]: crate::ClientType::from_config
15#[derive(Clone)]
16pub struct Config {
17    /// Proxy socks5 configuration, default None
18    socks5: Option<Socks5Config>,
19    /// timeout in seconds, default None (depends on TcpStream default)
20    timeout: Option<Duration>,
21    /// number of retry if any error, default 1
22    retry: u8,
23    /// when ssl, validate the domain, default true
24    validate_domain: bool,
25    /// Optional authorization provider for dynamic token injection
26    authorization_provider: Option<AuthProvider>,
27}
28
29// Custom Debug impl because AuthProvider doesn't implement Debug
30impl std::fmt::Debug for Config {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        f.debug_struct("Config")
33            .field("socks5", &self.socks5)
34            .field("timeout", &self.timeout)
35            .field("retry", &self.retry)
36            .field("validate_domain", &self.validate_domain)
37            .field(
38                "authorization_provider",
39                &self.authorization_provider.as_ref().map(|_| "<provider>"),
40            )
41            .finish()
42    }
43}
44
45/// Configuration for Socks5
46#[derive(Debug, Clone)]
47pub struct Socks5Config {
48    /// The address of the socks5 service
49    pub addr: String,
50    /// Optional credential for the service
51    pub credentials: Option<Socks5Credential>,
52}
53
54/// Credential for the proxy
55#[derive(Debug, Clone)]
56pub struct Socks5Credential {
57    pub username: String,
58    pub password: String,
59}
60
61/// [Config] Builder
62pub struct ConfigBuilder {
63    config: Config,
64}
65
66impl ConfigBuilder {
67    /// Create a builder with a default config, equivalent to [ConfigBuilder::default()]
68    pub fn new() -> Self {
69        ConfigBuilder {
70            config: Config::default(),
71        }
72    }
73
74    /// Set the socks5 config if Some, it accept an `Option` because it's easier for the caller to use
75    /// in a method chain
76    pub fn socks5(mut self, socks5_config: Option<Socks5Config>) -> Self {
77        self.config.socks5 = socks5_config;
78        self
79    }
80
81    /// Sets the timeout
82    pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
83        self.config.timeout = timeout;
84        self
85    }
86
87    /// Sets the retry attempts number
88    pub fn retry(mut self, retry: u8) -> Self {
89        self.config.retry = retry;
90        self
91    }
92
93    /// Sets if the domain has to be validated
94    pub fn validate_domain(mut self, validate_domain: bool) -> Self {
95        self.config.validate_domain = validate_domain;
96        self
97    }
98
99    /// Sets the authorization provider for dynamic token injection
100    pub fn authorization_provider(mut self, provider: Option<AuthProvider>) -> Self {
101        self.config.authorization_provider = provider;
102        self
103    }
104
105    /// Return the config and consume the builder
106    pub fn build(self) -> Config {
107        self.config
108    }
109}
110
111impl Default for ConfigBuilder {
112    fn default() -> Self {
113        Self::new()
114    }
115}
116
117impl Socks5Config {
118    /// Socks5Config constructor without credentials
119    pub fn new(addr: impl ToString) -> Self {
120        let addr = addr.to_string().replacen("socks5://", "", 1);
121        Socks5Config {
122            addr,
123            credentials: None,
124        }
125    }
126
127    /// Socks5Config constructor if we have credentials
128    pub fn with_credentials(addr: impl ToString, username: String, password: String) -> Self {
129        let mut config = Socks5Config::new(addr);
130        config.credentials = Some(Socks5Credential { username, password });
131        config
132    }
133}
134
135impl Config {
136    /// Get the configuration for `socks5`
137    ///
138    /// Set this with [`ConfigBuilder::socks5`]
139    pub fn socks5(&self) -> &Option<Socks5Config> {
140        &self.socks5
141    }
142
143    /// Get the configuration for `retry`
144    ///
145    /// Set this with [`ConfigBuilder::retry`]
146    pub fn retry(&self) -> u8 {
147        self.retry
148    }
149
150    /// Get the configuration for `timeout`
151    ///
152    /// Set this with [`ConfigBuilder::timeout`]
153    pub fn timeout(&self) -> Option<Duration> {
154        self.timeout
155    }
156
157    /// Get the configuration for `validate_domain`
158    ///
159    /// Set this with [`ConfigBuilder::validate_domain`]
160    pub fn validate_domain(&self) -> bool {
161        self.validate_domain
162    }
163
164    /// Get the configuration for `authorization_provider`
165    ///
166    /// Set this with [`ConfigBuilder::authorization_provider`]
167    pub fn authorization_provider(&self) -> Option<&AuthProvider> {
168        self.authorization_provider.as_ref()
169    }
170
171    /// Convenience method for calling [`ConfigBuilder::new`]
172    pub fn builder() -> ConfigBuilder {
173        ConfigBuilder::new()
174    }
175}
176
177impl Default for Config {
178    fn default() -> Self {
179        Config {
180            socks5: None,
181            timeout: None,
182            retry: 1,
183            validate_domain: true,
184            authorization_provider: None,
185        }
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_authorization_provider_builder() {
195        let token = "test-token-123".to_string();
196        let provider = Arc::new(move || Some(format!("Bearer {}", token)));
197
198        let config = ConfigBuilder::new()
199            .authorization_provider(Some(provider.clone()))
200            .build();
201
202        assert!(config.authorization_provider().is_some());
203
204        // Test that the provider returns the expected value
205        if let Some(auth_provider) = config.authorization_provider() {
206            assert_eq!(auth_provider(), Some("Bearer test-token-123".to_string()));
207        }
208    }
209
210    #[test]
211    fn test_authorization_provider_none() {
212        let config = ConfigBuilder::new().build();
213
214        assert!(config.authorization_provider().is_none());
215    }
216
217    #[test]
218    fn test_authorization_provider_returns_none() {
219        let provider = Arc::new(|| None);
220
221        let config = ConfigBuilder::new()
222            .authorization_provider(Some(provider))
223            .build();
224
225        assert!(config.authorization_provider().is_some());
226
227        // Test that the provider returns None
228        if let Some(auth_provider) = config.authorization_provider() {
229            assert_eq!(auth_provider(), None);
230        }
231    }
232
233    #[test]
234    fn test_authorization_provider_dynamic_token() {
235        use std::sync::RwLock;
236
237        // Simulate a token that can be updated
238        let token = Arc::new(RwLock::new("initial-token".to_string()));
239        let token_clone = token.clone();
240
241        let provider = Arc::new(move || Some(token_clone.read().unwrap().clone()));
242
243        let config = ConfigBuilder::new()
244            .authorization_provider(Some(provider.clone()))
245            .build();
246
247        // Initial token
248        if let Some(auth_provider) = config.authorization_provider() {
249            assert_eq!(auth_provider(), Some("initial-token".to_string()));
250        }
251
252        // Update the token
253        *token.write().unwrap() = "refreshed-token".to_string();
254
255        // Provider should return the new token
256        if let Some(auth_provider) = config.authorization_provider() {
257            assert_eq!(auth_provider(), Some("refreshed-token".to_string()));
258        }
259    }
260
261    #[test]
262    fn test_config_debug_with_provider() {
263        let provider = Arc::new(|| Some("secret-token".to_string()));
264
265        let config = ConfigBuilder::new()
266            .authorization_provider(Some(provider))
267            .build();
268
269        let debug_str = format!("{:?}", config);
270
271        // Should show <provider> instead of the actual function pointer
272        assert!(debug_str.contains("<provider>"));
273        // Should not leak the token value
274        assert!(!debug_str.contains("secret-token"));
275    }
276
277    #[test]
278    fn test_config_debug_without_provider() {
279        let config = ConfigBuilder::new().build();
280
281        let debug_str = format!("{:?}", config);
282
283        // Should show None for authorization_provider
284        assert!(debug_str.contains("authorization_provider"));
285    }
286}