]> Untitled Git - bdk-cli/commitdiff
ref(handlers): split handler fns into repl
authorVihiga Tyonum <withtvpeter@gmail.com>
Tue, 21 Apr 2026 10:25:55 +0000 (11:25 +0100)
committerVihiga Tyonum <withtvpeter@gmail.com>
Fri, 5 Jun 2026 16:12:20 +0000 (17:12 +0100)
- split handler fns into repl
- add entry point to the handlers subdir
- add wallet subdir entry point

src/handlers/mod.rs [new file with mode: 0644]
src/handlers/repl.rs [new file with mode: 0644]
src/wallet/mod.rs [new file with mode: 0644]

diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
new file mode 100644 (file)
index 0000000..f7a4737
--- /dev/null
@@ -0,0 +1,297 @@
+pub mod config;
+pub mod descriptor;
+pub mod key;
+pub mod offline;
+pub mod online;
+pub mod repl;
+pub mod wallets;
+
+#[cfg(feature = "repl")]
+use crate::handlers::repl::respond;
+use crate::{
+    commands::{CliOpts, CliSubCommand, WalletSubCommand},
+    error::BDKCliError as Error,
+    handlers::{
+        config::handle_config_subcommand, descriptor::handle_descriptor_command,
+        key::handle_key_subcommand, wallets::handle_wallets_subcommand,
+    },
+    utils::{load_wallet_config, prepare_home_dir},
+};
+
+#[cfg(any(feature = "sqlite", feature = "redb"))]
+use crate::utils::prepare_wallet_db_dir;
+#[cfg(not(any(feature = "sqlite", feature = "redb")))]
+use crate::wallet::new_wallet;
+
+#[cfg(feature = "compiler")]
+use {
+    crate::handlers::descriptor::handle_compile_subcommand, bdk_redb::Store as RedbStore,
+    std::sync::Arc,
+};
+
+#[cfg(feature = "repl")]
+use crate::handlers::repl::readline;
+
+#[cfg(any(feature = "sqlite", feature = "redb"))]
+use crate::commands::DatabaseType;
+use crate::handlers::offline::handle_offline_wallet_subcommand;
+use clap::CommandFactory;
+#[cfg(any(
+    feature = "electrum",
+    feature = "esplora",
+    feature = "rpc",
+    feature = "cbf",
+))]
+use {
+    crate::backend::new_blockchain_client, crate::handlers::online::handle_online_wallet_subcommand,
+};
+#[cfg(any(feature = "sqlite", feature = "redb"))]
+use {
+    crate::wallet::{new_persisted_wallet, persister::Persister},
+    bdk_wallet::rusqlite::Connection,
+    std::io::Write,
+};
+
+/// The global top level handler.
+pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
+    let pretty = cli_opts.pretty;
+    let subcommand = cli_opts.subcommand.clone();
+
+    let result: Result<String, Error> = match subcommand {
+        #[cfg(any(
+            feature = "electrum",
+            feature = "esplora",
+            feature = "cbf",
+            feature = "rpc"
+        ))]
+        CliSubCommand::Wallet {
+            wallet,
+            subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand),
+        } => {
+            let home_dir = prepare_home_dir(cli_opts.datadir)?;
+
+            let (wallet_opts, network) = load_wallet_config(&home_dir, &wallet)?;
+
+            let database_path = prepare_wallet_db_dir(&home_dir, &wallet)?;
+
+            #[cfg(any(feature = "sqlite", feature = "redb"))]
+            let result = {
+                #[cfg(feature = "sqlite")]
+                let mut persister: Persister = match &wallet_opts.database_type {
+                    #[cfg(feature = "sqlite")]
+                    DatabaseType::Sqlite => {
+                        let db_file = database_path.join("wallet.sqlite");
+                        let connection = Connection::open(db_file)?;
+                        log::debug!("Sqlite database opened successfully");
+                        Persister::Connection(connection)
+                    }
+                    #[cfg(feature = "redb")]
+                    DatabaseType::Redb => {
+                        let wallet_name = &wallet_opts.wallet;
+                        let db = Arc::new(bdk_redb::redb::Database::create(
+                            home_dir.join("wallet.redb"),
+                        )?);
+                        let store = RedbStore::new(
+                            db,
+                            wallet_name.as_deref().unwrap_or("wallet").to_string(),
+                        )?;
+                        log::debug!("Redb database opened successfully");
+                        Persister::RedbStore(store)
+                    }
+                };
+
+                let mut wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?;
+                let blockchain_client =
+                    new_blockchain_client(&wallet_opts, &wallet, database_path)?;
+
+                let result = handle_online_wallet_subcommand(
+                    &mut wallet,
+                    &blockchain_client,
+                    online_subcommand,
+                )
+                .await?;
+                wallet.persist(&mut persister)?;
+                result
+            };
+            #[cfg(not(any(feature = "sqlite", feature = "redb")))]
+            let result = {
+                let mut wallet = new_wallet(network, &wallet_opts)?;
+                let blockchain_client =
+                    new_blockchain_client(&wallet_opts, &wallet, database_path)?;
+                handle_online_wallet_subcommand(&mut wallet, &blockchain_client, online_subcommand)
+                    .await?
+            };
+            Ok(result)
+        }
+        CliSubCommand::Wallet {
+            wallet: wallet_name,
+            subcommand: WalletSubCommand::OfflineWalletSubCommand(offline_subcommand),
+        } => {
+            let datadir = cli_opts.datadir.clone();
+            let home_dir = prepare_home_dir(datadir)?;
+            let (wallet_opts, network) = load_wallet_config(&home_dir, &wallet_name)?;
+
+            #[cfg(any(feature = "sqlite", feature = "redb"))]
+            let result = {
+                let mut persister: Persister = match &wallet_opts.database_type {
+                    #[cfg(feature = "sqlite")]
+                    DatabaseType::Sqlite => {
+                        let database_path = prepare_wallet_db_dir(&home_dir, &wallet_name)?;
+                        let db_file = database_path.join("wallet.sqlite");
+                        let connection = Connection::open(db_file)?;
+                        log::debug!("Sqlite database opened successfully");
+                        Persister::Connection(connection)
+                    }
+                    #[cfg(feature = "redb")]
+                    DatabaseType::Redb => {
+                        let db = Arc::new(bdk_redb::redb::Database::create(
+                            home_dir.join("wallet.redb"),
+                        )?);
+                        let store = RedbStore::new(db, wallet_name)?;
+                        log::debug!("Redb database opened successfully");
+                        Persister::RedbStore(store)
+                    }
+                };
+
+                let mut wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?;
+
+                let result = handle_offline_wallet_subcommand(
+                    &mut wallet,
+                    &wallet_opts,
+                    &cli_opts,
+                    offline_subcommand.clone(),
+                )?;
+                wallet.persist(&mut persister)?;
+                result
+            };
+            #[cfg(not(any(feature = "sqlite", feature = "redb")))]
+            let result = {
+                let mut wallet = new_wallet(network, &wallet_opts)?;
+                handle_offline_wallet_subcommand(
+                    &mut wallet,
+                    &wallet_opts,
+                    &cli_opts,
+                    offline_subcommand.clone(),
+                )?
+            };
+            Ok(result)
+        }
+        CliSubCommand::Wallet {
+            wallet,
+            subcommand: WalletSubCommand::Config { force, wallet_opts },
+        } => {
+            let network = cli_opts.network;
+            let home_dir = prepare_home_dir(cli_opts.datadir)?;
+            let result = handle_config_subcommand(&home_dir, network, wallet, &wallet_opts, force)?;
+            Ok(result)
+        }
+        CliSubCommand::Wallets => {
+            let home_dir = prepare_home_dir(cli_opts.datadir)?;
+            let result = handle_wallets_subcommand(&home_dir, pretty)?;
+            Ok(result)
+        }
+        CliSubCommand::Key {
+            subcommand: key_subcommand,
+        } => {
+            let network = cli_opts.network;
+            let result = handle_key_subcommand(network, key_subcommand, pretty)?;
+            Ok(result)
+        }
+        #[cfg(feature = "compiler")]
+        CliSubCommand::Compile {
+            policy,
+            script_type,
+        } => {
+            let network = cli_opts.network;
+            let result = handle_compile_subcommand(network, policy, script_type, pretty)?;
+            Ok(result)
+        }
+        #[cfg(feature = "repl")]
+        CliSubCommand::Repl {
+            wallet: wallet_name,
+        } => {
+            let home_dir = prepare_home_dir(cli_opts.datadir.clone())?;
+            let (wallet_opts, network) = load_wallet_config(&home_dir, &wallet_name)?;
+
+            #[cfg(any(feature = "sqlite", feature = "redb"))]
+            let (mut wallet, mut persister) = {
+                let mut persister: Persister = match &wallet_opts.database_type {
+                    #[cfg(feature = "sqlite")]
+                    DatabaseType::Sqlite => {
+                        let database_path = prepare_wallet_db_dir(&home_dir, &wallet_name)?;
+                        let db_file = database_path.join("wallet.sqlite");
+                        let connection = Connection::open(db_file)?;
+                        log::debug!("Sqlite database opened successfully");
+                        Persister::Connection(connection)
+                    }
+                    #[cfg(feature = "redb")]
+                    DatabaseType::Redb => {
+                        let db = Arc::new(bdk_redb::redb::Database::create(
+                            home_dir.join("wallet.redb"),
+                        )?);
+                        let store = RedbStore::new(db, wallet_name.clone())?;
+                        log::debug!("Redb database opened successfully");
+                        Persister::RedbStore(store)
+                    }
+                };
+                let wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?;
+                (wallet, persister)
+            };
+            #[cfg(not(any(feature = "sqlite", feature = "redb")))]
+            let mut wallet = new_wallet(network, &loaded_wallet_opts)?;
+            let home_dir = prepare_home_dir(cli_opts.datadir.clone())?;
+            let database_path = prepare_wallet_db_dir(&home_dir, &wallet_name)?;
+            loop {
+                let line = readline()?;
+                let line = line.trim();
+                if line.is_empty() {
+                    continue;
+                }
+
+                let result = respond(
+                    network,
+                    &mut wallet,
+                    &wallet_name,
+                    &mut wallet_opts.clone(),
+                    line,
+                    database_path.clone(),
+                    &cli_opts,
+                )
+                .await;
+                #[cfg(any(feature = "sqlite", feature = "redb"))]
+                wallet.persist(&mut persister)?;
+
+                match result {
+                    Ok(quit) => {
+                        if quit {
+                            break;
+                        }
+                    }
+                    Err(err) => {
+                        writeln!(std::io::stdout(), "{err}")
+                            .map_err(|e| Error::Generic(e.to_string()))?;
+                        std::io::stdout()
+                            .flush()
+                            .map_err(|e| Error::Generic(e.to_string()))?;
+                    }
+                }
+            }
+            Ok("".to_string())
+        }
+        CliSubCommand::Descriptor { desc_type, key } => {
+            let descriptor = handle_descriptor_command(cli_opts.network, desc_type, key, pretty)?;
+            Ok(descriptor)
+        }
+        CliSubCommand::Completions { shell } => {
+            clap_complete::generate(
+                shell,
+                &mut CliOpts::command(),
+                "bdk-cli",
+                &mut std::io::stdout(),
+            );
+
+            Ok("".to_string())
+        }
+    };
+    result
+}
diff --git a/src/handlers/repl.rs b/src/handlers/repl.rs
new file mode 100644 (file)
index 0000000..432af01
--- /dev/null
@@ -0,0 +1,111 @@
+#[cfg(any(
+    feature = "electrum",
+    feature = "esplora",
+    feature = "cbf",
+    feature = "rpc"
+))]
+use crate::{backend::new_blockchain_client, handlers::online::handle_online_wallet_subcommand};
+
+#[cfg(feature = "sqlite")]
+use crate::commands::ReplSubCommand;
+#[cfg(feature = "repl")]
+use {
+    crate::error::BDKCliError as Error,
+    crate::{
+        commands::{CliOpts, WalletOpts, WalletSubCommand},
+        handlers::{
+            config::handle_config_subcommand, descriptor::handle_descriptor_command,
+            key::handle_key_subcommand, offline::handle_offline_wallet_subcommand,
+        },
+    },
+    bdk_wallet::{Wallet, bitcoin::Network},
+    std::io::Write,
+};
+
+#[cfg(feature = "repl")]
+pub(crate) async fn respond(
+    network: Network,
+    wallet: &mut Wallet,
+    wallet_name: &String,
+    wallet_opts: &mut WalletOpts,
+    line: &str,
+    _datadir: std::path::PathBuf,
+    cli_opts: &CliOpts,
+) -> Result<bool, String> {
+    use clap::Parser;
+
+    let args = shlex::split(line).ok_or("error: Invalid quoting".to_string())?;
+    let repl_subcommand = ReplSubCommand::try_parse_from(args).map_err(|e| e.to_string())?;
+    let response = match repl_subcommand {
+        #[cfg(any(
+            feature = "electrum",
+            feature = "esplora",
+            feature = "cbf",
+            feature = "rpc"
+        ))]
+        ReplSubCommand::Wallet {
+            subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand),
+        } => {
+            let blockchain =
+                new_blockchain_client(wallet_opts, wallet, _datadir).map_err(|e| e.to_string())?;
+            let value = handle_online_wallet_subcommand(wallet, &blockchain, online_subcommand)
+                .await
+                .map_err(|e| e.to_string())?;
+            Some(value)
+        }
+        ReplSubCommand::Wallet {
+            subcommand: WalletSubCommand::OfflineWalletSubCommand(offline_subcommand),
+        } => {
+            let value =
+                handle_offline_wallet_subcommand(wallet, wallet_opts, cli_opts, offline_subcommand)
+                    .map_err(|e| e.to_string())?;
+            Some(value)
+        }
+        ReplSubCommand::Wallet {
+            subcommand: WalletSubCommand::Config { force, wallet_opts },
+        } => {
+            let value = handle_config_subcommand(
+                &_datadir,
+                network,
+                wallet_name.to_string(),
+                &wallet_opts,
+                force,
+            )
+            .map_err(|e| e.to_string())?;
+            Some(value)
+        }
+        ReplSubCommand::Key { subcommand } => {
+            let value = handle_key_subcommand(network, subcommand, cli_opts.pretty)
+                .map_err(|e| e.to_string())?;
+            Some(value)
+        }
+        ReplSubCommand::Descriptor { desc_type, key } => {
+            let value = handle_descriptor_command(network, desc_type, key, cli_opts.pretty)
+                .map_err(|e| e.to_string())?;
+            Some(value)
+        }
+        ReplSubCommand::Exit => None,
+    };
+    if let Some(value) = response {
+        writeln!(std::io::stdout(), "{value}").map_err(|e| e.to_string())?;
+        std::io::stdout().flush().map_err(|e| e.to_string())?;
+        Ok(false)
+    } else {
+        writeln!(std::io::stdout(), "Exiting...").map_err(|e| e.to_string())?;
+        std::io::stdout().flush().map_err(|e| e.to_string())?;
+        Ok(true)
+    }
+}
+
+#[cfg(feature = "repl")]
+pub(crate) fn readline() -> Result<String, Error> {
+    write!(std::io::stdout(), "> ").map_err(|e| Error::Generic(e.to_string()))?;
+    std::io::stdout()
+        .flush()
+        .map_err(|e| Error::Generic(e.to_string()))?;
+    let mut buffer = String::new();
+    std::io::stdin()
+        .read_line(&mut buffer)
+        .map_err(|e| Error::Generic(e.to_string()))?;
+    Ok(buffer)
+}
diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs
new file mode 100644 (file)
index 0000000..7ef2e65
--- /dev/null
@@ -0,0 +1,74 @@
+use crate::commands::WalletOpts;
+use crate::error::BDKCliError as Error;
+use bdk_wallet::Wallet;
+use bdk_wallet::bitcoin::Network;
+#[cfg(any(feature = "sqlite", feature = "redb"))]
+use bdk_wallet::{KeychainKind, PersistedWallet, WalletPersister};
+
+#[cfg(any(feature = "sqlite", feature = "redb"))]
+pub mod persister;
+
+#[cfg(any(feature = "sqlite", feature = "redb"))]
+pub(crate) fn new_persisted_wallet<P: WalletPersister>(
+    network: Network,
+    persister: &mut P,
+    wallet_opts: &WalletOpts,
+) -> Result<PersistedWallet<P>, Error>
+where
+    P::Error: std::fmt::Display,
+{
+    let ext_descriptor = wallet_opts.ext_descriptor.clone();
+    let int_descriptor = wallet_opts.int_descriptor.clone();
+
+    let mut wallet_load_params = Wallet::load();
+    wallet_load_params =
+        wallet_load_params.descriptor(KeychainKind::External, Some(ext_descriptor.clone()));
+
+    if int_descriptor.is_some() {
+        wallet_load_params =
+            wallet_load_params.descriptor(KeychainKind::Internal, int_descriptor.clone());
+    }
+    wallet_load_params = wallet_load_params.extract_keys();
+
+    let wallet_opt = wallet_load_params
+        .check_network(network)
+        .load_wallet(persister)
+        .map_err(|e| Error::Generic(e.to_string()))?;
+
+    let wallet = match wallet_opt {
+        Some(wallet) => wallet,
+        None => match int_descriptor {
+            Some(int_descriptor) => Wallet::create(ext_descriptor, int_descriptor)
+                .network(network)
+                .create_wallet(persister)
+                .map_err(|e| Error::Generic(e.to_string()))?,
+            None => Wallet::create_single(ext_descriptor)
+                .network(network)
+                .create_wallet(persister)
+                .map_err(|e| Error::Generic(e.to_string()))?,
+        },
+    };
+
+    Ok(wallet)
+}
+
+#[cfg(not(any(feature = "sqlite", feature = "redb")))]
+pub(crate) fn new_wallet(network: Network, wallet_opts: &WalletOpts) -> Result<Wallet, Error> {
+    let ext_descriptor = wallet_opts.ext_descriptor.clone();
+    let int_descriptor = wallet_opts.int_descriptor.clone();
+
+    match int_descriptor {
+        Some(int_descriptor) => {
+            let wallet = Wallet::create(ext_descriptor, int_descriptor)
+                .network(network)
+                .create_wallet_no_persist()?;
+            Ok(wallet)
+        }
+        None => {
+            let wallet = Wallet::create_single(ext_descriptor)
+                .network(network)
+                .create_wallet_no_persist()?;
+            Ok(wallet)
+        }
+    }
+}