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, ScenarioAction, ScenarioArgs};
use crate::error::AppError;
use crate::output::CommandOutput;
use crate::utils::run_bitcoin_cli;
use crate::wallet_service::NetworkArg;
use crate::{confirm, load_wallet_session, persist_wallet_session, snapshot_dir};
use std::fs;

pub async fn run(cli: &Cli, args: &ScenarioArgs) -> Result<CommandOutput, AppError> {
    let mut session = load_wallet_session(cli)?;
    if !matches!(session.profile.network, NetworkArg::Regtest) {
        return Err(AppError::Invalid(
            "scenario commands are only supported on regtest profiles".to_string(),
        ));
    }

    let mut should_persist = true;
    let result = match &args.action {
        ScenarioAction::Mine { blocks, address } => {
            if *blocks == 0 {
                return Err(AppError::Invalid(
                    "--blocks must be greater than 0".to_string(),
                ));
            }

            let mining_address = if let Some(address) = address {
                address.clone()
            } else {
                session.wallet.peek_taproot_address(0).to_string()
            };

            let generated = run_bitcoin_cli(
                &session.profile,
                &[
                    "generatetoaddress".to_string(),
                    blocks.to_string(),
                    mining_address.clone(),
                ],
            )?;

            CommandOutput::ScenarioMine {
                blocks: u64::from(*blocks),
                address: mining_address,
                raw_output: generated,
            }
        }
        ScenarioAction::Fund {
            address,
            amount_btc,
            mine_blocks,
        } => {
            if *mine_blocks == 0 {
                return Err(AppError::Invalid(
                    "--mine-blocks must be greater than 0".to_string(),
                ));
            }
            let destination = if let Some(address) = address {
                address.clone()
            } else {
                session.wallet.peek_taproot_address(0).to_string()
            };

            let txid = run_bitcoin_cli(
                &session.profile,
                &[
                    "sendtoaddress".to_string(),
                    destination.clone(),
                    amount_btc.clone(),
                ],
            )?;

            let mine_address = session.wallet.peek_taproot_address(0).to_string();
            let generated = run_bitcoin_cli(
                &session.profile,
                &[
                    "generatetoaddress".to_string(),
                    mine_blocks.to_string(),
                    mine_address.clone(),
                ],
            )?;

            CommandOutput::ScenarioFund {
                address: destination,
                amount_btc: amount_btc.clone(),
                txid: txid.trim().to_string(),
                mine_blocks: u64::from(*mine_blocks),
                mine_address,
                generated_blocks: generated,
            }
        }
        ScenarioAction::Reset {
            remove_profile,
            remove_snapshots,
        } => {
            if !confirm("Are you sure you want to reset the scenario? This may delete your profile and/or snapshots.", cli) {
                return Err(AppError::Internal("aborted by user".to_string()));
            }
            should_persist = false;
            let mut removed = Vec::new();
            if (*remove_profile || (!remove_profile && !remove_snapshots))
                && session.profile_path.exists()
            {
                fs::remove_file(&session.profile_path).map_err(|e| {
                    AppError::Config(format!(
                        "failed to remove profile {}: {e}",
                        session.profile_path.display()
                    ))
                })?;
                removed.push(session.profile_path.display().to_string());
            }
            if *remove_snapshots || (!remove_profile && !remove_snapshots) {
                let snap_dir = snapshot_dir(cli)?;
                if snap_dir.exists() {
                    fs::remove_dir_all(&snap_dir).map_err(|e| {
                        AppError::Config(format!(
                            "failed to remove snapshots {}: {e}",
                            snap_dir.display()
                        ))
                    })?;
                    removed.push(snap_dir.display().to_string());
                }
            }

            CommandOutput::ScenarioReset { removed }
        }
    };

    if should_persist {
        persist_wallet_session(&mut session)?;
    }
    Ok(result)
}