1+ use std:: sync:: Arc ;
12use std:: time:: Duration ;
23
4+ /// A function that provides authorization tokens dynamically (e.g., for JWT refresh)
5+ pub type AuthProvider = Arc < dyn Fn ( ) -> Option < String > + Send + Sync > ;
6+
37/// Configuration for an electrum client
48///
59/// Refer to [`Client::from_config`] and [`ClientType::from_config`].
610///
711/// [`Client::from_config`]: crate::Client::from_config
812/// [`ClientType::from_config`]: crate::ClientType::from_config
9- #[ derive( Debug , Clone ) ]
13+ #[ derive( Clone ) ]
1014pub struct Config {
1115 /// Proxy socks5 configuration, default None
1216 socks5 : Option < Socks5Config > ,
@@ -16,6 +20,24 @@ pub struct Config {
1620 retry : u8 ,
1721 /// when ssl, validate the domain, default true
1822 validate_domain : bool ,
23+ /// Optional authorization provider for dynamic token injection
24+ authorization_provider : Option < AuthProvider > ,
25+ }
26+
27+ // Custom Debug impl because AuthProvider doesn't implement Debug
28+ impl std:: fmt:: Debug for Config {
29+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
30+ f. debug_struct ( "Config" )
31+ . field ( "socks5" , & self . socks5 )
32+ . field ( "timeout" , & self . timeout )
33+ . field ( "retry" , & self . retry )
34+ . field ( "validate_domain" , & self . validate_domain )
35+ . field (
36+ "authorization_provider" ,
37+ & self . authorization_provider . as_ref ( ) . map ( |_| "<provider>" ) ,
38+ )
39+ . finish ( )
40+ }
1941}
2042
2143/// Configuration for Socks5
@@ -72,6 +94,12 @@ impl ConfigBuilder {
7294 self
7395 }
7496
97+ /// Sets the authorization provider for dynamic token injection
98+ pub fn authorization_provider ( mut self , provider : Option < AuthProvider > ) -> Self {
99+ self . config . authorization_provider = provider;
100+ self
101+ }
102+
75103 /// Return the config and consume the builder
76104 pub fn build ( self ) -> Config {
77105 self . config
@@ -131,6 +159,13 @@ impl Config {
131159 self . validate_domain
132160 }
133161
162+ /// Get the configuration for `authorization_provider`
163+ ///
164+ /// Set this with [`ConfigBuilder::authorization_provider`]
165+ pub fn authorization_provider ( & self ) -> Option < & AuthProvider > {
166+ self . authorization_provider . as_ref ( )
167+ }
168+
134169 /// Convenience method for calling [`ConfigBuilder::new`]
135170 pub fn builder ( ) -> ConfigBuilder {
136171 ConfigBuilder :: new ( )
@@ -144,6 +179,106 @@ impl Default for Config {
144179 timeout : None ,
145180 retry : 1 ,
146181 validate_domain : true ,
182+ authorization_provider : None ,
183+ }
184+ }
185+ }
186+
187+ #[ cfg( test) ]
188+ mod tests {
189+ use super :: * ;
190+
191+ #[ test]
192+ fn test_authorization_provider_builder ( ) {
193+ let token = "test-token-123" . to_string ( ) ;
194+ let provider = Arc :: new ( move || Some ( format ! ( "Bearer {}" , token) ) ) ;
195+
196+ let config = ConfigBuilder :: new ( )
197+ . authorization_provider ( Some ( provider. clone ( ) ) )
198+ . build ( ) ;
199+
200+ assert ! ( config. authorization_provider( ) . is_some( ) ) ;
201+
202+ // Test that the provider returns the expected value
203+ if let Some ( auth_provider) = config. authorization_provider ( ) {
204+ assert_eq ! ( auth_provider( ) , Some ( "Bearer test-token-123" . to_string( ) ) ) ;
147205 }
148206 }
207+
208+ #[ test]
209+ fn test_authorization_provider_none ( ) {
210+ let config = ConfigBuilder :: new ( ) . build ( ) ;
211+
212+ assert ! ( config. authorization_provider( ) . is_none( ) ) ;
213+ }
214+
215+ #[ test]
216+ fn test_authorization_provider_returns_none ( ) {
217+ let provider = Arc :: new ( || None ) ;
218+
219+ let config = ConfigBuilder :: new ( )
220+ . authorization_provider ( Some ( provider) )
221+ . build ( ) ;
222+
223+ assert ! ( config. authorization_provider( ) . is_some( ) ) ;
224+
225+ // Test that the provider returns None
226+ if let Some ( auth_provider) = config. authorization_provider ( ) {
227+ assert_eq ! ( auth_provider( ) , None ) ;
228+ }
229+ }
230+
231+ #[ test]
232+ fn test_authorization_provider_dynamic_token ( ) {
233+ use std:: sync:: RwLock ;
234+
235+ // Simulate a token that can be updated
236+ let token = Arc :: new ( RwLock :: new ( "initial-token" . to_string ( ) ) ) ;
237+ let token_clone = token. clone ( ) ;
238+
239+ let provider = Arc :: new ( move || Some ( token_clone. read ( ) . unwrap ( ) . clone ( ) ) ) ;
240+
241+ let config = ConfigBuilder :: new ( )
242+ . authorization_provider ( Some ( provider. clone ( ) ) )
243+ . build ( ) ;
244+
245+ // Initial token
246+ if let Some ( auth_provider) = config. authorization_provider ( ) {
247+ assert_eq ! ( auth_provider( ) , Some ( "initial-token" . to_string( ) ) ) ;
248+ }
249+
250+ // Update the token
251+ * token. write ( ) . unwrap ( ) = "refreshed-token" . to_string ( ) ;
252+
253+ // Provider should return the new token
254+ if let Some ( auth_provider) = config. authorization_provider ( ) {
255+ assert_eq ! ( auth_provider( ) , Some ( "refreshed-token" . to_string( ) ) ) ;
256+ }
257+ }
258+
259+ #[ test]
260+ fn test_config_debug_with_provider ( ) {
261+ let provider = Arc :: new ( || Some ( "secret-token" . to_string ( ) ) ) ;
262+
263+ let config = ConfigBuilder :: new ( )
264+ . authorization_provider ( Some ( provider) )
265+ . build ( ) ;
266+
267+ let debug_str = format ! ( "{:?}" , config) ;
268+
269+ // Should show <provider> instead of the actual function pointer
270+ assert ! ( debug_str. contains( "<provider>" ) ) ;
271+ // Should not leak the token value
272+ assert ! ( !debug_str. contains( "secret-token" ) ) ;
273+ }
274+
275+ #[ test]
276+ fn test_config_debug_without_provider ( ) {
277+ let config = ConfigBuilder :: new ( ) . build ( ) ;
278+
279+ let debug_str = format ! ( "{:?}" , config) ;
280+
281+ // Should show None for authorization_provider
282+ assert ! ( debug_str. contains( "authorization_provider" ) ) ;
283+ }
149284}
0 commit comments