From: Vihiga Tyonum Date: Tue, 21 Apr 2026 10:25:55 +0000 (+0100) Subject: ref(handlers): split handler fns into repl X-Git-Url: http://internal-gitweb-vhost/blockdata/script/encode/-script/display/struct.WScriptHash.html?a=commitdiff_plain;h=0bf3e66c7f733fed152171e0dab63f070d24ff10;p=bdk-cli ref(handlers): split handler fns into repl - split handler fns into repl - add entry point to the handlers subdir - add wallet subdir entry point --- diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..f7a4737 --- /dev/null +++ b/src/handlers/mod.rs @@ -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 { + let pretty = cli_opts.pretty; + let subcommand = cli_opts.subcommand.clone(); + + let result: Result = 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 index 0000000..432af01 --- /dev/null +++ b/src/handlers/repl.rs @@ -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 { + 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 { + 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 index 0000000..7ef2e65 --- /dev/null +++ b/src/wallet/mod.rs @@ -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( + network: Network, + persister: &mut P, + wallet_opts: &WalletOpts, +) -> Result, 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 { + 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) + } + } +}