rustywallet_electrum/
discovery.rs1use std::net::{SocketAddr, ToSocketAddrs};
7use std::time::Duration;
8
9use crate::error::{ElectrumError, Result};
10use crate::types::ClientConfig;
11
12pub const DNS_SEEDS: &[&str] = &[
14 "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
24pub const TESTNET_DNS_SEEDS: &[&str] = &[
26 "electrum.blockstream.info",
27 "testnet.aranguren.org",
28];
29
30#[derive(Debug, Clone)]
32pub struct DiscoveredServer {
33 pub hostname: String,
35 pub addresses: Vec<SocketAddr>,
37 pub ssl_port: u16,
39 pub tcp_port: u16,
41 pub reachable: bool,
43 pub latency_ms: Option<u64>,
45}
46
47impl DiscoveredServer {
48 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 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 pub fn ssl_address(&self) -> String {
69 format!("{}:{}", self.hostname, self.ssl_port)
70 }
71
72 pub fn tcp_address(&self) -> String {
74 format!("{}:{}", self.hostname, self.tcp_port)
75 }
76
77 pub fn to_ssl_config(&self) -> ClientConfig {
79 ClientConfig::ssl(&self.hostname).with_port(self.ssl_port)
80 }
81
82 pub fn to_tcp_config(&self) -> ClientConfig {
84 ClientConfig::tcp(&self.hostname).with_port(self.tcp_port)
85 }
86}
87
88#[derive(Debug, Clone)]
90pub struct ServerDiscovery {
91 seeds: Vec<String>,
92 timeout: Duration,
93 prefer_ssl: bool,
94}
95
96impl ServerDiscovery {
97 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 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 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 pub fn with_timeout(mut self, timeout: Duration) -> Self {
126 self.timeout = timeout;
127 self
128 }
129
130 pub fn prefer_ssl(mut self, prefer: bool) -> Self {
132 self.prefer_ssl = prefer;
133 self
134 }
135
136 pub fn add_seed(&mut self, seed: impl Into<String>) {
138 self.seeds.push(seed.into());
139 }
140
141 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 if seed.contains("bluewallet") {
152 server = server.with_ports(443, 50001);
153 }
154
155 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 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 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 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 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 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
251pub 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
262pub 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}