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:
@@ -43,6 +43,25 @@ impl RpcHandler {
|
||||
return self.install_penpot_stack().await;
|
||||
}
|
||||
|
||||
// Dependency check: electrs requires a Bitcoin node
|
||||
if package_id == "mempool-electrs" || package_id == "electrs" {
|
||||
let btc_check = tokio::process::Command::new("sudo")
|
||||
.args(["podman", "ps", "--format", "{{.Names}}"])
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to check running containers")?;
|
||||
let running = String::from_utf8_lossy(&btc_check.stdout);
|
||||
let has_bitcoin = running.lines().any(|l| {
|
||||
let name = l.trim();
|
||||
name == "bitcoin-knots" || name == "bitcoin-core" || name == "bitcoin"
|
||||
});
|
||||
if !has_bitcoin {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Electrs requires a running Bitcoin node (Bitcoin Knots or Bitcoin Core). Please install and start Bitcoin Knots first."
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if container already exists
|
||||
let check_output = tokio::process::Command::new("sudo")
|
||||
.args(["podman", "ps", "-a", "--format", "{{.Names}}", "--filter", &format!("name=^{}$", package_id)])
|
||||
@@ -84,11 +103,14 @@ impl RpcHandler {
|
||||
debug!("Using local image: {}", docker_image);
|
||||
}
|
||||
|
||||
// Normalize container name: "electrs" alias -> "mempool-electrs"
|
||||
let container_name = if package_id == "electrs" { "mempool-electrs" } else { package_id };
|
||||
|
||||
// Create and start container with security constraints
|
||||
let mut run_args = vec![
|
||||
"podman", "run",
|
||||
"-d", // Detached
|
||||
"--name", package_id,
|
||||
"--name", container_name,
|
||||
"--restart=unless-stopped", // Auto-restart policy
|
||||
];
|
||||
|
||||
@@ -105,7 +127,7 @@ impl RpcHandler {
|
||||
let needs_archy_net = matches!(
|
||||
package_id,
|
||||
"bitcoin-knots" | "bitcoin" | "bitcoin-core"
|
||||
| "mempool" | "mempool-web" | "mempool-api" | "mempool-electrs" | "mysql-mempool" | "archy-mempool-db" | "archy-mempool-web"
|
||||
| "mempool" | "mempool-web" | "mempool-api" | "mempool-electrs" | "electrs" | "mysql-mempool" | "archy-mempool-db" | "archy-mempool-web"
|
||||
| "btcpay-server" | "btcpayserver" | "archy-btcpay-db"
|
||||
);
|
||||
|
||||
@@ -168,6 +190,27 @@ impl RpcHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-install: Create bitcoin.conf for Bitcoin nodes with RPC + txindex
|
||||
if matches!(package_id, "bitcoin" | "bitcoin-core" | "bitcoin-knots") {
|
||||
let bitcoin_dir = "/var/lib/archipelago/bitcoin";
|
||||
let conf_path = format!("{}/bitcoin.conf", bitcoin_dir);
|
||||
let bitcoin_conf = "\
|
||||
server=1\n\
|
||||
txindex=1\n\
|
||||
rpcuser=archipelago\n\
|
||||
rpcpassword=archipelago123\n\
|
||||
rpcbind=0.0.0.0\n\
|
||||
rpcallowip=0.0.0.0/0\n\
|
||||
rpcport=8332\n\
|
||||
listen=1\n\
|
||||
printtoconsole=1\n";
|
||||
let _ = tokio::process::Command::new("sudo")
|
||||
.args(["sh", "-c", &format!("echo '{}' > {}", bitcoin_conf, conf_path)])
|
||||
.output()
|
||||
.await;
|
||||
info!("Created bitcoin.conf at {} with RPC + txindex enabled", conf_path);
|
||||
}
|
||||
|
||||
// Add port mappings (skip if host network mode like Tailscale)
|
||||
if !is_tailscale {
|
||||
for port in &ports {
|
||||
@@ -244,6 +287,32 @@ impl RpcHandler {
|
||||
});
|
||||
}
|
||||
|
||||
// Post-install: Start electrs-ui container for electrs
|
||||
if matches!(package_id, "mempool-electrs" | "electrs") {
|
||||
tokio::spawn(async move {
|
||||
// Build and start electrs-ui with host networking so it can reach backend on 127.0.0.1:5678
|
||||
let ui_dir = "/opt/archipelago/docker/electrs-ui";
|
||||
let _ = tokio::process::Command::new("sudo")
|
||||
.args(["podman", "build", "-t", "localhost/electrs-ui", ui_dir])
|
||||
.output()
|
||||
.await;
|
||||
// Remove old UI container if it exists
|
||||
let _ = tokio::process::Command::new("sudo")
|
||||
.args(["podman", "rm", "-f", "electrs-ui"])
|
||||
.output()
|
||||
.await;
|
||||
let _ = tokio::process::Command::new("sudo")
|
||||
.args([
|
||||
"podman", "run", "-d", "--name", "electrs-ui",
|
||||
"--restart=unless-stopped", "--network=host",
|
||||
"localhost/electrs-ui",
|
||||
])
|
||||
.output()
|
||||
.await;
|
||||
info!("Electrs UI container started on port 50002");
|
||||
});
|
||||
}
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"success": true,
|
||||
"package_id": package_id,
|
||||
@@ -793,6 +862,25 @@ const TRUSTED_REGISTRIES: &[&str] = &[
|
||||
];
|
||||
|
||||
/// Validate Docker image against trusted registry allowlist.
|
||||
/// Detect which Bitcoin container is running on archy-net for DNS resolution.
|
||||
/// Returns the container name to use as the RPC host (e.g., "bitcoin-knots").
|
||||
fn detect_bitcoin_container_name() -> String {
|
||||
// Synchronous check — called from get_app_config which is sync
|
||||
let output = std::process::Command::new("sudo")
|
||||
.args(["podman", "ps", "--format", "{{.Names}}"])
|
||||
.output();
|
||||
if let Ok(out) = output {
|
||||
let names = String::from_utf8_lossy(&out.stdout);
|
||||
for candidate in &["bitcoin-knots", "bitcoin-core", "bitcoin"] {
|
||||
if names.lines().any(|l| l.trim() == *candidate) {
|
||||
return candidate.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Default to bitcoin-knots (most common)
|
||||
"bitcoin-knots".to_string()
|
||||
}
|
||||
|
||||
fn is_valid_docker_image(image: &str) -> bool {
|
||||
if image.is_empty() || image.len() > 256 {
|
||||
return false;
|
||||
@@ -938,24 +1026,28 @@ fn get_app_config(
|
||||
None,
|
||||
None,
|
||||
),
|
||||
"mempool-electrs" => (
|
||||
vec!["50001:50001".to_string()],
|
||||
vec!["/var/lib/archipelago/mempool-electrs:/data".to_string()],
|
||||
vec![],
|
||||
None,
|
||||
Some(vec![
|
||||
"--daemon-rpc-addr".to_string(),
|
||||
format!("{}:8332", host_ip),
|
||||
"--cookie".to_string(),
|
||||
"archipelago:archipelago123".to_string(),
|
||||
"--jsonrpc-import".to_string(),
|
||||
"--electrum-rpc-addr".to_string(),
|
||||
"0.0.0.0:50001".to_string(),
|
||||
"--db-dir".to_string(),
|
||||
"/data".to_string(),
|
||||
"--lightmode".to_string(),
|
||||
]),
|
||||
),
|
||||
"mempool-electrs" | "electrs" => {
|
||||
// Detect which bitcoin container is running for archy-net DNS resolution
|
||||
let bitcoin_host = detect_bitcoin_container_name();
|
||||
(
|
||||
vec!["50001:50001".to_string()],
|
||||
vec!["/var/lib/archipelago/mempool-electrs:/data".to_string()],
|
||||
vec![],
|
||||
None,
|
||||
Some(vec![
|
||||
"--daemon-rpc-addr".to_string(),
|
||||
format!("{}:8332", bitcoin_host),
|
||||
"--cookie".to_string(),
|
||||
"archipelago:archipelago123".to_string(),
|
||||
"--jsonrpc-import".to_string(),
|
||||
"--electrum-rpc-addr".to_string(),
|
||||
"0.0.0.0:50001".to_string(),
|
||||
"--db-dir".to_string(),
|
||||
"/data".to_string(),
|
||||
"--lightmode".to_string(),
|
||||
]),
|
||||
)
|
||||
},
|
||||
"mysql-mempool" => (
|
||||
vec![],
|
||||
vec!["/var/lib/archipelago/mysql-mempool:/var/lib/mysql".to_string()],
|
||||
|
||||
Reference in New Issue
Block a user