feat: electrs standalone install with bitcoin dependency + progress UI

- Add electrs to marketplace as standalone installable app
- Add dependency check: refuse install if no bitcoin node is running
- Use container DNS (bitcoin-knots:8332) on archy-net instead of host IP
- Auto-create bitcoin.conf with txindex + RPC on bitcoin-knots install
- Auto-build and start electrs-ui container post-install
- Show index size and estimated progress during initial sync
- Add /electrs-status and /health nginx proxy routes
- Remove Tailwind CDN from electrs-ui, use inline styles

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 02:16:02 +00:00
co-authored by Claude Opus 4.6
parent 825d082003
commit a5757d27f1
6 changed files with 474 additions and 67 deletions
+62 -2
View File
@@ -9,6 +9,9 @@ use std::time::Duration;
const ELECTRS_HOST: &str = "127.0.0.1";
const ELECTRS_PORT: u16 = 50001;
const BITCOIN_RPC_URL: &str = "http://127.0.0.1:8332/";
const ELECTRS_DATA_DIR: &str = "/var/lib/archipelago/mempool-electrs";
// Approximate final index size in bytes for mainnet with --lightmode (~35GB)
const ESTIMATED_FULL_INDEX_BYTES: f64 = 35_000_000_000.0;
/// Build Bitcoin RPC Basic auth header from env vars.
/// Falls back to cookie auth file if env vars are not set.
@@ -27,6 +30,35 @@ pub struct ElectrsSyncStatus {
pub progress_pct: f64,
pub status: String,
pub error: Option<String>,
/// Index data size in human-readable format (e.g. "11.2 GB")
pub index_size: Option<String>,
}
/// Get the total size of a directory in bytes.
fn dir_size_bytes(path: &str) -> u64 {
let mut total: u64 = 0;
if let Ok(entries) = std::fs::read_dir(path) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
total += dir_size_bytes(&path.to_string_lossy());
} else if let Ok(meta) = entry.metadata() {
total += meta.len();
}
}
}
total
}
/// Format bytes as human-readable string.
fn format_bytes(bytes: u64) -> String {
if bytes >= 1_000_000_000 {
format!("{:.1} GB", bytes as f64 / 1_000_000_000.0)
} else if bytes >= 1_000_000 {
format!("{:.1} MB", bytes as f64 / 1_000_000.0)
} else {
format!("{} KB", bytes / 1_000)
}
}
/// Fetch electrs indexed height via Electrum protocol (TCP JSON-RPC).
@@ -100,6 +132,14 @@ async fn bitcoin_network_height() -> Result<u64> {
/// Get electrs sync status. Runs blocking electrs call in spawn_blocking.
pub async fn get_electrs_sync_status() -> ElectrsSyncStatus {
// Get index data size (non-blocking, fast filesystem stat)
let data_bytes = dir_size_bytes(ELECTRS_DATA_DIR);
let index_size = if data_bytes > 0 {
Some(format_bytes(data_bytes))
} else {
None
};
let network_height = match bitcoin_network_height().await {
Ok(h) => h,
Err(e) => {
@@ -109,6 +149,7 @@ pub async fn get_electrs_sync_status() -> ElectrsSyncStatus {
progress_pct: 0.0,
status: "error".to_string(),
error: Some(format!("Bitcoin RPC: {}", e)),
index_size,
};
}
};
@@ -119,19 +160,36 @@ pub async fn get_electrs_sync_status() -> ElectrsSyncStatus {
// Electrs doesn't listen on 50001 until indexing completes (can take hours)
let err_msg = e.to_string();
let (status, error) = if err_msg.contains("connect") || err_msg.contains("Connection refused") {
// Estimate progress from data directory size
let est_pct = if data_bytes > 0 {
((data_bytes as f64 / ESTIMATED_FULL_INDEX_BYTES) * 100.0).min(99.0)
} else {
0.0
};
let size_str = index_size.clone().unwrap_or_else(|| "0 MB".to_string());
(
"indexing".to_string(),
Some("Electrs is building the index. Electrum RPC will be available when indexing completes (may take hours).".to_string()),
Some(format!(
"Building index ({} / ~35 GB estimated). Electrum RPC will be available when complete.",
size_str
)),
)
} else {
("error".to_string(), Some(format!("Electrs: {}", e)))
};
// Use estimated progress when indexing
let progress_pct = if status == "indexing" && data_bytes > 0 {
((data_bytes as f64 / ESTIMATED_FULL_INDEX_BYTES) * 100.0).min(99.0)
} else {
0.0
};
return ElectrsSyncStatus {
indexed_height: 0,
network_height,
progress_pct: 0.0,
progress_pct,
status,
error,
index_size,
};
}
Err(e) => {
@@ -141,6 +199,7 @@ pub async fn get_electrs_sync_status() -> ElectrsSyncStatus {
progress_pct: 0.0,
status: "error".to_string(),
error: Some(format!("Task: {}", e)),
index_size,
};
}
};
@@ -163,5 +222,6 @@ pub async fn get_electrs_sync_status() -> ElectrsSyncStatus {
progress_pct,
status: status.to_string(),
error: None,
index_size,
}
}