zinc-wallet-cli 0.4.0

Agent-first Bitcoin + Ordinals CLI wallet with account-based taproot ordinals + native segwit payment addresses (optional human mode)
use crate::cli::{Cli, WaitAction, WaitArgs};
use crate::error::AppError;
use crate::load_wallet_session;
use crate::network_retry::with_network_retry;
use crate::output::CommandOutput;
use crate::wallet_service::map_wallet_error;
use indicatif::{ProgressBar, ProgressStyle};
use std::time::Duration;
use tokio::time::sleep;

pub async fn run(cli: &Cli, args: &WaitArgs) -> Result<CommandOutput, AppError> {
    let mut session = load_wallet_session(cli)?;

    let spinner = if cli.agent {
        None
    } else {
        let pb = ProgressBar::new_spinner();
        pb.set_style(
            ProgressStyle::default_spinner()
                .template("{spinner:.green} {msg}")
                .unwrap(),
        );
        pb.enable_steady_tick(std::time::Duration::from_millis(100));
        Some(pb)
    };

    match &args.action {
        WaitAction::TxConfirmed {
            txid,
            timeout_secs,
            poll_secs,
        } => {
            if let Some(ref pb) = spinner {
                pb.set_message(format!("Waiting for tx {}...", txid));
            }
            let start = std::time::Instant::now();
            let timeout = Duration::from_secs(*timeout_secs);
            let poll = Duration::from_secs(*poll_secs);
            let esplora_url = session.profile.esplora_url.clone();

            loop {
                let _: Vec<String> = with_network_retry(
                    cli,
                    "wait tx-confirmed sync",
                    &mut session.wallet,
                    |wallet| {
                        let url = esplora_url.clone();
                        Box::pin(async move { wallet.sync(&url).await.map_err(map_wallet_error) })
                    },
                )
                .await?;

                let txs = session.wallet.get_transactions(1000);
                let confirmation_time = txs
                    .iter()
                    .find(|t| t.txid == **txid)
                    .and_then(|t| t.confirmation_time);

                if confirmation_time.is_some() {
                    if let Some(pb) = spinner {
                        pb.finish_with_message("Transaction confirmed!");
                    }
                    return Ok(CommandOutput::WaitTxConfirmed {
                        txid: txid.clone(),
                        confirmation_time,
                        confirmed: true,
                        waited_secs: start.elapsed().as_secs(),
                    });
                }

                if start.elapsed() >= timeout {
                    return Err(AppError::Internal(format!(
                        "timed out waiting for tx {txid} after {timeout_secs}s"
                    )));
                }

                sleep(poll).await;
            }
        }
        WaitAction::Balance {
            confirmed_at_least,
            timeout_secs,
            poll_secs,
        } => {
            if *confirmed_at_least == 0 {
                return Ok(CommandOutput::WaitBalance {
                    confirmed: 0,
                    confirmed_balance: 0,
                    target: *confirmed_at_least,
                    waited_secs: 0,
                });
            }

            if let Some(ref pb) = spinner {
                pb.set_message(format!("Waiting for balance {}...", confirmed_at_least));
            }
            let start = std::time::Instant::now();
            let timeout = Duration::from_secs(*timeout_secs);
            let poll = Duration::from_secs(*poll_secs);
            let esplora_url = session.profile.esplora_url.clone();

            loop {
                let _: Vec<String> =
                    with_network_retry(cli, "wait balance sync", &mut session.wallet, |wallet| {
                        let url = esplora_url.clone();
                        Box::pin(async move { wallet.sync(&url).await.map_err(map_wallet_error) })
                    })
                    .await?;

                let confirmed = session.wallet.get_balance().total.confirmed.to_sat();
                if confirmed >= *confirmed_at_least {
                    if let Some(pb) = spinner {
                        pb.finish_with_message("Balance reached!");
                    }
                    return Ok(CommandOutput::WaitBalance {
                        confirmed,
                        confirmed_balance: confirmed,
                        target: *confirmed_at_least,
                        waited_secs: start.elapsed().as_secs(),
                    });
                }

                if start.elapsed() >= timeout {
                    return Err(AppError::Internal(format!(
                        "timed out waiting for balance {confirmed_at_least} after {timeout_secs}s"
                    )));
                }

                sleep(poll).await;
            }
        }
    }
}