use crate::docker::compose::DockerCompose;
use crate::docker::health::HealthChecker;
use crate::error::{Result, ZecKitError};
use colored::*;
use indicatif::{ProgressBar, ProgressStyle};
use reqwest::Client;
use serde_json::json;
use std::fs;
use std::io::{self, Write};
use tokio::time::{sleep, Duration};
const DEFAULT_FAUCET_ADDRESS: &str = "tmBsTi2xWTjUdEXnuTceL7fecEQKeWaPDJd";
pub async fn execute(backend: String, fresh: bool, timeout: u64, action_mode: bool, miner_address: Option<String>, fund_address: Option<String>, fund_amount: f64, project_dir: Option<String>, image_prefix: Option<String>, block_interval: u64, activation_heights: Option<String>) -> Result<()> {
println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan());
println!("{}", " ZecKit - Starting Devnet".cyan().bold());
println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan());
println!();
if let Some(ref heights) = activation_heights {
std::env::set_var("ZECKIT_ACTIVATION_HEIGHTS", heights);
} else {
std::env::remove_var("ZECKIT_ACTIVATION_HEIGHTS");
}
std::env::set_var("BLOCK_INTERVAL", block_interval.to_string());
let compose = DockerCompose::new(project_dir.clone(), image_prefix)?;
if fresh {
println!("{}", "🧹 Cleaning up old data (fresh start)...".yellow());
compose.down(true)?;
}
let (services, profile) = match backend.as_str() {
"lwd" => (vec!["zebra-miner", "zebra-sync", "lightwalletd", "faucet-lwd"], "lwd"),
"zaino" => (vec!["zebra-miner", "zebra-sync", "zaino", "faucet-zaino"], "zaino"),
"none" => (vec!["zebra-miner", "zebra-sync"], "none"),
_ => {
return Err(ZecKitError::Config(format!(
"Invalid backend: {}. Use 'lwd', 'zaino', or 'none'",
backend
)));
}
};
println!("Starting services: {}", services.join(", "));
println!();
println!("📝 Configuring Zebra mining address...");
let resolved_miner_address = miner_address.as_deref().unwrap_or(DEFAULT_FAUCET_ADDRESS);
match update_zebra_config_file(resolved_miner_address, project_dir.clone(), activation_heights.clone()) {
Ok(_) => {
println!("✓ Updated docker/configs/zebra.toml");
if activation_heights.is_some() {
println!("✓ Updated custom activation heights in zebra.toml and zebra-sync.toml");
}
println!(" Mining to: {}", resolved_miner_address);
}
Err(e) => {
println!("{}", format!("Warning: Could not update zebra.toml: {}", e).yellow());
println!(" Using existing config");
}
}
println!();
if backend == "lwd" || backend == "zaino" {
compose.up_with_profile(profile, fresh)?;
println!();
} else {
compose.up(&services)?;
}
println!("Starting services...");
println!();
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.green} {msg}")
.unwrap()
);
let checker = HealthChecker::new();
let deadline = std::time::Instant::now() + Duration::from_secs(timeout * 60);
fn secs_remaining(deadline: std::time::Instant) -> u64 {
deadline.saturating_duration_since(std::time::Instant::now()).as_secs()
}
println!("Waiting for Zebra Miner node to initialize...");
let mut last_error_miner = String::new();
let mut last_error_print = std::time::Instant::now();
loop {
pb.tick();
match checker.check_zebra_miner_ready().await {
Ok(_) => {
println!("\n[1.1/3] Zebra Miner ready");
break;
}
Err(e) => {
let err_str = e.to_string();
if err_str != last_error_miner || last_error_print.elapsed().as_secs() > 10 {
println!(" Miner: {}", err_str);
last_error_miner = err_str;
last_error_print = std::time::Instant::now();
}
if std::time::Instant::now() >= deadline {
let _ = save_faucet_stats_artifact(action_mode, project_dir.clone()).await;
return Err(ZecKitError::ServiceNotReady(format!(
"Zebra Miner not ready within the {} minute global timeout: {}",
timeout, e
)));
}
}
}
sleep(Duration::from_secs(2)).await;
}
println!("Mining 10 initial blocks to unblock backend indexers...");
if let Err(e) = mine_additional_blocks(10).await {
println!(" Warning: Could not mine 10 blocks immediately: {}", e);
}
println!("Waiting for Zebra Sync node to initialize and peer...");
let mut last_error_sync = String::new();
last_error_print = std::time::Instant::now();
loop {
pb.tick();
match checker.check_zebra_sync_ready().await {
Ok(_) => {
println!("\n[1.2/3] Zebra Sync Node ready");
break;
}
Err(e) => {
let err_str = e.to_string();
if err_str != last_error_sync || last_error_print.elapsed().as_secs() > 10 {
println!(" Sync Node: {}", err_str);
last_error_sync = err_str;
last_error_print = std::time::Instant::now();
}
if std::time::Instant::now() >= deadline {
let _ = save_faucet_stats_artifact(action_mode, project_dir.clone()).await;
return Err(ZecKitError::ServiceNotReady(format!(
"Zebra Sync Node not ready within the {} minute global timeout: {}",
timeout, e
)));
}
}
}
sleep(Duration::from_secs(2)).await;
}
println!("Waiting for Sync Node to catch up with Miner Node...");
let parity_deadline = (std::time::Instant::now() + Duration::from_secs(30)).min(deadline);
loop {
pb.tick();
match checker.check_zebra_sync_parity().await {
Ok(_) => {
println!("✓ Sync parity achieved");
break;
}
Err(e) => {
if std::time::Instant::now() >= deadline {
let _ = save_faucet_stats_artifact(action_mode, project_dir.clone()).await;
return Err(ZecKitError::ServiceNotReady(format!(
"Global timeout reached during Sync Parity. (Budget: {} min)",
timeout
)));
}
if std::time::Instant::now() >= parity_deadline {
println!("{}", format!("⚠ Warning: Sync node is lagging ({}). Continuing anyway...", e).yellow());
println!("{}", " (LWD and Faucet will point to the healthy Miner node)".yellow());
break;
}
}
}
sleep(Duration::from_secs(2)).await;
}
println!("[1/3] Zebra Cluster ready (100%)");
println!();
if backend == "lwd" || backend == "zaino" {
let backend_name = if backend == "lwd" { "Lightwalletd" } else { "Zaino" };
loop {
pb.tick();
if checker.wait_for_backend(&backend, &pb).await.is_ok() {
println!("[2/3] {} ready (100%)", backend_name);
break;
}
if std::time::Instant::now() >= deadline {
let _ = save_faucet_stats_artifact(action_mode, project_dir.clone()).await;
return Err(ZecKitError::ServiceNotReady(format!(
"{} not ready within the {} minute global timeout",
backend_name, timeout
)));
}
let remaining = secs_remaining(deadline);
let total = timeout * 60;
let elapsed = total.saturating_sub(remaining);
let progress = (elapsed as f64 / total as f64 * 100.0).min(99.0) as u32;
print!("\r[2/3] Starting {}... {}%", backend_name, progress);
io::stdout().flush().ok();
sleep(Duration::from_secs(1)).await;
}
println!();
}
loop {
pb.tick();
if checker.wait_for_faucet(&pb).await.is_ok() {
println!("[3/3] Faucet ready (100%)");
break;
}
if std::time::Instant::now() >= deadline {
let _ = save_faucet_stats_artifact(action_mode, project_dir.clone()).await;
return Err(ZecKitError::ServiceNotReady(format!(
"Faucet not ready within the {} minute global timeout",
timeout
)));
}
let remaining = secs_remaining(deadline);
let total = timeout * 60;
let elapsed = total.saturating_sub(remaining);
let progress = (elapsed as f64 / total as f64 * 100.0).min(99.0) as u32;
print!("\r[3/3] Starting Faucet... {}%", progress);
io::stdout().flush().ok();
sleep(Duration::from_secs(1)).await;
}
println!();
pb.finish_and_clear();
println!();
println!("🔍 Verifying wallet configuration...");
match get_wallet_transparent_address_from_faucet().await {
Ok(addr) => {
println!("✓ Faucet wallet address: {}", addr);
if addr != DEFAULT_FAUCET_ADDRESS {
println!("{}", format!("⚠ Warning: Address mismatch!").yellow());
println!("{}", format!(" Expected: {}", DEFAULT_FAUCET_ADDRESS).yellow());
println!("{}", format!(" Got: {}", addr).yellow());
println!("{}", " This may cause funds to be lost!".yellow());
} else {
println!("✓ Address matches Zebra mining configuration");
}
}
Err(e) => {
println!("{}", format!("Warning: Could not verify wallet address: {}", e).yellow());
}
}
println!();
println!();
let current_blocks = get_block_count(&Client::new()).await.unwrap_or(0);
let target_blocks = 200;
if current_blocks < target_blocks {
let needed = (target_blocks - current_blocks) as u32;
println!("Mining {} initial blocks for full coinbase maturity...", needed);
mine_additional_blocks(needed).await?;
}
println!();
println!("Waiting for Backend ({}) to sync these blocks...", backend);
if let Err(e) = checker.wait_for_backend(&backend, &pb).await {
if std::time::Instant::now() >= deadline {
let _ = save_faucet_stats_artifact(action_mode, project_dir.clone()).await;
return Err(ZecKitError::ServiceNotReady(format!(
"Global timeout reached during final verification: {}", e
)));
}
println!("{}", format!("Warning: Sync verification incomplete: {}", e).yellow());
println!(" Continuing with best-effort wait...");
sleep(Duration::from_secs(15)).await;
} else {
println!("✓ Backend fully synchronized at target height");
}
println!();
println!("Waiting for blocks to propagate and indexer to catch up...");
sleep(Duration::from_secs(45)).await;
println!();
println!("Generating ZIP-316 Unified Address fixtures...");
match generate_ua_fixtures_from_faucet().await {
Ok(address) => {
println!("Generated UA: {}...", &address[..20]);
}
Err(e) => {
println!("{}", format!("Warning: Could not generate UA fixture ({})", e).yellow());
}
}
println!();
println!("Funding faucet wallet (sync → shield → verify Orchard balance)...");
let max_fund_attempts = 8;
let mut orchard_balance_confirmed = false;
for attempt in 1..=max_fund_attempts {
println!();
println!(" [Attempt {}/{}] Syncing wallet...", attempt, max_fund_attempts);
sleep(Duration::from_secs(10)).await;
if let Err(e) = sync_wallet_via_faucet().await {
println!("{}", format!(" Sync failed: {}. Retrying...", e).yellow());
sleep(Duration::from_secs(20)).await;
continue;
}
println!(" ✓ Wallet synced");
let (transparent, orchard, _total) = match check_wallet_balance().await {
Ok(b) => b,
Err(e) => {
println!("{}", format!(" Balance check failed: {}. Retrying...", e).yellow());
sleep(Duration::from_secs(20)).await;
continue;
}
};
println!(" Transparent: {} ZEC | Orchard: {} ZEC", transparent, orchard);
if orchard > 0.0 {
println!("{}", " ✓ Faucet already has Orchard balance!".green());
orchard_balance_confirmed = true;
break;
}
if transparent == 0.0 {
println!("{}", " No transparent balance yet — mining 10 more blocks...".yellow());
let _ = mine_additional_blocks(10).await;
sleep(Duration::from_secs(20)).await;
continue;
}
println!(" Shielding {} ZEC transparent → Orchard...", transparent);
if let Err(e) = shield_transparent_funds().await {
println!("{}", format!(" Shield failed: {}. Retrying...", e).yellow());
sleep(Duration::from_secs(20)).await;
continue;
}
println!(" ✓ Shield tx submitted. Mining confirmation blocks...");
let _ = mine_additional_blocks(2).await;
sleep(Duration::from_secs(20)).await;
if let Err(e) = sync_wallet_via_faucet().await {
println!("{}", format!(" Post-shield sync failed: {}. Retrying...", e).yellow());
sleep(Duration::from_secs(10)).await;
continue;
}
if let Ok((_, post_orchard, _)) = check_wallet_balance().await {
if post_orchard > 0.0 {
println!("{}", format!(" ✓ Orchard balance confirmed: {} ZEC", post_orchard).green());
orchard_balance_confirmed = true;
break;
}
}
println!("{}", " Orchard balance still 0. Retrying...".yellow());
sleep(Duration::from_secs(10)).await;
}
if !orchard_balance_confirmed {
println!("{}", "⚠ WARNING: Faucet Orchard balance is 0 after all retries.".yellow().bold());
println!("{}", " The smoke tests will fail. Check that Zebra is mining to the correct address.".yellow());
} else {
println!();
println!("{}", "✓ Faucet wallet funded and ready!".green().bold());
}
println!();
println!("Final wallet balance:");
match check_wallet_balance().await {
Ok((transparent, orchard, total)) => {
println!(" Transparent: {} ZEC", transparent);
println!(" Orchard: {} ZEC", orchard);
println!(" Total: {} ZEC", total);
if total > 0.0 {
println!();
println!("{}", "✓ Faucet wallet funded and ready!".green().bold());
}
}
Err(e) => {
println!("{}", format!("Could not check balance: {}", e).yellow());
}
}
println!();
println!("Starting continuous background miner (1 block every {}s)...", block_interval);
start_background_miner(block_interval).await?;
print_connection_info(&backend);
print_mining_info().await?;
println!();
println!("{}", "✓ Devnet is running with continuous mining".green().bold());
println!("{}", format!(" New blocks will be mined every {} seconds", block_interval).green());
println!("{}", " Press Ctrl+C to stop".green());
if action_mode {
let _ = save_faucet_stats_artifact(action_mode, project_dir.clone()).await;
}
if let Some(ref dest_addr) = fund_address {
println!();
println!("💸 Auto-funding destination address...");
println!(" Recipient: {}", dest_addr);
println!(" Amount: {} ZEC", fund_amount);
match fund_destination_address(dest_addr, fund_amount).await {
Ok(txid) => {
println!("✓ Funded destination: {} ZEC → {}", fund_amount, dest_addr);
println!(" TXID: {}", txid);
}
Err(e) => {
println!("{}", format!("⚠ Warning: Auto-fund failed: {}", e).yellow());
println!("{}", " The devnet is still running; fund manually via the faucet.".yellow());
}
}
}
Ok(())
}
async fn save_faucet_stats_artifact(action_mode: bool, project_dir_override: Option<String>) -> Result<()> {
if !action_mode {
return Ok(());
}
let project_dir = if let Some(dir) = project_dir_override {
std::path::PathBuf::from(dir)
} else {
dirs::home_dir()
.ok_or_else(|| ZecKitError::Config("Could not find home directory".into()))?
.join(".zeckit")
};
let log_dir = project_dir.join("logs");
fs::create_dir_all(&log_dir).ok();
match Client::new().get("http://127.0.0.1:8080/stats").send().await {
Ok(resp) => {
if let Ok(json) = resp.json::<serde_json::Value>().await {
let stats_path = log_dir.join("faucet-stats.json");
fs::write(
&stats_path,
serde_json::to_string_pretty(&json)?
).ok();
println!("✓ Saved {:?}", stats_path);
}
}
Err(e) => println!(" Warning: Could not get faucet stats for artifact: {}", e),
}
Ok(())
}
fn update_zebra_config_file(address: &str, project_dir_override: Option<String>, activation_heights: Option<String>) -> Result<()> {
use regex::Regex;
let project_dir = if let Some(dir) = project_dir_override {
std::path::PathBuf::from(dir)
} else {
dirs::home_dir()
.ok_or_else(|| ZecKitError::Config("Could not find home directory".into()))?
.join(".zeckit")
};
let config_path = project_dir.join("docker/configs/zebra.toml");
let sync_config_path = project_dir.join("docker/configs/zebra-sync.toml");
let update_file = |path: &std::path::PathBuf| -> Result<()> {
let config = fs::read_to_string(path)
.map_err(|e| ZecKitError::Config(format!("Could not read {:?}: {}", path, e)))?;
let mut updated = if config.contains("miner_address") {
let re = Regex::new(r#"miner_address\s*=\s*"[^"]*""#)
.map_err(|e| ZecKitError::Config(format!("Regex error: {}", e)))?;
re.replace(&config, format!("miner_address = \"{}\"", address)).to_string()
} else {
if config.contains("[mining]") {
config.replace(
"[mining]",
&format!("[mining]\nminer_address = \"{}\"", address)
)
} else {
format!("{}\n\n[mining]\nminer_address = \"{}\"\n", config, address)
}
};
if let Some(ref heights_str) = activation_heights {
updated = update_activation_heights_in_toml(&updated, heights_str)?;
}
fs::write(path, updated)
.map_err(|e| ZecKitError::Config(format!("Could not write {:?}: {}", path, e)))?;
Ok(())
};
update_file(&config_path)?;
if sync_config_path.exists() {
update_file(&sync_config_path).ok(); }
Ok(())
}
fn update_activation_heights_in_toml(content: &str, heights_str: &str) -> Result<String> {
let mut heights_map = std::collections::HashMap::new();
for part in heights_str.split(',') {
let kv: Vec<&str> = part.split('=').collect();
if kv.len() == 2 {
let key = kv[0].trim().to_uppercase();
let val = kv[1].trim().parse::<u64>().map_err(|e| ZecKitError::Config(format!("Invalid height value: {}", e)))?;
heights_map.insert(key, val);
}
}
if heights_map.is_empty() {
return Ok(content.to_string());
}
let section_header = "[network.testnet_parameters.activation_heights]";
if !content.contains(section_header) {
return Err(ZecKitError::Config("Could not find [network.testnet_parameters.activation_heights] section in zebra config".into()));
}
let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
let mut header_line_idx = 0;
for (i, line) in lines.iter().enumerate() {
if line.trim() == section_header {
header_line_idx = i;
break;
}
}
let mut new_lines = lines[0..=header_line_idx].to_vec();
let mut current_idx = header_line_idx + 1;
let mut skipped_keys = std::collections::HashSet::new();
for (key, val) in &heights_map {
let key_formatted = if key.contains('.') {
format!("\"{}\"", key)
} else {
key.clone()
};
new_lines.push(format!("{} = {}", key_formatted, val));
skipped_keys.insert(key.clone());
}
while current_idx < lines.len() {
let line = &lines[current_idx];
let trimmed = line.trim();
if trimmed.starts_with('[') {
new_lines.extend_from_slice(&lines[current_idx..]);
break;
} else {
if let Some(eq_idx) = trimmed.find('=') {
let original_key = trimmed[..eq_idx].trim().trim_matches('"').to_uppercase();
if !skipped_keys.contains(&original_key) {
new_lines.push(line.clone());
}
} else if trimmed.starts_with('#') || trimmed.is_empty() {
new_lines.push(line.clone());
}
}
current_idx += 1;
}
Ok(new_lines.join("\n"))
}
async fn mine_additional_blocks(count: u32) -> Result<()> {
let client = Client::new();
println!("Mining {} additional blocks...", count);
let mut successful_mines = 0;
while successful_mines < count {
let res = client
.post("http://127.0.0.1:8232")
.json(&json!({
"jsonrpc": "2.0",
"id": "generate",
"method": "generate",
"params": [1]
}))
.timeout(Duration::from_secs(10))
.send()
.await;
match res {
Ok(resp) if resp.status().is_success() => {
successful_mines += 1;
if successful_mines % 10 == 0 || successful_mines == count {
print!("\r Mined {} / {} blocks", successful_mines, count);
io::stdout().flush().ok();
}
sleep(Duration::from_millis(100)).await;
}
Ok(_resp) => {
sleep(Duration::from_millis(500)).await;
}
Err(_) => {
sleep(Duration::from_millis(500)).await;
}
}
}
println!("\n✓ Mined {} additional blocks", count);
Ok(())
}
async fn start_background_miner(interval_secs: u64) -> Result<()> {
tokio::spawn(async move {
let client = Client::new();
let mut interval = tokio::time::interval(Duration::from_secs(interval_secs));
loop {
interval.tick().await;
let _ = client
.post("http://127.0.0.1:8232")
.json(&json!({
"jsonrpc": "2.0",
"id": "bgminer",
"method": "generate",
"params": [1]
}))
.timeout(Duration::from_secs(10))
.send()
.await;
}
});
Ok(())
}
async fn shield_transparent_funds() -> Result<()> {
let client = Client::new();
println!("Shielding transparent funds to Orchard...");
let resp = client
.post("http://127.0.0.1:8080/shield")
.timeout(Duration::from_secs(300)) .send()
.await?;
let json: serde_json::Value = resp.json().await?;
if json["status"] == "no_funds" {
return Err(ZecKitError::HealthCheck("No transparent funds to shield".into()));
}
if let Some(txid) = json.get("txid").and_then(|v| v.as_str()) {
println!("✓ Shielded {} ZEC", json["transparent_amount"].as_f64().unwrap_or(0.0));
println!(" Transaction ID: {}", txid);
println!(" Waiting for confirmation...");
sleep(Duration::from_secs(20)).await;
return Ok(());
}
let error_msg = json.get("error").and_then(|v| v.as_str()).unwrap_or("Shield transaction failed");
Err(ZecKitError::HealthCheck(error_msg.to_string()))
}
async fn get_block_count(client: &Client) -> Result<u64> {
let resp = client
.post("http://127.0.0.1:8232")
.json(&json!({
"jsonrpc": "2.0",
"id": "blockcount",
"method": "getblockcount",
"params": []
}))
.timeout(Duration::from_secs(5))
.send()
.await?;
let json: serde_json::Value = resp.json().await?;
let miner_height = json.get("result")
.and_then(|v| v.as_u64())
.ok_or_else(|| ZecKitError::HealthCheck("Invalid miner block count".into()))?;
if let Ok(resp_sync) = client
.post("http://127.0.0.1:18232")
.json(&json!({
"jsonrpc": "2.0",
"id": "blockcount",
"method": "getblockcount",
"params": []
}))
.timeout(Duration::from_secs(2))
.send()
.await {
if let Ok(json_sync) = resp_sync.json::<serde_json::Value>().await {
if let Some(sync_height) = json_sync.get("result").and_then(|v| v.as_u64()) {
if sync_height < miner_height {
}
}
}
}
Ok(miner_height)
}
async fn get_wallet_transparent_address_from_faucet() -> Result<String> {
let client = Client::new();
let resp = client
.get("http://127.0.0.1:8080/address")
.timeout(Duration::from_secs(10))
.send()
.await
.map_err(|e| ZecKitError::HealthCheck(format!("Faucet API call failed: {}", e)))?;
let json: serde_json::Value = resp.json().await?;
json.get("transparent_address")
.and_then(|v| v.as_str())
.ok_or_else(|| ZecKitError::HealthCheck("No transparent address in faucet response".into()))
.map(|s| s.to_string())
}
async fn generate_ua_fixtures_from_faucet() -> Result<String> {
let client = Client::new();
let resp = client
.get("http://127.0.0.1:8080/address")
.timeout(Duration::from_secs(10))
.send()
.await
.map_err(|e| ZecKitError::HealthCheck(format!("Faucet API call failed: {}", e)))?;
let json: serde_json::Value = resp.json().await?;
let ua_address = json.get("unified_address")
.and_then(|v| v.as_str())
.ok_or_else(|| ZecKitError::HealthCheck("No unified address in faucet response".into()))?;
let fixture = json!({
"faucet_address": ua_address,
"type": "unified",
"receivers": ["orchard"]
});
fs::create_dir_all("fixtures")?;
fs::write(
"fixtures/unified-addresses.json",
serde_json::to_string_pretty(&fixture)?
)?;
Ok(ua_address.to_string())
}
async fn sync_wallet_via_faucet() -> Result<()> {
let client = Client::new();
let resp = client
.post("http://127.0.0.1:8080/sync")
.timeout(Duration::from_secs(60))
.send()
.await
.map_err(|e| ZecKitError::HealthCheck(format!("Faucet sync failed: {}", e)))?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(ZecKitError::HealthCheck(
format!("Wallet sync failed ({}): {}", status, body)
));
}
Ok(())
}
async fn check_wallet_balance() -> Result<(f64, f64, f64)> {
let client = Client::new();
let resp = client
.get("http://127.0.0.1:8080/stats")
.timeout(Duration::from_secs(5))
.send()
.await?;
let json: serde_json::Value = resp.json().await?;
let transparent = json["transparent_balance"].as_f64().unwrap_or(0.0);
let orchard = json["orchard_balance"].as_f64().unwrap_or(0.0);
let total = json["current_balance"].as_f64().unwrap_or(0.0);
Ok((transparent, orchard, total))
}
async fn print_mining_info() -> Result<()> {
let client = Client::new();
if let Ok(height) = get_block_count(&client).await {
println!();
println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan());
println!("{}", " Blockchain Status".cyan().bold());
println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan());
println!();
println!(" Block Height: {}", height);
println!(" Network: Regtest");
println!(" Mining: Continuous (1 block / 15s)");
}
Ok(())
}
fn print_connection_info(backend: &str) {
println!();
println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan());
println!("{}", " Services Ready".cyan().bold());
println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan());
println!();
println!(" Zebra RPC: http://127.0.0.1:8232");
println!(" Faucet API: http://127.0.0.1:8080");
if backend == "lwd" {
println!(" LightwalletD: http://127.0.0.1:9067");
} else if backend == "zaino" {
println!(" Zaino: http://127.0.0.1:9067");
}
println!();
println!("Next steps:");
println!(" • Check balance: curl http://127.0.0.1:8080/stats");
println!(" • View fixtures: cat fixtures/unified-addresses.json");
println!(" • Request funds: curl -X POST http://127.0.0.1:8080/request -d '{{\"address\":\"...\"}}'");
println!();
}
async fn fund_destination_address(addr: &str, amount: f64) -> Result<String> {
let client = Client::new();
let resp = client
.post("http://127.0.0.1:8080/request")
.json(&json!({
"address": addr,
"amount": amount
}))
.timeout(Duration::from_secs(30))
.send()
.await
.map_err(|e| ZecKitError::HealthCheck(format!("Auto-fund request failed: {}", e)))?;
let json: serde_json::Value = resp.json().await?;
if let Some(txid) = json.get("txid").and_then(|v| v.as_str()) {
Ok(txid.to_string())
} else {
let err = json.get("error").and_then(|v| v.as_str()).unwrap_or("Unknown error");
Err(ZecKitError::HealthCheck(err.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_update_activation_heights_in_toml() {
let original_toml = r#"[network]
network = "Regtest"
[network.testnet_parameters.activation_heights]
NU5 = 1
NU6 = 1
"NU6.1" = 1
[[network.testnet_parameters.lockbox_disbursements]]
address = "t2RnBRiqrN1nW4ecZs1Fj3WWjNdnSs4kiX8"
"#;
let heights_str = "nu5=10,nu6=20,nu6.1=30,nu7=40";
let updated = update_activation_heights_in_toml(original_toml, heights_str).unwrap();
assert!(updated.contains("NU5 = 10"));
assert!(updated.contains("NU6 = 20"));
assert!(updated.contains("\"NU6.1\" = 30"));
assert!(updated.contains("NU7 = 40"));
assert!(updated.contains("network = \"Regtest\""));
}
}