bdk-cli 4.0.0

An experimental CLI wallet application and playground, powered by BDK
use std::collections::HashMap;

#[cfg(any(
    feature = "electrum",
    feature = "esplora",
    feature = "rpc",
    feature = "cbf"
))]
use crate::client::ClientType;
use crate::commands::WalletOpts;
use crate::config::{WalletConfig, WalletConfigInner};
use crate::error::BDKCliError as Error;
use crate::handlers::Init;
use crate::handlers::{AppCommand, AppContext};
#[cfg(any(feature = "sqlite", feature = "redb"))]
use crate::persister::DatabaseType;
use crate::utils::types::{StatusResult, WalletsListResult};
use bdk_wallet::bitcoin::Network;
use clap::Args;

#[derive(Args, Debug, Clone, PartialEq)]
pub struct SaveConfigCommand {
    /// Overwrite existing wallet configuration if it exists.
    #[arg(short = 'f', long = "force", default_value_t = false)]
    pub(crate) force: bool,

    #[command(flatten)]
    pub(crate) wallet_opts: WalletOpts,
}

impl AppCommand<AppContext<Init>> for SaveConfigCommand {
    type Output = StatusResult;

    fn execute(&self, ctx: &mut AppContext<Init>) -> Result<Self::Output, Error> {
        if ctx.network == Network::Bitcoin {
            eprintln!("WARNING: Configuring for Bitcoin MAINNET. Experimental software!");
        }

        let wallet_name = match &self.wallet_opts.wallet {
            Some(wallet) => wallet,
            None => return Err(Error::Generic("wallet is required".to_owned())),
        };

        let ext_descriptor = self.wallet_opts.ext_descriptor.clone();
        let int_descriptor = self.wallet_opts.int_descriptor.clone();

        if ext_descriptor.contains("xprv") || ext_descriptor.contains("tprv") {
            eprintln!(
                "WARNING: Your external descriptor contains PRIVATE KEYS.
             Private keys will be saved in PLAINTEXT in the config file.
             This is a security risk. Consider using public descriptors instead.\n"
            );
        }

        if let Some(ref internal_desc) = int_descriptor
            && (internal_desc.contains("xprv") || internal_desc.contains("tprv"))
        {
            eprintln!(
                "WARNING: Your internal descriptor contains PRIVATE KEYS.
                 Private keys will be saved in PLAINTEXT in the config file.
                 This is a security risk. Consider using public descriptors instead.\n"
            );
        }

        let mut config = WalletConfig::load(&ctx.datadir)?.unwrap_or(WalletConfig {
            wallets: HashMap::new(),
        });

        if config.wallets.contains_key(wallet_name.as_str()) && !self.force {
            return Err(Error::Generic(format!(
                "Wallet '{}' already exists. Use --force to overwrite.",
                wallet_name
            )));
        };

        #[cfg(any(
            feature = "electrum",
            feature = "esplora",
            feature = "rpc",
            feature = "cbf"
        ))]
        let client_type = match self.wallet_opts.client_type.clone() {
            #[cfg(feature = "electrum")]
            ClientType::Electrum => "electrum".to_string(),
            #[cfg(feature = "esplora")]
            ClientType::Esplora => "esplora".to_string(),
            #[cfg(feature = "rpc")]
            ClientType::Rpc => "rpc".to_string(),
            #[cfg(feature = "cbf")]
            ClientType::Cbf => "cbf".to_string(),
        };

        let wallet_config = WalletConfigInner {
            wallet: wallet_name.clone(),
            network: ctx.network.to_string(),
            ext_descriptor: self.wallet_opts.ext_descriptor.clone(),
            int_descriptor: self.wallet_opts.int_descriptor.clone(),
            #[cfg(any(feature = "sqlite", feature = "redb"))]
            database_type: match self.wallet_opts.database_type {
                #[cfg(feature = "sqlite")]
                DatabaseType::Sqlite => "sqlite".to_string(),
                #[cfg(feature = "redb")]
                DatabaseType::Redb => "redb".to_string(),
            },

            #[cfg(any(
                feature = "electrum",
                feature = "esplora",
                feature = "rpc",
                feature = "cbf"
            ))]
            client_type: Some(client_type),

            #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
            server_url: Some(self.wallet_opts.url.clone()),

            #[cfg(feature = "rpc")]
            rpc_user: Some(self.wallet_opts.basic_auth.0.clone()),
            #[cfg(feature = "rpc")]
            rpc_password: Some(self.wallet_opts.basic_auth.1.clone()),
            #[cfg(feature = "electrum")]
            batch_size: Some(self.wallet_opts.batch_size),
            #[cfg(feature = "esplora")]
            parallel_requests: Some(self.wallet_opts.parallel_requests),
            #[cfg(feature = "rpc")]
            cookie: self.wallet_opts.cookie.clone(),

            #[cfg(any(feature = "electrum", feature = "esplora"))]
            proxy: self.wallet_opts.proxy_opts.proxy.clone(),
            #[cfg(any(feature = "electrum", feature = "esplora"))]
            proxy_auth: self
                .wallet_opts
                .proxy_opts
                .proxy_auth
                .as_ref()
                .map(|(u, p)| format!("{u}:{p}")),
            #[cfg(any(feature = "electrum", feature = "esplora"))]
            proxy_retries: Some(self.wallet_opts.proxy_opts.retries),
            #[cfg(any(feature = "electrum", feature = "esplora"))]
            proxy_timeout: self.wallet_opts.proxy_opts.timeout,
            #[cfg(feature = "cbf")]
            conn_count: Some(self.wallet_opts.compactfilter_opts.conn_count),
        };

        config.wallets.insert(wallet_name.clone(), wallet_config);
        config
            .save(&ctx.datadir)
            .map_err(|error| Error::Generic(error.to_string()))?;

        Ok(StatusResult {
            message: format!(
                "Wallet '{}' initialized successfully in {:?}",
                wallet_name,
                ctx.datadir.join("config.toml")
            ),
        })
    }
}

#[derive(Args, Debug, Clone, PartialEq)]
pub struct ListWalletsCommand;

impl AppCommand<AppContext<Init>> for ListWalletsCommand {
    type Output = WalletsListResult;

    fn execute(&self, ctx: &mut AppContext<Init>) -> Result<Self::Output, Error> {
        let config = match WalletConfig::load(&ctx.datadir)? {
            Some(cfg) => cfg,
            None => return Err(Error::Generic("No wallets configured yet.".into())),
        };

        Ok(WalletsListResult(config.wallets))
    }
}