From ca0f266e18d3c505cd4eddbf80f8194588aced8b Mon Sep 17 00:00:00 2001 From: Edward Houston Date: Fri, 12 Dec 2025 14:52:46 +0100 Subject: [PATCH 1/3] feat: add dynamic JWT authorization support via callback provider --- examples/JWT_AUTH_EXAMPLE.md | 224 +++++++++++++++++++++++++++++++++++ examples/jwt_auth.rs | 34 ++++++ src/client.rs | 17 +-- src/config.rs | 137 ++++++++++++++++++++- src/raw_client.rs | 130 +++++++++++++++++++- src/types.rs | 70 +++++++++++ 6 files changed, 601 insertions(+), 11 deletions(-) create mode 100644 examples/JWT_AUTH_EXAMPLE.md create mode 100644 examples/jwt_auth.rs diff --git a/examples/JWT_AUTH_EXAMPLE.md b/examples/JWT_AUTH_EXAMPLE.md new file mode 100644 index 0000000..66438f3 --- /dev/null +++ b/examples/JWT_AUTH_EXAMPLE.md @@ -0,0 +1,224 @@ +# JWT Authentication with Electrum Client + +This guide demonstrates how to use dynamic JWT authentication with the electrum-client library. + +## Overview + +The electrum-client now supports embedding authorization tokens (such as JWT Bearer tokens) directly in JSON-RPC requests. This is achieved through an `AuthProvider` callback that is invoked before each request. + +## Basic Usage + +```rust +use electrum_client::{Client, ConfigBuilder}; +use std::sync::{Arc, RwLock}; + +// Simple example: Static token +fn main() -> Result<(), Box> { + let token = "your-jwt-token-here".to_string(); + + let config = ConfigBuilder::new() + .authorization_provider(Some(Arc::new(move || { + Some(format!("Bearer {}", token)) + }))) + .build(); + + let client = Client::from_config("tcp://your-server:50001", config)?; + + // All RPC calls will now include: "authorization": "Bearer your-jwt-token-here" + let features = client.server_features()?; + println!("{:?}", features); + + Ok(()) +} +``` + +## Advanced: Token Refresh with Keycloak + +This example demonstrates automatic token refresh every 4 minutes (before the 5-minute expiration). + +```rust +use electrum_client::{Client, ConfigBuilder, ElectrumApi}; +use std::sync::{Arc, RwLock}; +use std::time::Duration; +use tokio::time::sleep; + +/// Manages JWT tokens from Keycloak with automatic refresh +struct KeycloakTokenManager { + token: Arc>>, + keycloak_url: String, + client_id: String, + client_secret: String, +} + +impl KeycloakTokenManager { + fn new(keycloak_url: String, client_id: String, client_secret: String) -> Self { + Self { + token: Arc::new(RwLock::new(None)), + keycloak_url, + client_id, + client_secret, + } + } + + /// Get the current token (for the auth provider) + fn get_token(&self) -> Option { + self.token.read().unwrap().clone() + } + + /// Fetch a fresh token from Keycloak + async fn fetch_token(&self) -> Result> { + // Example using reqwest to get JWT from Keycloak + let client = reqwest::Client::new(); + let response = client + .post(&format!("{}/protocol/openid-connect/token", self.keycloak_url)) + .form(&[ + ("grant_type", "client_credentials"), + ("client_id", &self.client_id), + ("client_secret", &self.client_secret), + ]) + .send() + .await?; + + let json: serde_json::Value = response.json().await?; + let access_token = json["access_token"] + .as_str() + .ok_or("Missing access_token")? + .to_string(); + + Ok(format!("Bearer {}", access_token)) + } + + /// Background task that refreshes the token every 4 minutes + async fn refresh_loop(self: Arc) { + loop { + // Refresh every 4 minutes (tokens expire at 5 minutes) + sleep(Duration::from_secs(240)).await; + + match self.fetch_token().await { + Ok(new_token) => { + println!("Token refreshed successfully"); + *self.token.write().unwrap() = Some(new_token); + } + Err(e) => { + eprintln!("Failed to refresh token: {}", e); + // Keep using old token until we can refresh + } + } + } + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Setup token manager + let token_manager = Arc::new(KeycloakTokenManager::new( + "https://your-keycloak-server/auth/realms/your-realm".to_string(), + "your-client-id".to_string(), + "your-client-secret".to_string(), + )); + + // Fetch initial token + let initial_token = token_manager.fetch_token().await?; + *token_manager.token.write().unwrap() = Some(initial_token); + + // Start background refresh task + let tm_clone = token_manager.clone(); + tokio::spawn(async move { + tm_clone.refresh_loop().await; + }); + + // Create Electrum client with dynamic auth provider + let tm_for_provider = token_manager.clone(); + let config = ConfigBuilder::new() + .authorization_provider(Some(Arc::new(move || { + tm_for_provider.get_token() + }))) + .build(); + + let client = Client::from_config("tcp://your-api-gateway:50001", config)?; + + // All RPC calls will automatically include fresh JWT tokens + loop { + match client.server_features() { + Ok(features) => println!("Connected: {:?}", features), + Err(e) => eprintln!("Error: {}", e), + } + + tokio::time::sleep(Duration::from_secs(10)).await; + } +} +``` + +## Integration with BDK + +### Using BDK's Convenience Constructor (Recommended) + +```rust +use bdk_electrum::{BdkElectrumClient, ConfigBuilder}; +use std::sync::Arc; + +// Assuming you have a TokenManager as shown above +fn create_bdk_client( + token_manager: Arc +) -> Result, Box> { + + let config = ConfigBuilder::new() + .authorization_provider(Some(Arc::new(move || { + token_manager.get_token() + }))) + .build(); + + // BDK provides a from_config() convenience method + let bdk_client = BdkElectrumClient::from_config( + "tcp://your-api-gateway:50001", + config + )?; + + Ok(bdk_client) +} +``` + +### Alternative: Manual Construction + +If you need more control over the underlying client: + +```rust +use bdk_electrum::BdkElectrumClient; +use electrum_client::{Client, ConfigBuilder}; +use std::sync::Arc; + +let config = ConfigBuilder::new() + .authorization_provider(Some(Arc::new(move || { + token_manager.get_token() + }))) + .timeout(Some(30)) + .build(); + +let electrum_client = Client::from_config("tcp://your-api-gateway:50001", config)?; +let bdk_client = BdkElectrumClient::new(electrum_client); +``` + +## JSON-RPC Request Format + +With the auth provider configured, each JSON-RPC request will include the authorization field: + +```json +{ + "jsonrpc": "2.0", + "method": "blockchain.headers.subscribe", + "params": [], + "id": 1, + "authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." +} +``` + +If the provider returns `None`, the authorization field is omitted from the request. + +## Thread Safety + +The `AuthProvider` type is defined as: +```rust +pub type AuthProvider = Arc Option + Send + Sync>; +``` + +This ensures thread-safe access to tokens across all RPC calls. diff --git a/examples/jwt_auth.rs b/examples/jwt_auth.rs new file mode 100644 index 0000000..662032a --- /dev/null +++ b/examples/jwt_auth.rs @@ -0,0 +1,34 @@ +extern crate electrum_client; + +use electrum_client::{Client, ConfigBuilder, ElectrumApi}; +use std::sync::Arc; + +fn main() { + // Example 1: Static JWT token + println!("Example 1: Static JWT token"); + + let config = ConfigBuilder::new() + .authorization_provider(Some(Arc::new(|| { + // In production, fetch this from your token manager + Some("Bearer example-jwt-token-12345".to_string()) + }))) + .build(); + + match Client::from_config("tcp://localhost:50001", config) { + Ok(client) => { + println!("Connected to server with JWT auth"); + match client.server_features() { + Ok(features) => println!("Server features: {:#?}", features), + Err(e) => eprintln!("Error fetching features: {}", e), + } + } + Err(e) => { + eprintln!("Connection error: {}", e); + eprintln!("\nNote: This example requires an Electrum server that accepts JWT auth."); + eprintln!("Update the URL and token to match your setup."); + } + } + + // Example 2: Dynamic token with refresh + // See JWT_AUTH_EXAMPLE.md for complete implementation with automatic token refresh +} diff --git a/src/client.rs b/src/client.rs index 7e4e626..53a138c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -120,19 +120,22 @@ impl ClientType { None => { RawClient::new_ssl(url.as_str(), config.validate_domain(), config.timeout())? } - }; + } + .with_auth_provider(config.authorization_provider().clone()); Ok(ClientType::SSL(client)) } else { let url = url.replacen("tcp://", "", 1); Ok(match config.socks5().as_ref() { - None => ClientType::TCP(RawClient::new(url.as_str(), config.timeout())?), - Some(socks5) => ClientType::Socks5(RawClient::new_proxy( - url.as_str(), - socks5, - config.timeout(), - )?), + None => ClientType::TCP( + RawClient::new(url.as_str(), config.timeout())? + .with_auth_provider(config.authorization_provider().clone()), + ), + Some(socks5) => ClientType::Socks5( + RawClient::new_proxy(url.as_str(), socks5, config.timeout())? + .with_auth_provider(config.authorization_provider().clone()), + ), }) } } diff --git a/src/config.rs b/src/config.rs index e4c5770..d644d3d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,12 +1,16 @@ +use std::sync::Arc; use std::time::Duration; +/// A function that provides authorization tokens dynamically (e.g., for JWT refresh) +pub type AuthProvider = Arc Option + Send + Sync>; + /// Configuration for an electrum client /// /// Refer to [`Client::from_config`] and [`ClientType::from_config`]. /// /// [`Client::from_config`]: crate::Client::from_config /// [`ClientType::from_config`]: crate::ClientType::from_config -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct Config { /// Proxy socks5 configuration, default None socks5: Option, @@ -16,6 +20,24 @@ pub struct Config { retry: u8, /// when ssl, validate the domain, default true validate_domain: bool, + /// Optional authorization provider for dynamic token injection + authorization_provider: Option, +} + +// Custom Debug impl because AuthProvider doesn't implement Debug +impl std::fmt::Debug for Config { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Config") + .field("socks5", &self.socks5) + .field("timeout", &self.timeout) + .field("retry", &self.retry) + .field("validate_domain", &self.validate_domain) + .field( + "authorization_provider", + &self.authorization_provider.as_ref().map(|_| ""), + ) + .finish() + } } /// Configuration for Socks5 @@ -72,6 +94,12 @@ impl ConfigBuilder { self } + /// Sets the authorization provider for dynamic token injection + pub fn authorization_provider(mut self, provider: Option) -> Self { + self.config.authorization_provider = provider; + self + } + /// Return the config and consume the builder pub fn build(self) -> Config { self.config @@ -131,6 +159,13 @@ impl Config { self.validate_domain } + /// Get the configuration for `authorization_provider` + /// + /// Set this with [`ConfigBuilder::authorization_provider`] + pub fn authorization_provider(&self) -> &Option { + &self.authorization_provider + } + /// Convenience method for calling [`ConfigBuilder::new`] pub fn builder() -> ConfigBuilder { ConfigBuilder::new() @@ -144,6 +179,106 @@ impl Default for Config { timeout: None, retry: 1, validate_domain: true, + authorization_provider: None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_authorization_provider_builder() { + let token = "test-token-123".to_string(); + let provider = Arc::new(move || Some(format!("Bearer {}", token))); + + let config = ConfigBuilder::new() + .authorization_provider(Some(provider.clone())) + .build(); + + assert!(config.authorization_provider().is_some()); + + // Test that the provider returns the expected value + if let Some(auth_provider) = config.authorization_provider() { + assert_eq!(auth_provider(), Some("Bearer test-token-123".to_string())); } } + + #[test] + fn test_authorization_provider_none() { + let config = ConfigBuilder::new().build(); + + assert!(config.authorization_provider().is_none()); + } + + #[test] + fn test_authorization_provider_returns_none() { + let provider = Arc::new(|| None); + + let config = ConfigBuilder::new() + .authorization_provider(Some(provider)) + .build(); + + assert!(config.authorization_provider().is_some()); + + // Test that the provider returns None + if let Some(auth_provider) = config.authorization_provider() { + assert_eq!(auth_provider(), None); + } + } + + #[test] + fn test_authorization_provider_dynamic_token() { + use std::sync::RwLock; + + // Simulate a token that can be updated + let token = Arc::new(RwLock::new("initial-token".to_string())); + let token_clone = token.clone(); + + let provider = Arc::new(move || Some(token_clone.read().unwrap().clone())); + + let config = ConfigBuilder::new() + .authorization_provider(Some(provider.clone())) + .build(); + + // Initial token + if let Some(auth_provider) = config.authorization_provider() { + assert_eq!(auth_provider(), Some("initial-token".to_string())); + } + + // Update the token + *token.write().unwrap() = "refreshed-token".to_string(); + + // Provider should return the new token + if let Some(auth_provider) = config.authorization_provider() { + assert_eq!(auth_provider(), Some("refreshed-token".to_string())); + } + } + + #[test] + fn test_config_debug_with_provider() { + let provider = Arc::new(|| Some("secret-token".to_string())); + + let config = ConfigBuilder::new() + .authorization_provider(Some(provider)) + .build(); + + let debug_str = format!("{:?}", config); + + // Should show instead of the actual function pointer + assert!(debug_str.contains("")); + // Should not leak the token value + assert!(!debug_str.contains("secret-token")); + } + + #[test] + fn test_config_debug_without_provider() { + let config = ConfigBuilder::new().build(); + + let debug_str = format!("{:?}", config); + + // Should show None for authorization_provider + assert!(debug_str.contains("authorization_provider")); + } } diff --git a/src/raw_client.rs b/src/raw_client.rs index 5d67467..a6e8816 100644 --- a/src/raw_client.rs +++ b/src/raw_client.rs @@ -43,6 +43,7 @@ use crate::stream::ClonableStream; use crate::api::ElectrumApi; use crate::batch::Batch; +use crate::config::AuthProvider; use crate::types::*; macro_rules! impl_batch_call { @@ -128,7 +129,6 @@ impl_to_socket_addrs_domain!((std::net::Ipv6Addr, u16)); /// /// More transport methods can be used by manually creating an instance of this struct with an /// arbitray `S` type. -#[derive(Debug)] pub struct RawClient where S: Read + Write, @@ -142,10 +142,33 @@ where headers: Mutex>, script_notifications: Mutex>>, + auth_provider: Option, + #[cfg(feature = "debug-calls")] calls: AtomicUsize, } +// Custom Debug impl because AuthProvider doesn't implement Debug +impl std::fmt::Debug for RawClient +where + S: Read + Write, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RawClient") + .field("stream", &"") + .field("buf_reader", &"") + .field("last_id", &self.last_id) + .field("waiting_map", &self.waiting_map) + .field("headers", &self.headers) + .field("script_notifications", &self.script_notifications) + .field( + "auth_provider", + &self.auth_provider.as_ref().map(|_| ""), + ) + .finish() + } +} + impl From for RawClient where S: Read + Write, @@ -163,6 +186,8 @@ where headers: Mutex::new(VecDeque::new()), script_notifications: Mutex::new(HashMap::new()), + auth_provider: None, + #[cfg(feature = "debug-calls")] calls: AtomicUsize::new(0), } @@ -540,6 +565,13 @@ enum ChannelMessage { } impl RawClient { + /// Sets the authorization provider for dynamic token injection. + /// This should be called immediately after constructing the client. + pub fn with_auth_provider(mut self, provider: Option) -> Self { + self.auth_provider = provider; + self + } + // TODO: to enable this we have to find a way to allow concurrent read and writes to the // underlying transport struct. This can be done pretty easily for TcpStream because it can be // split into a "read" and a "write" object, but it's not as trivial for other types. Without @@ -667,6 +699,12 @@ impl RawClient { let (sender, receiver) = channel(); self.waiting_map.lock()?.insert(req.id, sender); + // Apply authorization token if provider is set + let mut req = req; + if let Some(provider) = &self.auth_provider { + req.authorization = provider(); + } + let mut raw = serde_json::to_vec(&req)?; trace!("==> {}", String::from_utf8_lossy(&raw)); @@ -789,12 +827,20 @@ impl ElectrumApi for RawClient { // Add our listener to the map before we send the request - for (method, params) in batch.iter() { - let req = Request::new_id( + for (index, (method, params)) in batch.iter().enumerate() { + let mut req = Request::new_id( self.last_id.fetch_add(1, Ordering::SeqCst), method, params.to_vec(), ); + + // Apply authorization token only to the first request in batch + if index == 0 { + if let Some(provider) = &self.auth_provider { + req.authorization = provider(); + } + } + // Add distinct channel to each request so when we remove our request id (and sender) from the waiting_map // we can be sure that the response gets sent to the correct channel in self.recv let (sender, receiver) = channel(); @@ -1675,4 +1721,82 @@ mod test { 00000" ) } + + #[test] + fn test_authorization_provider_with_client() { + use std::sync::{Arc, RwLock}; + + // Track how many times the provider is called + let call_count = Arc::new(RwLock::new(0)); + let call_count_clone = call_count.clone(); + + let provider = Arc::new(move || { + *call_count_clone.write().unwrap() += 1; + Some("Bearer test-token-123".to_string()) + }); + + let mut client = RawClient::new(get_test_server(), None).unwrap(); + client = client.with_auth_provider(Some(provider)); + + // Make a request - provider should be called + let _ = client.server_features(); + + // Provider should have been called at least once + assert!(*call_count.read().unwrap() >= 1); + } + + #[test] + fn test_authorization_provider_dynamic_token_refresh() { + use std::sync::{Arc, RwLock}; + + // Simulate a token that can be refreshed + let token = Arc::new(RwLock::new("initial-token".to_string())); + let token_clone = token.clone(); + + let provider = Arc::new(move || Some(token_clone.read().unwrap().clone())); + + let mut client = RawClient::new(get_test_server(), None).unwrap(); + client = client.with_auth_provider(Some(provider.clone())); + + // Make first request with initial token + let _ = client.server_features(); + + // Simulate token refresh + *token.write().unwrap() = "refreshed-token".to_string(); + + // Make second request - should use the new token + let _ = client.server_features(); + + // Verify the provider now returns the refreshed token + assert_eq!(provider(), Some("refreshed-token".to_string())); + } + + #[test] + fn test_authorization_provider_returns_none() { + use std::sync::Arc; + + let provider = Arc::new(|| None); + + let mut client = RawClient::new(get_test_server(), None).unwrap(); + client = client.with_auth_provider(Some(provider)); + + // Should still work when provider returns None + let result = client.server_features(); + assert!(result.is_ok()); + } + + #[test] + fn test_with_auth_provider_method_chaining() { + use std::sync::Arc; + + let provider = Arc::new(|| Some("Bearer test".to_string())); + + let client = RawClient::new(get_test_server(), None) + .unwrap() + .with_auth_provider(Some(provider.clone())); + + // Verify the provider was set + let result = client.server_features(); + assert!(result.is_ok()); + } } diff --git a/src/types.rs b/src/types.rs index ce3ef9f..c4f1c6d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -46,6 +46,9 @@ pub struct Request<'a> { pub method: &'a str, /// The request parameters pub params: Vec, + /// Optional authorization token (e.g., JWT Bearer token) + #[serde(skip_serializing_if = "Option::is_none")] + pub authorization: Option, } impl<'a> Request<'a> { @@ -56,6 +59,7 @@ impl<'a> Request<'a> { jsonrpc: JSONRPC_2_0, method, params, + authorization: None, } } @@ -419,6 +423,8 @@ impl From for Error { mod tests { use crate::ScriptStatus; + use super::{Param, Request}; + #[test] fn script_status_roundtrip() { let script_status: ScriptStatus = [1u8; 32].into(); @@ -426,4 +432,68 @@ mod tests { let script_status_back = serde_json::from_str(&script_status_json).unwrap(); assert_eq!(script_status, script_status_back); } + + #[test] + fn test_request_serialization_without_authorization() { + let req = Request::new_id(1, "server.version", vec![]); + + let json = serde_json::to_string(&req).unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); + + // Authorization field should not be present when None + assert!(parsed.get("authorization").is_none()); + assert_eq!(parsed["jsonrpc"], "2.0"); + assert_eq!(parsed["method"], "server.version"); + assert_eq!(parsed["id"], 1); + } + + #[test] + fn test_request_serialization_with_authorization() { + let mut req = Request::new_id(1, "server.version", vec![]); + req.authorization = Some("Bearer test-jwt-token".to_string()); + + let json = serde_json::to_string(&req).unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); + + // Authorization field should be present + assert_eq!( + parsed["authorization"], + serde_json::Value::String("Bearer test-jwt-token".to_string()) + ); + assert_eq!(parsed["jsonrpc"], "2.0"); + assert_eq!(parsed["method"], "server.version"); + assert_eq!(parsed["id"], 1); + } + + #[test] + fn test_request_with_params_and_authorization() { + let mut req = Request::new_id( + 42, + "blockchain.scripthash.get_balance", + vec![Param::String("test-scripthash".to_string())], + ); + req.authorization = Some("Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9".to_string()); + + let json = serde_json::to_string(&req).unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); + + assert_eq!(parsed["id"], 42); + assert_eq!(parsed["method"], "blockchain.scripthash.get_balance"); + assert_eq!( + parsed["authorization"], + "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9" + ); + assert!(parsed["params"].is_array()); + assert_eq!(parsed["params"][0], "test-scripthash"); + } + + #[test] + fn test_authorization_field_omitted_when_none() { + let req = Request::new_id(1, "test.method", vec![]); + + let json = serde_json::to_string(&req).unwrap(); + + // The JSON should not contain the word "authorization" at all + assert!(!json.contains("authorization")); + } } From 0d4206d907b488fc50a01d50b89e62d34f1b1102 Mon Sep 17 00:00:00 2001 From: Edward Houston Date: Mon, 19 Jan 2026 17:02:03 +0100 Subject: [PATCH 2/3] Remove reference to convenience method in bdk --- examples/JWT_AUTH_EXAMPLE.md | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/examples/JWT_AUTH_EXAMPLE.md b/examples/JWT_AUTH_EXAMPLE.md index 66438f3..d84cd60 100644 --- a/examples/JWT_AUTH_EXAMPLE.md +++ b/examples/JWT_AUTH_EXAMPLE.md @@ -151,36 +151,7 @@ async fn main() -> Result<(), Box> { ## Integration with BDK -### Using BDK's Convenience Constructor (Recommended) - -```rust -use bdk_electrum::{BdkElectrumClient, ConfigBuilder}; -use std::sync::Arc; - -// Assuming you have a TokenManager as shown above -fn create_bdk_client( - token_manager: Arc -) -> Result, Box> { - - let config = ConfigBuilder::new() - .authorization_provider(Some(Arc::new(move || { - token_manager.get_token() - }))) - .build(); - - // BDK provides a from_config() convenience method - let bdk_client = BdkElectrumClient::from_config( - "tcp://your-api-gateway:50001", - config - )?; - - Ok(bdk_client) -} -``` - -### Alternative: Manual Construction - -If you need more control over the underlying client: +To use with BDK, create the electrum client with your config, then wrap it: ```rust use bdk_electrum::BdkElectrumClient; From db944917a3b41519d298c9ae14d56c287fcc4f54 Mon Sep 17 00:00:00 2001 From: Edward Houston Date: Mon, 19 Jan 2026 17:27:27 +0100 Subject: [PATCH 3/3] Fix timeout call in example doc --- examples/JWT_AUTH_EXAMPLE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/JWT_AUTH_EXAMPLE.md b/examples/JWT_AUTH_EXAMPLE.md index d84cd60..ad6575a 100644 --- a/examples/JWT_AUTH_EXAMPLE.md +++ b/examples/JWT_AUTH_EXAMPLE.md @@ -157,12 +157,13 @@ To use with BDK, create the electrum client with your config, then wrap it: use bdk_electrum::BdkElectrumClient; use electrum_client::{Client, ConfigBuilder}; use std::sync::Arc; +use std::time::Duration; let config = ConfigBuilder::new() .authorization_provider(Some(Arc::new(move || { token_manager.get_token() }))) - .timeout(Some(30)) + .timeout(Some(Duration::from_secs(30))) .build(); let electrum_client = Client::from_config("tcp://your-api-gateway:50001", config)?;