use std::net::{SocketAddr, ToSocketAddrs};
use std::time::Duration;
use crate::error::{ElectrumError, Result};
use crate::types::ClientConfig;
pub const DNS_SEEDS: &[&str] = &[
"electrum.blockstream.info",
"electrum1.bluewallet.io",
"electrum2.bluewallet.io",
"bitcoin.aranguren.org",
"electrum.bitaroo.net",
"electrum.emzy.de",
"electrum.hodlister.co",
];
pub const TESTNET_DNS_SEEDS: &[&str] = &[
"electrum.blockstream.info",
"testnet.aranguren.org",
];
#[derive(Debug, Clone)]
pub struct DiscoveredServer {
pub hostname: String,
pub addresses: Vec<SocketAddr>,
pub ssl_port: u16,
pub tcp_port: u16,
pub reachable: bool,
pub latency_ms: Option<u64>,
}
impl DiscoveredServer {
pub fn new(hostname: impl Into<String>) -> Self {
Self {
hostname: hostname.into(),
addresses: Vec::new(),
ssl_port: 50002,
tcp_port: 50001,
reachable: false,
latency_ms: None,
}
}
pub fn with_ports(mut self, ssl: u16, tcp: u16) -> Self {
self.ssl_port = ssl;
self.tcp_port = tcp;
self
}
pub fn ssl_address(&self) -> String {
format!("{}:{}", self.hostname, self.ssl_port)
}
pub fn tcp_address(&self) -> String {
format!("{}:{}", self.hostname, self.tcp_port)
}
pub fn to_ssl_config(&self) -> ClientConfig {
ClientConfig::ssl(&self.hostname).with_port(self.ssl_port)
}
pub fn to_tcp_config(&self) -> ClientConfig {
ClientConfig::tcp(&self.hostname).with_port(self.tcp_port)
}
}
#[derive(Debug, Clone)]
pub struct ServerDiscovery {
seeds: Vec<String>,
timeout: Duration,
prefer_ssl: bool,
}
impl ServerDiscovery {
pub fn new() -> Self {
Self {
seeds: DNS_SEEDS.iter().map(|s| s.to_string()).collect(),
timeout: Duration::from_secs(5),
prefer_ssl: true,
}
}
pub fn testnet() -> Self {
Self {
seeds: TESTNET_DNS_SEEDS.iter().map(|s| s.to_string()).collect(),
timeout: Duration::from_secs(5),
prefer_ssl: true,
}
}
pub fn with_seeds(seeds: Vec<String>) -> Self {
Self {
seeds,
timeout: Duration::from_secs(5),
prefer_ssl: true,
}
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn prefer_ssl(mut self, prefer: bool) -> Self {
self.prefer_ssl = prefer;
self
}
pub fn add_seed(&mut self, seed: impl Into<String>) {
self.seeds.push(seed.into());
}
pub fn discover(&self) -> Vec<DiscoveredServer> {
let mut servers = Vec::new();
for seed in &self.seeds {
let mut server = DiscoveredServer::new(seed);
if seed.contains("bluewallet") {
server = server.with_ports(443, 50001);
}
let port = if self.prefer_ssl { server.ssl_port } else { server.tcp_port };
let addr_str = format!("{}:{}", seed, port);
if let Ok(addrs) = addr_str.to_socket_addrs() {
server.addresses = addrs.collect();
server.reachable = !server.addresses.is_empty();
}
servers.push(server);
}
servers
}
pub async fn discover_and_test(&self) -> Vec<DiscoveredServer> {
let mut servers = self.discover();
for server in &mut servers {
if server.addresses.is_empty() {
continue;
}
let port = if self.prefer_ssl { server.ssl_port } else { server.tcp_port };
let addr = format!("{}:{}", server.hostname, port);
let start = std::time::Instant::now();
match tokio::time::timeout(
self.timeout,
tokio::net::TcpStream::connect(&addr),
)
.await
{
Ok(Ok(_)) => {
server.reachable = true;
server.latency_ms = Some(start.elapsed().as_millis() as u64);
}
_ => {
server.reachable = false;
}
}
}
servers
}
pub async fn best_server(&self) -> Result<DiscoveredServer> {
let servers = self.discover_and_test().await;
servers
.into_iter()
.filter(|s| s.reachable)
.min_by_key(|s| s.latency_ms.unwrap_or(u64::MAX))
.ok_or_else(|| ElectrumError::ConnectionFailed("No reachable servers found".into()))
}
pub async fn reachable_servers(&self) -> Vec<DiscoveredServer> {
let mut servers = self.discover_and_test().await;
servers.retain(|s| s.reachable);
servers.sort_by_key(|s| s.latency_ms.unwrap_or(u64::MAX));
servers
}
pub async fn random_server(&self) -> Result<DiscoveredServer> {
use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hasher};
let servers = self.reachable_servers().await;
if servers.is_empty() {
return Err(ElectrumError::ConnectionFailed("No reachable servers found".into()));
}
let random = RandomState::new().build_hasher().finish() as usize;
let index = random % servers.len();
Ok(servers.into_iter().nth(index).unwrap())
}
}
impl Default for ServerDiscovery {
fn default() -> Self {
Self::new()
}
}
pub fn hardcoded_servers() -> Vec<DiscoveredServer> {
vec![
DiscoveredServer::new("electrum.blockstream.info").with_ports(50002, 50001),
DiscoveredServer::new("electrum1.bluewallet.io").with_ports(443, 50001),
DiscoveredServer::new("electrum2.bluewallet.io").with_ports(443, 50001),
DiscoveredServer::new("bitcoin.aranguren.org").with_ports(50002, 50001),
DiscoveredServer::new("electrum.bitaroo.net").with_ports(50002, 50001),
]
}
pub fn default_server() -> ClientConfig {
ClientConfig::ssl("electrum.blockstream.info")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_discovered_server() {
let server = DiscoveredServer::new("example.com");
assert_eq!(server.ssl_address(), "example.com:50002");
assert_eq!(server.tcp_address(), "example.com:50001");
}
#[test]
fn test_custom_ports() {
let server = DiscoveredServer::new("example.com").with_ports(443, 80);
assert_eq!(server.ssl_address(), "example.com:443");
assert_eq!(server.tcp_address(), "example.com:80");
}
#[test]
fn test_discovery_seeds() {
let discovery = ServerDiscovery::new();
assert!(!discovery.seeds.is_empty());
}
#[test]
fn test_hardcoded_servers() {
let servers = hardcoded_servers();
assert!(!servers.is_empty());
assert!(servers.iter().any(|s| s.hostname.contains("blockstream")));
}
}