Skip to content

Commit fb86d00

Browse files
authored
feat: tweaks for local network (#8)
* feat: support regtest * fix: memebers * feat: tweaks for local network * refactor: remove redundant Network enum implementation
1 parent 6fef18c commit fb86d00

File tree

5 files changed

+141
-64
lines changed

5 files changed

+141
-64
lines changed

src/api.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,10 +220,20 @@ async fn get_previous_quorums(
220220

221221
#[axum::debug_handler]
222222
async fn get_masternodes(
223-
State((_, _, masternode_cache)): State<(SharedQuorumList, SharedConfig, SharedMasternodeCache)>,
223+
State((_, config, masternode_cache)): State<(SharedQuorumList, SharedConfig, SharedMasternodeCache)>,
224224
) -> Result<Json<ApiResponse<EvoMasternodeList>>, StatusCode> {
225225
match masternode_cache.get_masternodes().await {
226-
Ok(masternodes) => Ok(Json(ApiResponse::success(masternodes))),
226+
Ok(masternodes) => {
227+
// Apply address host override if configured
228+
let masternodes: EvoMasternodeList = masternodes
229+
.into_iter()
230+
.map(|mut m| {
231+
m.address = config.apply_address_host_override(&m.address);
232+
m
233+
})
234+
.collect();
235+
Ok(Json(ApiResponse::success(masternodes)))
236+
}
227237
Err(e) => Ok(Json(ApiResponse::error(format!("Failed to load masternodes: {}", e))))
228238
}
229239
}

src/config.rs

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,17 @@ pub struct Config {
7777

7878
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
7979
pub struct DockerConfig {
80-
/// Replace 127.0.0.1 in masternode addresses with this host.
81-
/// Useful when running in Docker to reach host services.
80+
/// Use this host for version checks instead of the masternode's reported address.
81+
/// Useful when running in Docker where the reported IPs are not reachable.
8282
/// Example: "host.docker.internal" for Docker Desktop
8383
#[serde(default)]
84-
pub localhost_replacement: Option<String>,
84+
pub version_check_host: Option<String>,
85+
86+
/// Replace the host in masternode addresses returned by the /masternodes endpoint.
87+
/// Useful when clients need to connect via a different host than what's reported.
88+
/// Example: "127.0.0.1" for local testing
89+
#[serde(default)]
90+
pub address_host_override: Option<String>,
8591
}
8692

8793
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -141,6 +147,7 @@ impl Config {
141147
// Fall back to environment variables or defaults
142148
let mut config = Config::default();
143149

150+
144151
if let Ok(port) = std::env::var("API_PORT") {
145152
if let Ok(port_num) = port.parse::<u16>() {
146153
config.server.port = port_num;
@@ -174,8 +181,12 @@ impl Config {
174181
Network::try_from(network_str.as_str()).unwrap_or_else(|e| panic!("{}", e));
175182
}
176183

177-
if let Ok(localhost_replacement) = std::env::var("LOCALHOST_REPLACEMENT") {
178-
config.docker.localhost_replacement = Some(localhost_replacement);
184+
if let Ok(version_check_host) = std::env::var("VERSION_CHECK_HOST") {
185+
config.docker.version_check_host = Some(version_check_host);
186+
}
187+
188+
if let Ok(address_host_override) = std::env::var("ADDRESS_HOST_OVERRIDE") {
189+
config.docker.address_host_override = Some(address_host_override);
179190
}
180191

181192
config
@@ -202,13 +213,35 @@ impl Config {
202213
self.network.dapi_port()
203214
}
204215

205-
/// Replace 127.0.0.1 in an address with the configured replacement host.
206-
/// Returns the original address if no replacement is configured.
207-
pub fn replace_localhost(&self, address: &str) -> String {
208-
if let Some(ref replacement) = self.docker.localhost_replacement {
209-
address.replace("127.0.0.1", replacement)
216+
/// Get the host to use for version checks.
217+
/// If version_check_host is configured, use that host instead of the original address host.
218+
/// Returns the original host if no replacement is configured.
219+
pub fn get_version_check_host(&self, address: &str) -> String {
220+
if let Some(ref replacement) = self.docker.version_check_host {
221+
replacement.clone()
222+
} else {
223+
// Extract host from address (format: "ip:port")
224+
address
225+
.rsplit_once(':')
226+
.map(|(h, _)| h.to_string())
227+
.unwrap_or_else(|| address.to_string())
228+
}
229+
}
230+
231+
/// Apply address host override to an address string.
232+
/// If address_host_override is configured, replace the host portion while keeping the port.
233+
/// Returns the original address if no override is configured.
234+
pub fn apply_address_host_override(&self, address: &str) -> String {
235+
if let Some(ref override_host) = self.docker.address_host_override {
236+
// Extract port from address (format: "ip:port")
237+
if let Some((_, port)) = address.rsplit_once(':') {
238+
format!("{}:{}", override_host, port)
239+
} else {
240+
address.to_string()
241+
}
210242
} else {
211243
address.to_string()
212244
}
213245
}
214246
}
247+

src/grpc_client.rs

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::config::Network;
12
use semver::Version;
23
use std::time::Duration;
34
use tonic::transport::{Channel, ClientTlsConfig};
@@ -16,21 +17,31 @@ pub mod platform {
1617
use platform::{platform_client::PlatformClient, get_status_request::GetStatusRequestV0};
1718
use platform::GetStatusRequest;
1819

19-
pub async fn check_node_version(address: &str, port: u16) -> Result<VersionCheckResult, Box<dyn std::error::Error + Send + Sync>> {
20-
// Parse the address and create the endpoint
21-
let endpoint = format!("https://{}:{}", address, port);
22-
20+
pub async fn check_node_version(address: &str, port: u16, network: Network) -> Result<VersionCheckResult, Box<dyn std::error::Error + Send + Sync>> {
2321
// Create a channel with TLS configuration
24-
let tls = ClientTlsConfig::new()
25-
.with_native_roots()
26-
.assume_http2(true);
27-
28-
let channel = Channel::from_shared(endpoint)?
29-
.tls_config(tls)?
30-
.timeout(Duration::from_secs(2))
31-
.connect_timeout(Duration::from_secs(2))
32-
.connect()
33-
.await?;
22+
// For regtest, use HTTP (no TLS) since local nodes use self-signed certs
23+
// For mainnet/testnet, use HTTPS with proper certificate verification
24+
let channel = if network == Network::Regtest {
25+
// For regtest, use plain HTTP to avoid certificate issues
26+
let endpoint = format!("http://{}:{}", address, port);
27+
Channel::from_shared(endpoint)?
28+
.timeout(Duration::from_secs(2))
29+
.connect_timeout(Duration::from_secs(2))
30+
.connect()
31+
.await?
32+
} else {
33+
let endpoint = format!("https://{}:{}", address, port);
34+
let tls = ClientTlsConfig::new()
35+
.with_native_roots()
36+
.assume_http2(true);
37+
38+
Channel::from_shared(endpoint)?
39+
.tls_config(tls)?
40+
.timeout(Duration::from_secs(2))
41+
.connect_timeout(Duration::from_secs(2))
42+
.connect()
43+
.await?
44+
};
3445

3546
// Create the gRPC client
3647
let mut client = PlatformClient::new(channel);

src/masternode.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ pub struct EvoMasternodeInfo {
4040
pub pro_tx_hash: String,
4141
pub address: String,
4242
pub status: String,
43+
#[serde(rename = "platformHTTPPort", skip_serializing_if = "Option::is_none")]
44+
pub platform_http_port: Option<u16>,
4345
#[serde(rename = "versionCheck")]
4446
pub version_check: String, // "success", "fail", or "pending"
4547
#[serde(rename = "dapiVersion", skip_serializing_if = "Option::is_none")]
@@ -57,11 +59,12 @@ impl From<MasternodeInfo> for Option<EvoMasternodeInfo> {
5759
} else {
5860
"pending".to_string()
5961
};
60-
62+
6163
Some(EvoMasternodeInfo {
6264
pro_tx_hash: info.pro_tx_hash,
6365
address: info.address,
6466
status: info.status,
67+
platform_http_port: info.platform_http_port,
6568
version_check,
6669
dapi_version: None,
6770
drive_version: None,

src/masternode_cache.rs

Lines changed: 57 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use crate::config::Config;
2+
use crate::grpc_client;
23
use crate::masternode::EvoMasternodeList;
34
use crate::masternode_loader;
4-
use crate::grpc_client;
5+
use chrono::Local;
56
use std::sync::{Arc, RwLock};
67
use std::time::{Duration, Instant};
78
use tokio::sync::Mutex;
8-
use chrono::Local;
99

1010
pub struct MasternodeCache {
1111
data: Arc<RwLock<Option<EvoMasternodeList>>>,
@@ -24,7 +24,9 @@ impl MasternodeCache {
2424
}
2525
}
2626

27-
pub async fn get_masternodes(&self) -> Result<EvoMasternodeList, Box<dyn std::error::Error + Send + Sync>> {
27+
pub async fn get_masternodes(
28+
&self,
29+
) -> Result<EvoMasternodeList, Box<dyn std::error::Error + Send + Sync>> {
2830
// Check if we need to update the cache
2931
let should_update = {
3032
let last_update_guard = self.last_update.lock().await;
@@ -40,18 +42,16 @@ impl MasternodeCache {
4042

4143
// Return the cached data
4244
let cached_data = {
43-
let data = self.data.read()
44-
.map_err(|_| "Failed to read cache")?;
45+
let data = self.data.read().map_err(|_| "Failed to read cache")?;
4546
data.clone()
4647
};
47-
48+
4849
match cached_data {
4950
Some(masternodes) => Ok(masternodes),
5051
None => {
5152
// This shouldn't happen as we just updated, but handle it gracefully
5253
self.update_cache().await?;
53-
let data = self.data.read()
54-
.map_err(|_| "Failed to read cache")?;
54+
let data = self.data.read().map_err(|_| "Failed to read cache")?;
5555
Ok(data.as_ref().ok_or("No masternode data available")?.clone())
5656
}
5757
}
@@ -63,8 +63,9 @@ impl MasternodeCache {
6363
// Wrap the entire operation in a timeout (30 seconds)
6464
let result = tokio::time::timeout(
6565
tokio::time::Duration::from_secs(30),
66-
self.update_cache_internal()
67-
).await;
66+
self.update_cache_internal(),
67+
)
68+
.await;
6869

6970
match result {
7071
Ok(Ok(())) => Ok(()),
@@ -80,12 +81,16 @@ impl MasternodeCache {
8081
// Fetch new data
8182
let mut masternodes = masternode_loader::load_masternode_list(&self.config).await?;
8283

83-
println!("Checking version for {} Evo masternodes...", masternodes.len());
84-
84+
println!(
85+
"Checking version for {} Evo masternodes...",
86+
masternodes.len()
87+
);
88+
8589
// Check version for each masternode
8690
let check_tasks: Vec<_> = masternodes.iter().enumerate().map(|(idx, node)| {
8791
let address = node.address.clone();
8892
let status = node.status.clone();
93+
let platform_http_port = node.platform_http_port;
8994
let config = self.config.clone();
9095

9196
async move {
@@ -97,23 +102,17 @@ impl MasternodeCache {
97102
return (idx, "fail".to_string(), None, None, start.elapsed());
98103
}
99104

100-
// Parse address to get IP and port, applying localhost replacement if configured
101-
let resolved_address = config.replace_localhost(&address);
102-
let parts: Vec<&str> = resolved_address.split(':').collect();
103-
if parts.len() != 2 {
104-
println!("❌ Node {} at {} - invalid address format", idx, address);
105-
return (idx, "fail".to_string(), None, None, start.elapsed());
106-
}
107-
108-
let ip = parts[0].to_string();
109-
let port = config.get_dapi_port();
105+
// Get the host to use for version check (may be replaced by version_check_host)
106+
let ip = config.get_version_check_host(&address);
107+
// Use platformHTTPPort from masternode info, fallback to config default
108+
let port = platform_http_port.unwrap_or_else(|| config.get_dapi_port());
110109

111110
println!("🔍 Node {} at {} (resolved: {}:{}) - checking version...", idx, address, ip, port);
112111

113112
// Check version with additional timeout wrapper (2 seconds total)
114113
let result = match tokio::time::timeout(
115114
tokio::time::Duration::from_secs(2),
116-
grpc_client::check_node_version(&ip, port)
115+
grpc_client::check_node_version(&ip, port, config.network)
117116
).await {
118117
Ok(Ok(result)) => {
119118
let elapsed = start.elapsed();
@@ -141,7 +140,7 @@ impl MasternodeCache {
141140
result
142141
}
143142
}).collect();
144-
143+
145144
// Execute all version checks concurrently
146145
let overall_start = std::time::Instant::now();
147146
let results = futures::future::join_all(check_tasks).await;
@@ -162,33 +161,44 @@ impl MasternodeCache {
162161
}
163162
}
164163

165-
let success_count = masternodes.iter().filter(|n| n.version_check == "success").count();
166-
let fail_count = masternodes.iter().filter(|n| n.version_check == "fail").count();
167-
println!("Version check complete: {} success, {} fail (total time: {:?})", success_count, fail_count, total_elapsed);
164+
let success_count = masternodes
165+
.iter()
166+
.filter(|n| n.version_check == "success")
167+
.count();
168+
let fail_count = masternodes
169+
.iter()
170+
.filter(|n| n.version_check == "fail")
171+
.count();
172+
println!(
173+
"Version check complete: {} success, {} fail (total time: {:?})",
174+
success_count, fail_count, total_elapsed
175+
);
168176

169177
// Report slow nodes
170178
if !slow_nodes.is_empty() {
171-
println!("\n🐌 SLOW NODES DETECTED ({} nodes took >2s):", slow_nodes.len());
179+
println!(
180+
"\n🐌 SLOW NODES DETECTED ({} nodes took >2s):",
181+
slow_nodes.len()
182+
);
172183
slow_nodes.sort_by(|a, b| b.2.cmp(&a.2)); // Sort by duration, slowest first
173184
for (idx, address, duration) in slow_nodes.iter().take(10) {
174185
println!(" Node {} at {} took {:?}", idx, address, duration);
175186
}
176187
println!();
177188
}
178-
189+
179190
// Update the cache
180191
{
181-
let mut data = self.data.write()
182-
.map_err(|_| "Failed to write to cache")?;
192+
let mut data = self.data.write().map_err(|_| "Failed to write to cache")?;
183193
*data = Some(masternodes);
184194
}
185-
195+
186196
// Update the timestamp
187197
{
188198
let mut last_update = self.last_update.lock().await;
189199
*last_update = Some(Instant::now());
190200
}
191-
201+
192202
println!("Masternode cache updated successfully");
193203
Ok(())
194204
}
@@ -198,12 +208,22 @@ impl MasternodeCache {
198208
loop {
199209
tokio::time::sleep(self.update_interval).await;
200210
let now = Local::now();
201-
println!("🔄 [{}] Background refresh: Starting masternode cache update...", now.format("%Y-%m-%d %H:%M:%S"));
211+
println!(
212+
"🔄 [{}] Background refresh: Starting masternode cache update...",
213+
now.format("%Y-%m-%d %H:%M:%S")
214+
);
202215
match self.update_cache().await {
203-
Ok(_) => println!("✅ [{}] Background refresh: Masternode cache updated successfully", Local::now().format("%Y-%m-%d %H:%M:%S")),
204-
Err(e) => eprintln!("❌ [{}] Background refresh: Failed to update masternode cache: {}", Local::now().format("%Y-%m-%d %H:%M:%S"), e),
216+
Ok(_) => println!(
217+
"✅ [{}] Background refresh: Masternode cache updated successfully",
218+
Local::now().format("%Y-%m-%d %H:%M:%S")
219+
),
220+
Err(e) => eprintln!(
221+
"❌ [{}] Background refresh: Failed to update masternode cache: {}",
222+
Local::now().format("%Y-%m-%d %H:%M:%S"),
223+
e
224+
),
205225
}
206226
}
207227
});
208228
}
209-
}
229+
}

0 commit comments

Comments
 (0)