rustywallet_electrum/
discovery.rs

1//! Server discovery via DNS seeds.
2//!
3//! This module provides functionality to discover Electrum servers
4//! using DNS seed queries, similar to how Bitcoin nodes discover peers.
5
6use std::net::{SocketAddr, ToSocketAddrs};
7use std::time::Duration;
8
9use crate::error::{ElectrumError, Result};
10use crate::types::ClientConfig;
11
12/// DNS seeds for discovering Electrum servers.
13pub const DNS_SEEDS: &[&str] = &[
14    // Bitcoin mainnet Electrum DNS seeds
15    "electrum.blockstream.info",
16    "electrum1.bluewallet.io",
17    "electrum2.bluewallet.io",
18    "bitcoin.aranguren.org",
19    "electrum.bitaroo.net",
20    "electrum.emzy.de",
21    "electrum.hodlister.co",
22];
23
24/// Testnet DNS seeds.
25pub const TESTNET_DNS_SEEDS: &[&str] = &[
26    "electrum.blockstream.info",
27    "testnet.aranguren.org",
28];
29
30/// Discovered server information.
31#[derive(Debug, Clone)]
32pub struct DiscoveredServer {
33    /// Server hostname
34    pub hostname: String,
35    /// Resolved IP addresses
36    pub addresses: Vec<SocketAddr>,
37    /// SSL port (typically 50002)
38    pub ssl_port: u16,
39    /// TCP port (typically 50001)
40    pub tcp_port: u16,
41    /// Whether the server is reachable
42    pub reachable: bool,
43    /// Response time in milliseconds (if tested)
44    pub latency_ms: Option<u64>,
45}
46
47impl DiscoveredServer {
48    /// Create a new discovered server entry.
49    pub fn new(hostname: impl Into<String>) -> Self {
50        Self {
51            hostname: hostname.into(),
52            addresses: Vec::new(),
53            ssl_port: 50002,
54            tcp_port: 50001,
55            reachable: false,
56            latency_ms: None,
57        }
58    }
59
60    /// Set custom ports.
61    pub fn with_ports(mut self, ssl: u16, tcp: u16) -> Self {
62        self.ssl_port = ssl;
63        self.tcp_port = tcp;
64        self
65    }
66
67    /// Get SSL address string.
68    pub fn ssl_address(&self) -> String {
69        format!("{}:{}", self.hostname, self.ssl_port)
70    }
71
72    /// Get TCP address string.
73    pub fn tcp_address(&self) -> String {
74        format!("{}:{}", self.hostname, self.tcp_port)
75    }
76
77    /// Convert to ClientConfig for SSL connection.
78    pub fn to_ssl_config(&self) -> ClientConfig {
79        ClientConfig::ssl(&self.hostname).with_port(self.ssl_port)
80    }
81
82    /// Convert to ClientConfig for TCP connection.
83    pub fn to_tcp_config(&self) -> ClientConfig {
84        ClientConfig::tcp(&self.hostname).with_port(self.tcp_port)
85    }
86}
87
88/// Server discovery service.
89#[derive(Debug, Clone)]
90pub struct ServerDiscovery {
91    seeds: Vec<String>,
92    timeout: Duration,
93    prefer_ssl: bool,
94}
95
96impl ServerDiscovery {
97    /// Create a new discovery service with default mainnet seeds.
98    pub fn new() -> Self {
99        Self {
100            seeds: DNS_SEEDS.iter().map(|s| s.to_string()).collect(),
101            timeout: Duration::from_secs(5),
102            prefer_ssl: true,
103        }
104    }
105
106    /// Create a discovery service for testnet.
107    pub fn testnet() -> Self {
108        Self {
109            seeds: TESTNET_DNS_SEEDS.iter().map(|s| s.to_string()).collect(),
110            timeout: Duration::from_secs(5),
111            prefer_ssl: true,
112        }
113    }
114
115    /// Create with custom seeds.
116    pub fn with_seeds(seeds: Vec<String>) -> Self {
117        Self {
118            seeds,
119            timeout: Duration::from_secs(5),
120            prefer_ssl: true,
121        }
122    }
123
124    /// Set discovery timeout.
125    pub fn with_timeout(mut self, timeout: Duration) -> Self {
126        self.timeout = timeout;
127        self
128    }
129
130    /// Set SSL preference.
131    pub fn prefer_ssl(mut self, prefer: bool) -> Self {
132        self.prefer_ssl = prefer;
133        self
134    }
135
136    /// Add a custom seed.
137    pub fn add_seed(&mut self, seed: impl Into<String>) {
138        self.seeds.push(seed.into());
139    }
140
141    /// Discover servers by resolving DNS seeds.
142    ///
143    /// Returns a list of discovered servers with resolved addresses.
144    pub fn discover(&self) -> Vec<DiscoveredServer> {
145        let mut servers = Vec::new();
146
147        for seed in &self.seeds {
148            let mut server = DiscoveredServer::new(seed);
149            
150            // Special handling for known servers with non-standard ports
151            if seed.contains("bluewallet") {
152                server = server.with_ports(443, 50001);
153            }
154
155            // Resolve DNS
156            let port = if self.prefer_ssl { server.ssl_port } else { server.tcp_port };
157            let addr_str = format!("{}:{}", seed, port);
158            
159            if let Ok(addrs) = addr_str.to_socket_addrs() {
160                server.addresses = addrs.collect();
161                server.reachable = !server.addresses.is_empty();
162            }
163
164            servers.push(server);
165        }
166
167        servers
168    }
169
170    /// Discover and test servers for connectivity.
171    ///
172    /// This performs actual TCP connections to verify reachability.
173    pub async fn discover_and_test(&self) -> Vec<DiscoveredServer> {
174        let mut servers = self.discover();
175
176        for server in &mut servers {
177            if server.addresses.is_empty() {
178                continue;
179            }
180
181            let port = if self.prefer_ssl { server.ssl_port } else { server.tcp_port };
182            let addr = format!("{}:{}", server.hostname, port);
183
184            let start = std::time::Instant::now();
185            
186            match tokio::time::timeout(
187                self.timeout,
188                tokio::net::TcpStream::connect(&addr),
189            )
190            .await
191            {
192                Ok(Ok(_)) => {
193                    server.reachable = true;
194                    server.latency_ms = Some(start.elapsed().as_millis() as u64);
195                }
196                _ => {
197                    server.reachable = false;
198                }
199            }
200        }
201
202        servers
203    }
204
205    /// Get the best server based on latency.
206    pub async fn best_server(&self) -> Result<DiscoveredServer> {
207        let servers = self.discover_and_test().await;
208        
209        servers
210            .into_iter()
211            .filter(|s| s.reachable)
212            .min_by_key(|s| s.latency_ms.unwrap_or(u64::MAX))
213            .ok_or_else(|| ElectrumError::ConnectionFailed("No reachable servers found".into()))
214    }
215
216    /// Get all reachable servers sorted by latency.
217    pub async fn reachable_servers(&self) -> Vec<DiscoveredServer> {
218        let mut servers = self.discover_and_test().await;
219        
220        servers.retain(|s| s.reachable);
221        servers.sort_by_key(|s| s.latency_ms.unwrap_or(u64::MAX));
222        
223        servers
224    }
225
226    /// Get a random reachable server.
227    pub async fn random_server(&self) -> Result<DiscoveredServer> {
228        use std::collections::hash_map::RandomState;
229        use std::hash::{BuildHasher, Hasher};
230
231        let servers = self.reachable_servers().await;
232        
233        if servers.is_empty() {
234            return Err(ElectrumError::ConnectionFailed("No reachable servers found".into()));
235        }
236
237        // Simple random selection using hash
238        let random = RandomState::new().build_hasher().finish() as usize;
239        let index = random % servers.len();
240        
241        Ok(servers.into_iter().nth(index).unwrap())
242    }
243}
244
245impl Default for ServerDiscovery {
246    fn default() -> Self {
247        Self::new()
248    }
249}
250
251/// Hardcoded server list for fallback.
252pub fn hardcoded_servers() -> Vec<DiscoveredServer> {
253    vec![
254        DiscoveredServer::new("electrum.blockstream.info").with_ports(50002, 50001),
255        DiscoveredServer::new("electrum1.bluewallet.io").with_ports(443, 50001),
256        DiscoveredServer::new("electrum2.bluewallet.io").with_ports(443, 50001),
257        DiscoveredServer::new("bitcoin.aranguren.org").with_ports(50002, 50001),
258        DiscoveredServer::new("electrum.bitaroo.net").with_ports(50002, 50001),
259    ]
260}
261
262/// Get a default server configuration.
263pub fn default_server() -> ClientConfig {
264    ClientConfig::ssl("electrum.blockstream.info")
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn test_discovered_server() {
273        let server = DiscoveredServer::new("example.com");
274        assert_eq!(server.ssl_address(), "example.com:50002");
275        assert_eq!(server.tcp_address(), "example.com:50001");
276    }
277
278    #[test]
279    fn test_custom_ports() {
280        let server = DiscoveredServer::new("example.com").with_ports(443, 80);
281        assert_eq!(server.ssl_address(), "example.com:443");
282        assert_eq!(server.tcp_address(), "example.com:80");
283    }
284
285    #[test]
286    fn test_discovery_seeds() {
287        let discovery = ServerDiscovery::new();
288        assert!(!discovery.seeds.is_empty());
289    }
290
291    #[test]
292    fn test_hardcoded_servers() {
293        let servers = hardcoded_servers();
294        assert!(!servers.is_empty());
295        assert!(servers.iter().any(|s| s.hostname.contains("blockstream")));
296    }
297}