use crate::error::Result;
use colored::*;
use reqwest::Client;
use serde_json::{Value, json};
use tokio::time::{sleep, Duration};
use std::fs;
use chrono;
pub async fn execute(amount: f64, memo: String, action_mode: bool, project_dir: Option<String>) -> Result<()> {
println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan());
println!("{}", " ZecKit - Running Smoke Tests".cyan().bold());
println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan());
println!();
let client = Client::new();
if let Err(e) = start_background_miner().await {
println!("{} {}", "WARN (non-fatal): Could not start background miner".yellow(), e);
}
let mut passed = 0;
let mut failed = 0;
let mut shield_txid = String::new();
let mut send_txid = String::new();
let mut faucet_address = String::new();
print!(" [0/7] Cluster synchronization... ");
match test_cluster_sync(&client).await {
Ok(_) => {
println!("{}", "PASS".green());
passed += 1;
}
Err(e) => {
println!("{} {}", "WARN (non-fatal)".yellow(), e);
passed += 1;
}
}
print!(" [1/7] Zebra RPC connectivity (Miner)... ");
match test_zebra_rpc(&client, 8232).await {
Ok(_) => {
println!("{}", "PASS".green());
passed += 1;
}
Err(e) => {
println!("{} {}", "FAIL".red(), e);
failed += 1;
}
}
print!(" [2/7] Faucet health check... ");
match test_faucet_health(&client).await {
Ok(_) => {
println!("{}", "PASS".green());
passed += 1;
}
Err(e) => {
println!("{} {}", "FAIL".red(), e);
failed += 1;
}
}
print!(" [3/7] Faucet address retrieval... ");
match test_faucet_address(&client).await {
Ok(addr) => {
println!("{}", "PASS".green());
faucet_address = addr;
passed += 1;
}
Err(e) => {
println!("{} {}", "FAIL".red(), e);
failed += 1;
}
}
print!(" [4/7] Wallet sync capability... ");
let mut sync_success = false;
let mut last_sync_error = String::new();
for i in 1..=3 {
match test_wallet_sync(&client).await {
Ok(_) => {
println!("{}", "PASS".green());
sync_success = true;
break;
}
Err(e) => {
last_sync_error = e.to_string();
if i < 3 {
print!("{} (retrying in 10s)... ", "LAG".yellow());
sleep(Duration::from_secs(10)).await;
}
}
}
}
if sync_success {
passed += 1;
} else {
println!("{} {}", "FAIL".red(), last_sync_error);
failed += 1;
}
print!(" [5/7] Wallet balance and shield... ");
match test_wallet_shield(&client).await {
Ok(txid) => {
println!("{}", "PASS".green());
shield_txid = txid;
passed += 1;
}
Err(e) => {
println!("{} {}", "FAIL".red(), e);
failed += 1;
}
}
print!(" [6/7] Shielded send (E2E)... ");
match test_shielded_send(&client, amount, memo).await {
Ok(txid) => {
println!("{}", "PASS".green());
send_txid = txid;
passed += 1;
}
Err(e) => {
println!("{} {}", "FAIL".red(), e);
failed += 1;
}
}
println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan());
println!(" Summary: {} passed, {} failed", passed, failed);
println!();
if action_mode {
let final_balance = get_wallet_balance_via_api(&client).await.ok();
let _ = save_faucet_stats_artifact(
action_mode,
&client,
project_dir.clone()
).await;
let _ = save_run_summary_artifact(
action_mode,
faucet_address,
shield_txid,
send_txid,
final_balance.map(|b| b.orchard).unwrap_or(0.0),
if failed == 0 { "pass" } else { "fail" },
project_dir.clone(),
).await;
}
if failed > 0 {
return Err(crate::error::ZecKitError::HealthCheck(
format!("{} test(s) failed", failed)
));
}
Ok(())
}
async fn save_faucet_stats_artifact(
action_mode: bool,
client: &Client,
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(|| crate::error::ZecKitError::Config("Could not find home directory".into()))?
.join(".zeckit")
};
let log_dir = project_dir.join("logs");
fs::create_dir_all(&log_dir).ok();
let stats_res = client
.get("http://127.0.0.1:8080/stats")
.send()
.await;
let stats_json = match stats_res {
Ok(resp) if resp.status().is_success() => {
match resp.json::<serde_json::Value>().await {
Ok(v) => v,
Err(_) => json!({"error": "Failed to parse stats response"}),
}
}
Ok(resp) => json!({"error": format!("Stats endpoint returned {}", resp.status())}),
Err(e) => json!({"error": format!("Could not reach faucet stats: {}", e)}),
};
let stats_path = log_dir.join("faucet-stats.json");
fs::write(&stats_path, serde_json::to_string_pretty(&stats_json)?).ok();
println!("✓ Saved {:?}", stats_path);
Ok(())
}
async fn save_run_summary_artifact(
action_mode: bool,
faucet_address: String,
shield_txid: String,
send_txid: String,
final_balance: f64,
test_result: &str,
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(|| crate::error::ZecKitError::Config("Could not find home directory".into()))?
.join(".zeckit")
};
let log_dir = project_dir.join("logs");
fs::create_dir_all(&log_dir).ok();
let summary = json!({
"faucet_address": faucet_address,
"shield_txid": shield_txid,
"send_txid": send_txid,
"final_balance": final_balance,
"test_result": test_result,
"timestamp": chrono::Utc::now().to_rfc3339(),
});
let summary_path = log_dir.join("run-summary.json");
fs::write(
&summary_path,
serde_json::to_string_pretty(&summary)?
).ok();
println!("✓ Saved {:?}", summary_path);
Ok(())
}
async fn test_zebra_rpc(client: &Client, port: u16) -> Result<()> {
let url = format!("http://127.0.0.1:{}", port);
let resp = client
.post(&url)
.json(&serde_json::json!({
"jsonrpc": "2.0",
"id": "test",
"method": "getblockcount",
"params": []
}))
.send()
.await?;
if !resp.status().is_success() {
return Err(crate::error::ZecKitError::HealthCheck(
format!("Zebra RPC on port {} not responding", port)
));
}
Ok(())
}
async fn test_cluster_sync(client: &Client) -> Result<()> {
let miner_resp = client
.post("http://127.0.0.1:8232")
.json(&serde_json::json!({
"jsonrpc": "2.0",
"id": "sync_test",
"method": "getblockcount",
"params": []
}))
.send()
.await?;
let miner_json: Value = miner_resp.json().await?;
let miner_height = miner_json.get("result").and_then(|v| v.as_u64()).unwrap_or(0);
let sync_resp = client
.post("http://127.0.0.1:18232")
.json(&serde_json::json!({
"jsonrpc": "2.0",
"id": "sync_test",
"method": "getblockcount",
"params": []
}))
.send()
.await?;
let sync_json: Value = sync_resp.json().await?;
let sync_height = sync_json.get("result").and_then(|v| v.as_u64()).unwrap_or(0);
if sync_height < miner_height {
return Err(crate::error::ZecKitError::HealthCheck(
format!("Sync node lagging: Miner={} Sync={}", miner_height, sync_height)
));
}
Ok(())
}
async fn test_faucet_health(client: &Client) -> Result<()> {
let resp = client
.get("http://127.0.0.1:8080/health")
.send()
.await?;
if !resp.status().is_success() {
return Err(crate::error::ZecKitError::HealthCheck(
"Faucet health check failed".into()
));
}
let json: Value = resp.json().await?;
if json.get("status").and_then(|v| v.as_str()) != Some("healthy") {
return Err(crate::error::ZecKitError::HealthCheck(
"Faucet not reporting healthy status".into()
));
}
Ok(())
}
async fn test_faucet_address(client: &Client) -> Result<String> {
let resp = client
.get("http://127.0.0.1:8080/address")
.send()
.await?;
if !resp.status().is_success() {
return Err(crate::error::ZecKitError::HealthCheck(
"Could not get faucet address".into()
));
}
let json: Value = resp.json().await?;
let ua = json.get("unified_address")
.and_then(|v| v.as_str())
.ok_or_else(|| crate::error::ZecKitError::HealthCheck(
"Missing unified address in response".into()
))?;
if json.get("transparent_address").is_none() {
return Err(crate::error::ZecKitError::HealthCheck(
"Missing transparent address in response".into()
));
}
Ok(ua.to_string())
}
async fn test_wallet_sync(client: &Client) -> Result<()> {
let resp = client
.post("http://127.0.0.1:8080/sync")
.send()
.await?;
if !resp.status().is_success() {
return Err(crate::error::ZecKitError::HealthCheck(
"Wallet sync failed".into()
));
}
let json: Value = resp.json().await?;
if json.get("status").and_then(|v| v.as_str()) != Some("synced") {
let err_part = json.get("error").and_then(|v| v.as_str()).unwrap_or("Wallet sync failed");
return Err(crate::error::ZecKitError::HealthCheck(err_part.to_string()));
}
Ok(())
}
async fn test_wallet_shield(client: &Client) -> Result<String> {
println!();
println!(" Checking wallet balance via API...");
let balance = get_wallet_balance_via_api(client).await?;
let transparent_before = balance.transparent;
let orchard_before = balance.orchard;
println!(" Transparent: {} ZEC", transparent_before);
println!(" Orchard: {} ZEC", orchard_before);
let min_shield_amount = 0.0002;
if transparent_before >= min_shield_amount {
println!(" Shielding {} ZEC to Orchard via API...", transparent_before);
let shield_resp = client
.post("http://127.0.0.1:8080/shield")
.send()
.await?;
if !shield_resp.status().is_success() {
let json: Value = shield_resp.json().await.unwrap_or(json!({"error": "Unknown error"}));
let error_text = json.get("error").and_then(|v| v.as_str()).unwrap_or("Unknown shielding error");
let helpful_tip = if error_text.contains("Insufficient balance") {
format!("\n {} Faucet shielding fails if you try to shield the entire balance. \n Wait 30s for more blocks to mine or try manual shielding with a margin.", "TIP:".blue().bold())
} else {
String::new()
};
return Err(crate::error::ZecKitError::HealthCheck(
format!("Shield API call failed: {}{}", error_text, helpful_tip)
));
}
let shield_json: Value = shield_resp.json().await?;
let status = shield_json.get("status").and_then(|v| v.as_str()).unwrap_or("unknown");
let txid = shield_json.get("txid").and_then(|v| v.as_str()).unwrap_or("").to_string();
match status {
"shielded" => {
if !txid.is_empty() {
println!(" Shield transaction broadcast!");
println!(" TXID: {}...", &txid[..16.min(txid.len())]);
}
println!(" Waiting for transaction to confirm (45s)...");
sleep(Duration::from_secs(45)).await;
println!(" Syncing wallet to update balance...");
let _ = client.post("http://127.0.0.1:8080/sync").send().await;
sleep(Duration::from_secs(5)).await;
let balance_after = get_wallet_balance_via_api(client).await?;
println!(" Balance after shield:");
println!(" Transparent: {} ZEC (was {})", balance_after.transparent, transparent_before);
println!(" Orchard: {} ZEC (was {})", balance_after.orchard, orchard_before);
if balance_after.orchard > orchard_before || balance_after.transparent < transparent_before {
println!(" Shield successful - funds moved!");
} else {
println!(" Shield transaction sent but balance not yet updated");
println!(" (May need more time to confirm)");
}
println!();
return Ok(txid);
}
"no_funds" => {
println!(" No transparent funds to shield (already shielded)");
println!();
return Ok(String::new());
}
_ => {
println!(" Shield status: {}", status);
if let Some(msg) = shield_json.get("message").and_then(|v| v.as_str()) {
println!(" Message: {}", msg);
}
println!();
return Ok(String::new());
}
}
} else if orchard_before >= 0.001 {
println!(" Wallet already has {} ZEC shielded in Orchard - PASS", orchard_before);
println!();
return Ok(String::new());
} else if transparent_before > 0.0 {
println!(" Wallet has {} ZEC transparent (too small to shield)", transparent_before);
println!(" Need at least {} ZEC to cover shield + fee", min_shield_amount);
println!(" FAIL (insufficient transparent balance)");
println!();
return Err(crate::error::ZecKitError::HealthCheck(
format!("Insufficient transparent balance for shielding: {} < {}", transparent_before, min_shield_amount)
));
} else {
println!(" No balance found");
println!(" FAIL (needs mining to complete)");
println!();
return Err(crate::error::ZecKitError::HealthCheck(
"No balance found for shielding".into()
));
}
}
#[derive(Debug)]
struct WalletBalance {
transparent: f64,
orchard: f64,
}
async fn get_wallet_balance_via_api(client: &Client) -> Result<WalletBalance> {
let resp = client
.get("http://127.0.0.1:8080/stats")
.send()
.await?;
if !resp.status().is_success() {
return Err(crate::error::ZecKitError::HealthCheck(
"Failed to get balance from stats endpoint".into()
));
}
let json: Value = resp.json().await?;
let transparent = json.get("transparent_balance")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
let orchard = json.get("orchard_balance")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
Ok(WalletBalance {
transparent,
orchard,
})
}
async fn test_shielded_send(client: &Client, amount: f64, memo: String) -> Result<String> {
println!();
println!(" Checking faucet Orchard balance...");
let balance = get_wallet_balance_via_api(client).await?;
if balance.orchard < amount {
println!(" Faucet has insufficient Orchard balance: {} ZEC", balance.orchard);
println!(" FAIL (need at least {} ZEC shielded)", amount);
println!();
return Err(crate::error::ZecKitError::HealthCheck(
format!("Insufficient Orchard balance: {} < {}", balance.orchard, amount)
));
}
println!(" Faucet Orchard balance: {} ZEC", balance.orchard);
println!(" Syncing wallet to ensure spendable balance...");
let _ = client.post("http://127.0.0.1:8080/sync").send().await;
sleep(Duration::from_secs(10)).await;
println!(" Getting recipient address...");
let addr_resp = client
.get("http://127.0.0.1:8080/address")
.send()
.await?;
if !addr_resp.status().is_success() {
return Err(crate::error::ZecKitError::HealthCheck(
"Failed to get recipient address".into()
));
}
let addr_json: Value = addr_resp.json().await?;
let recipient_address = addr_json.get("unified_address")
.and_then(|v| v.as_str())
.ok_or_else(|| crate::error::ZecKitError::HealthCheck(
"No unified address in response".into()
))?;
println!(" Recipient: {}...", &recipient_address[..20.min(recipient_address.len())]);
println!(" Sending {} ZEC (shielded)...", amount);
let send_resp = client
.post("http://127.0.0.1:8080/send")
.json(&serde_json::json!({
"address": recipient_address,
"amount": amount,
"memo": memo
}))
.send()
.await?;
if !send_resp.status().is_success() {
let error_text = send_resp.text().await.unwrap_or_else(|_| "Unknown error".to_string());
return Err(crate::error::ZecKitError::HealthCheck(
format!("Shielded send failed: {}", error_text)
));
}
let send_json: Value = send_resp.json().await?;
let status = send_json.get("status").and_then(|v| v.as_str());
if status == Some("sent") {
let txid = send_json.get("txid").and_then(|v| v.as_str()).unwrap_or("").to_string();
if !txid.is_empty() {
println!(" ✓ Shielded send successful!");
println!(" TXID: {}...", &txid[..16.min(txid.len())]);
}
if let Some(new_balance) = send_json.get("orchard_balance").and_then(|v| v.as_f64()) {
println!(" New Orchard balance: {} ZEC (was {})", new_balance, balance.orchard);
}
println!(" ✓ E2E Golden Flow Complete:");
println!(" - Faucet had shielded funds (Orchard)");
println!(" - Sent {} ZEC to recipient UA", amount);
println!(" - Transaction broadcast successfully");
println!();
return Ok(txid);
} else {
println!(" Unexpected status: {:?}", status);
if let Some(msg) = send_json.get("message").and_then(|v| v.as_str()) {
println!(" Message: {}", msg);
}
println!();
println!();
return Err(crate::error::ZecKitError::HealthCheck(
"Shielded send did not complete as expected".into()
));
}
}
async fn start_background_miner() -> Result<()> {
tokio::spawn(async {
let client = Client::new();
let mut interval = tokio::time::interval(Duration::from_secs(15));
loop {
interval.tick().await;
let _ = client
.post("http://127.0.0.1:8232")
.json(&serde_json::json!({
"jsonrpc": "2.0",
"id": "bgminer",
"method": "generate",
"params": [1]
}))
.timeout(Duration::from_secs(10))
.send()
.await;
}
});
Ok(())
}