From: Vihiga Tyonum Date: Sun, 31 May 2026 11:00:52 +0000 (+0100) Subject: refactor(main): add runtime wallet module X-Git-Url: http://internal-gitweb-vhost/blockdata/script/encode/-script/display/struct.ScriptHash.html?a=commitdiff_plain;h=aa8876d192d773e9f431bebbc6397f78868661aa;p=bdk-cli refactor(main): add runtime wallet module - add wallet runtime module to serve as context manager - fix clippy errors --- diff --git a/src/client.rs b/src/client.rs index c90aab2..ad51391 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,5 +1,3 @@ -#[cfg(feature = "rpc")] -use bdk_bitcoind_rpc::{Emitter, bitcoincore_rpc::RpcApi}; #[cfg(feature = "esplora")] use bdk_esplora::EsploraAsyncExt; #[cfg(any( @@ -14,11 +12,15 @@ use { bdk_wallet::{ Wallet, bitcoin::{Transaction, Txid}, - chain::CanonicalizationParams, }, clap::ValueEnum, std::path::PathBuf, }; +#[cfg(feature = "rpc")] +use { + bdk_bitcoind_rpc::{Emitter, bitcoincore_rpc::RpcApi}, + bdk_wallet::chain::CanonicalizationParams, +}; #[cfg(feature = "cbf")] use { @@ -79,7 +81,7 @@ pub(crate) enum BlockchainClient { impl BlockchainClient { pub async fn broadcast(&self, tx: Transaction) -> Result { match self { - // #[cfg(feature = "electrum")] + #[cfg(feature = "electrum")] Self::Electrum { client, .. } => client .transaction_broadcast(&tx) .map_err(|e| Error::Generic(e.to_string())), diff --git a/src/commands.rs b/src/commands.rs index 226e447..cc4e121 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -116,7 +116,7 @@ pub enum CliSubCommand { #[cfg(feature = "compiler")] #[clap(long_about = "Miniscript policy compiler")] Compile(CompileCommand), - // #[cfg(feature = "repl")] + #[cfg(feature = "repl")] /// REPL command loop mode. /// /// REPL command loop can be used to make recurring callbacks to an already loaded wallet. diff --git a/src/error.rs b/src/error.rs index 3019e5c..1d5fd82 100644 --- a/src/error.rs +++ b/src/error.rs @@ -61,7 +61,7 @@ pub enum BDKCliError { #[error("PsbtError: {0}")] PsbtError(#[from] bdk_wallet::bitcoin::psbt::Error), - // #[cfg(feature = "sqlite")] + #[cfg(feature = "sqlite")] #[error("Rusqlite error: {0}")] RusqliteError(Box), diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index d63ec3b..6e24339 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -102,12 +102,6 @@ pub trait AppCommand { ))] pub trait AsyncAppCommand { type Output: FormatOutput; - + async fn execute(&self, ctx: &mut C) -> Result; } - -// context for online and online -// => cli.rs -// handlers/{mod for commands} -// wallet subdir / -// wallet-offline and wallet-online (client mod) diff --git a/src/handlers/offline.rs b/src/handlers/offline.rs index c8bb18f..d45a772 100644 --- a/src/handlers/offline.rs +++ b/src/handlers/offline.rs @@ -522,7 +522,7 @@ impl AppCommand>> for CombinePsbtCommand { } } -// #[cfg(feature = "bip322")] +#[cfg(feature = "bip322")] #[derive(Debug, Parser, Clone, PartialEq)] pub struct SignMessageCommand { /// The message to sign @@ -572,7 +572,7 @@ impl AppCommand>> for SignMessageCommand { } } -// #[cfg(feature = "bip322")] +#[cfg(feature = "bip322")] #[derive(Debug, Parser, Clone, PartialEq)] pub struct VerifyMessageCommand { /// The signature proof to verify diff --git a/src/handlers/online.rs b/src/handlers/online.rs index 0c44c34..adb6268 100644 --- a/src/handlers/online.rs +++ b/src/handlers/online.rs @@ -170,7 +170,9 @@ impl AsyncAppCommand>> for FullScanCommand { sync_kyoto_client(wallet, client).await?; } } - Ok(StatusResult::new("Full scan completed successfully.")) + Ok(StatusResult { + message: "Full scan completed successfully.".to_string(), + }) } } @@ -190,7 +192,7 @@ impl AsyncAppCommand>> for SyncCommand { &self, ctx: &mut AppContext>, ) -> Result { - let mut wallet = &mut ctx.state.wallet; + let wallet = &mut ctx.state.wallet; let client = ctx.state.client; #[cfg(any(feature = "electrum", feature = "esplora"))] let request = wallet @@ -267,12 +269,14 @@ impl AsyncAppCommand>> for SyncCommand { let mempool_txs = emitter.mempool()?; wallet.apply_unconfirmed_txs(mempool_txs.update); } - // #[cfg(feature = "cbf")] - KyotoClient { client } => sync_kyoto_client(&mut wallet, client) + #[cfg(feature = "cbf")] + KyotoClient { client } => sync_kyoto_client(wallet, client) .await .map_err(|e| Error::Generic(e.to_string()))?, } - Ok(StatusResult::new("Wallet synced successfully.")) + Ok(StatusResult { + message: "Wallet synced successfully.".to_string(), + }) } } @@ -321,7 +325,7 @@ impl AsyncAppCommand>> for BroadcastCommand { psbt.extract_tx()? } (None, Some(tx)) => { - let tx_bytes = Vec::::from_hex(&tx)?; + let tx_bytes = Vec::::from_hex(tx)?; Transaction::consensus_decode(&mut tx_bytes.as_slice())? } (Some(_), Some(_)) => { diff --git a/src/handlers/repl.rs b/src/handlers/repl.rs index 802e735..977e819 100644 --- a/src/handlers/repl.rs +++ b/src/handlers/repl.rs @@ -1,13 +1,11 @@ -use bdk_wallet::{Wallet, bitcoin::Network}; - -#[cfg(feature = "repl")] -use crate::handlers::AppCommand; -#[cfg(feature = "repl")] -use crate::{handlers::AppContext, utils::output::FormatOutput}; - #[cfg(feature = "repl")] -use crate::commands::ReplSubCommand; -use clap::Parser; +use { + crate::commands::ReplSubCommand, + crate::handlers::{AppCommand, AppContext}, + crate::utils::output::FormatOutput, + bdk_wallet::{Wallet, bitcoin::Network}, + clap::Parser, +}; #[cfg(any( feature = "electrum", @@ -17,11 +15,7 @@ use clap::Parser; ))] use crate::client::BlockchainClient; #[cfg(feature = "repl")] -use std::io::Write; -use { - crate::commands::{CliOpts, WalletSubCommand}, - crate::error::BDKCliError as Error, -}; +use {crate::commands::WalletSubCommand, crate::error::BDKCliError as Error, std::io::Write}; #[cfg(feature = "repl")] pub(crate) async fn respond( @@ -36,7 +30,6 @@ pub(crate) async fn respond( client: Option<&BlockchainClient>, line: &str, datadir: std::path::PathBuf, - _cli_opts: &CliOpts, ) -> Result { let args = shlex::split(line).ok_or("error: Invalid quoting".to_string())?; diff --git a/src/main.rs b/src/main.rs index 1d66122..d3cac7f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,26 +25,15 @@ mod payjoin; mod persister; mod utils; -#[cfg(feature = "redb")] -use bdk_redb::Store as RedbStore; use bdk_wallet::bitcoin::Network; use log::{debug, warn}; -#[cfg(any( - feature = "electrum", - feature = "esplora", - feature = "rpc", - feature = "cbf" -))] -use crate::client::new_blockchain_client; use crate::commands::{CliOpts, CliSubCommand, WalletSubCommand}; use crate::error::BDKCliError as Error; use crate::handlers::{AppCommand, AppContext}; -#[cfg(any(feature = "sqlite", feature = "redb"))] -use crate::persister::{Persister, new_persisted_wallet}; use crate::utils::output::FormatOutput; -use crate::utils::prepare_wallet_db_dir; -use crate::utils::{load_wallet_config, prepare_home_dir}; +use crate::utils::runtime::WalletRuntime; +use crate::utils::{command_requires_db, prepare_home_dir}; use clap::Parser; #[tokio::main] @@ -81,70 +70,36 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> { feature = "rpc", feature = "cbf" ))] - WalletSubCommand::OnlineWalletSubCommand(online_cmd) => { - let (wallet_opts, network) = load_wallet_config(&home_dir, &wallet_name)?; - - let database_path = prepare_wallet_db_dir(&home_dir, &wallet_name)?; - #[cfg(any(feature = "sqlite", feature = "redb"))] - let mut persister: Persister = match &wallet_opts.database_type { - #[cfg(feature = "sqlite")] - crate::persister::DatabaseType::Sqlite => { - let db_file = database_path.join("wallet.sqlite"); - let connection = bdk_wallet::rusqlite::Connection::open(db_file)?; - Persister::Connection(connection) - } - #[cfg(feature = "redb")] - crate::persister::DatabaseType::Redb => { - use crate::persister::Persister; - - let db = std::sync::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 client = new_blockchain_client(&wallet_opts, &wallet, database_path)?; - - let mut ctx = - AppContext::new_online_wallet(network, home_dir, &mut wallet, &client); - - online_cmd.execute(&mut ctx).await?; + WalletSubCommand::OnlineWalletSubCommand(cmd) => { + let runtime = WalletRuntime::load(&home_dir, &wallet_name)?; + + let mut wallet = runtime.build_wallet(true)?; + let client = runtime.build_client(&wallet)?; + + let mut ctx = AppContext::new_online_wallet( + runtime.network, + runtime.home_dir.clone(), + &mut wallet, + &client, + ); + + cmd.execute(&mut ctx).await?; } - WalletSubCommand::OfflineWalletSubCommand(offline_cmd) => { - let (wallet_opts, network) = load_wallet_config(&home_dir, &wallet_name)?; - - let database_path = prepare_wallet_db_dir(&home_dir, &wallet_name)?; - - #[cfg(any(feature = "sqlite", feature = "redb"))] - let mut persister: Persister = match &wallet_opts.database_type { - #[cfg(feature = "sqlite")] - crate::persister::DatabaseType::Sqlite => { - let db_file = database_path.join("wallet.sqlite"); - let connection = bdk_wallet::rusqlite::Connection::open(db_file)?; - Persister::Connection(connection) - } - #[cfg(feature = "redb")] - crate::persister::DatabaseType::Redb => { - use crate::persister::Persister; - let db = std::sync::Arc::new(bdk_redb::redb::Database::create( - home_dir.join("wallet.redb"), - )?); - let store = RedbStore::new(db, wallet_name)?; - Persister::RedbStore(store) - } - }; - - let mut wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?; - - let mut ctx = AppContext::new_offline_wallet(network, home_dir, &mut wallet); - - offline_cmd.execute(&mut ctx)?; + + WalletSubCommand::OfflineWalletSubCommand(cmd) => { + let runtime = WalletRuntime::load(&home_dir, &wallet_name)?; + + let mut wallet = runtime.build_wallet(command_requires_db(&cmd))?; + + let mut ctx = AppContext::new_offline_wallet( + runtime.network, + runtime.home_dir.clone(), + &mut wallet, + ); + + cmd.execute(&mut ctx)?; } + WalletSubCommand::Config(mut config_cmd) => { config_cmd.wallet_opts.wallet = Some(wallet_name); @@ -156,107 +111,81 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> { CliSubCommand::Key { subcommand } => { let mut ctx = AppContext::new(cli_opts.network, home_dir); + subcommand.execute(&mut ctx)?; } - CliSubCommand::Descriptor(descriptor_command) => { + + CliSubCommand::Descriptor(cmd) => { let mut ctx = AppContext::new(cli_opts.network, home_dir); - descriptor_command - .execute(&mut ctx)? - .write_out(std::io::stdout())?; + + cmd.execute(&mut ctx)?.write_out(std::io::stdout())?; } + CliSubCommand::Wallets(cmd) => { let mut ctx = AppContext::new(cli_opts.network, home_dir); + cmd.execute(&mut ctx)?.write_out(std::io::stdout())?; } + + #[cfg(feature = "repl")] CliSubCommand::Repl { wallet: wallet_name, } => { - #[cfg(feature = "repl")] - { - let (wallet_opts, network) = load_wallet_config(&home_dir, &wallet_name)?; - let database_path = prepare_wallet_db_dir(&home_dir, &wallet_name)?; - - #[cfg(any(feature = "sqlite", feature = "redb"))] - let mut persister: Persister = match &wallet_opts.database_type { - #[cfg(feature = "sqlite")] - crate::persister::DatabaseType::Sqlite => { - let db_file = database_path.join("wallet.sqlite"); - let connection = bdk_wallet::rusqlite::Connection::open(db_file)?; - Persister::Connection(connection) - } - #[cfg(feature = "redb")] - crate::persister::DatabaseType::Redb => { - use crate::persister::Persister; - let db = std::sync::Arc::new(bdk_redb::redb::Database::create( - home_dir.join("wallet.redb"), - )?); - let store = RedbStore::new(db, wallet_name.clone())?; - Persister::RedbStore(store) - } - }; - - let mut wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?; - - #[cfg(any( - feature = "electrum", - feature = "esplora", - feature = "rpc", - feature = "cbf" - ))] - let client = Some(new_blockchain_client(&wallet_opts, &wallet, database_path)?); - - println!( - "Entering REPL mode for wallet '{}'. Type 'exit' to quit.", - wallet_name - ); + let runtime = WalletRuntime::load(&home_dir, &wallet_name)?; - loop { - let line = crate::handlers::repl::readline()?; - if line.trim().is_empty() { - continue; - } - - // Pass it to our newly refactored respond function - let should_exit = crate::handlers::repl::respond( - network, - &mut wallet, - #[cfg(any( - feature = "electrum", - feature = "esplora", - feature = "rpc", - feature = "cbf" - ))] - client.as_ref(), - &line, - home_dir.clone(), - &cli_opts, - ) - .await - .map_err(Error::Generic)?; - - // Break the loop if the user typed `exit` - if should_exit { - break; - } + let mut wallet = runtime.build_wallet(true)?; + + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" + ))] + let client = runtime.build_client(&wallet).ok(); + + println!( + "Entering REPL mode for wallet '{}'. \ + Type 'exit' to quit.", + wallet_name + ); + + loop { + let line = crate::handlers::repl::readline()?; + + if line.trim().is_empty() { + continue; } - } - #[cfg(not(feature = "repl"))] - { - return Err(Error::Generic( - "The 'repl' feature is not enabled in this build.".into(), - )); + let should_exit = crate::handlers::repl::respond( + runtime.network, + &mut wallet, + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" + ))] + client.as_ref(), + &line, + runtime.home_dir.clone(), + ) + .await + .map_err(Error::Generic)?; + + if should_exit { + break; + } } } - CliSubCommand::Completions { shell } => { - shell; - } + #[cfg(feature = "compiler")] CliSubCommand::Compile(cmd) => { let mut ctx = AppContext::new(cli_opts.network, home_dir); + cmd.execute(&mut ctx)?.write_out(std::io::stdout())?; } - }; + CliSubCommand::Completions { shell: _ } => unimplemented!(), + } Ok(()) } diff --git a/src/persister.rs b/src/persister.rs index 2a17db1..564000f 100644 --- a/src/persister.rs +++ b/src/persister.rs @@ -1,9 +1,9 @@ -#[cfg(any(feature = "sqlite", feature = "redb"))] 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, bitcoin::Network}; -use bdk_wallet::{Wallet, WalletPersister}; +use bdk_wallet::{KeychainKind, PersistedWallet, WalletPersister}; use clap::ValueEnum; #[derive(Clone, ValueEnum, Debug, Eq, PartialEq)] @@ -25,6 +25,7 @@ pub(crate) enum Persister { RedbStore(bdk_redb::Store), } +#[cfg(any(feature = "sqlite", feature = "redb"))] impl WalletPersister for Persister { type Error = Error; @@ -97,7 +98,6 @@ where 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(); diff --git a/src/utils/common.rs b/src/utils/common.rs index 7783463..43ff5d8 100644 --- a/src/utils/common.rs +++ b/src/utils/common.rs @@ -14,6 +14,7 @@ use bdk_wallet::bitcoin::{Address, Network, OutPoint, ScriptBuf}; #[cfg(feature = "silent-payments")] use bdk_sp::encoding::SilentPaymentCode; +use crate::commands::OfflineWalletSubCommand; use std::{ path::{Path, PathBuf}, str::FromStr, @@ -217,3 +218,28 @@ pub(crate) fn parse_signature_format(format_str: &str) -> Result bool { + match command { + OfflineWalletSubCommand::Balance(_) + | OfflineWalletSubCommand::Unspent(_) + | OfflineWalletSubCommand::Transactions(_) + | OfflineWalletSubCommand::BumpFee(_) => true, + + OfflineWalletSubCommand::NewAddress(_) + | OfflineWalletSubCommand::UnusedAddress(_) + | OfflineWalletSubCommand::CreateTx(_) + | OfflineWalletSubCommand::Policies(_) + | OfflineWalletSubCommand::PublicDescriptor(_) + | OfflineWalletSubCommand::Sign(_) + | OfflineWalletSubCommand::ExtractPsbt(_) + | OfflineWalletSubCommand::FinalizePsbt(_) + | OfflineWalletSubCommand::CombinePsbt(_) => false, + + #[cfg(feature = "bip322")] + OfflineWalletSubCommand::SignMessage(_) => true, + + #[cfg(feature = "bip322")] + OfflineWalletSubCommand::VerifyMessage(_) => false, + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index afd02f9..084cab5 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -2,4 +2,5 @@ pub mod common; pub mod descriptors; pub mod output; pub use common::*; +pub mod runtime; pub mod types; diff --git a/src/utils/runtime.rs b/src/utils/runtime.rs new file mode 100644 index 0000000..2bf6ec1 --- /dev/null +++ b/src/utils/runtime.rs @@ -0,0 +1,137 @@ +#[cfg(feature = "redb")] +use bdk_redb::Store as RedbStore; +use bdk_wallet::{Wallet, bitcoin::Network}; +use std::{ + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, +}; + +use crate::{ + error::BDKCliError as Error, + persister::new_wallet, + utils::{load_wallet_config, prepare_wallet_db_dir}, +}; +#[cfg(any(feature = "sqlite", feature = "redb"))] +use { + crate::persister::{DatabaseType, Persister, new_persisted_wallet}, + bdk_wallet::PersistedWallet, +}; + +#[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" +))] +use crate::client::{BlockchainClient, new_blockchain_client}; + +pub enum RuntimeWallet { + Standard(Wallet), + #[cfg(any(feature = "sqlite", feature = "redb"))] + Persisted(PersistedWallet), +} + +impl Deref for RuntimeWallet { + type Target = Wallet; + fn deref(&self) -> &Self::Target { + match self { + Self::Standard(wallet) => wallet, + #[cfg(any(feature = "sqlite", feature = "redb"))] + Self::Persisted(wallet) => wallet, + } + } +} + +impl DerefMut for RuntimeWallet { + fn deref_mut(&mut self) -> &mut Self::Target { + match self { + Self::Standard(wallet) => wallet, + #[cfg(any(feature = "sqlite", feature = "redb"))] + Self::Persisted(wallet) => wallet, + } + } +} + +#[allow(unused)] +pub struct WalletRuntime { + pub wallet_name: String, + pub wallet_opts: crate::commands::WalletOpts, + pub network: Network, + pub home_dir: PathBuf, + pub database_path: PathBuf, +} + +impl WalletRuntime { + pub fn load(home_dir: &Path, wallet_name: &str) -> Result { + let (wallet_opts, network) = load_wallet_config(home_dir, wallet_name)?; + + let database_path = prepare_wallet_db_dir(home_dir, wallet_name)?; + + Ok(Self { + wallet_name: wallet_name.to_string(), + wallet_opts, + network, + home_dir: home_dir.to_path_buf(), + database_path, + }) + } + + pub fn build_wallet(&self, require_db: bool) -> Result { + if !require_db { + return Ok(RuntimeWallet::Standard(new_wallet( + self.network, + &self.wallet_opts, + )?)); + } + + #[cfg(any(feature = "sqlite", feature = "redb"))] + { + let mut persister = self.create_persister()?; + let wallet = new_persisted_wallet(self.network, &mut persister, &self.wallet_opts)?; + Ok(RuntimeWallet::Persisted(wallet)) + } + + #[cfg(not(any(feature = "sqlite", feature = "redb")))] + { + Ok(RuntimeWallet::Standard(new_wallet( + self.network, + &self.wallet_opts, + )?)) + } + } + + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" + ))] + pub fn build_client(&self, wallet: &Wallet) -> Result { + new_blockchain_client(&self.wallet_opts, wallet, self.database_path.clone()) + } + + #[cfg(any(feature = "sqlite", feature = "redb"))] + fn create_persister(&self) -> Result { + match &self.wallet_opts.database_type { + #[cfg(feature = "sqlite")] + DatabaseType::Sqlite => { + let db_file = self.database_path.join("wallet.sqlite"); + + let connection = bdk_wallet::rusqlite::Connection::open(db_file)?; + + Ok(Persister::Connection(connection)) + } + + #[cfg(feature = "redb")] + DatabaseType::Redb => { + let db = std::sync::Arc::new(bdk_redb::redb::Database::create( + self.home_dir.join("wallet.redb"), + )?); + + let store = RedbStore::new(db, self.wallet_name.clone())?; + + Ok(Persister::RedbStore(store)) + } + } + } +} diff --git a/src/utils/types.rs b/src/utils/types.rs index 2ed11ae..a8e2998 100644 --- a/src/utils/types.rs +++ b/src/utils/types.rs @@ -25,6 +25,7 @@ impl From for AddressResult { } } +#[allow(unused)] /// Represents the data for a single transaction #[derive(Serialize)] pub struct TransactionDetails { @@ -133,14 +134,6 @@ pub struct StatusResult { pub message: String, } -impl StatusResult { - pub fn new(msg: &str) -> Self { - Self { - message: msg.to_string(), - } - } -} - #[cfg(any( feature = "electrum", feature = "esplora", diff --git a/tests/integration.rs b/tests/integration.rs index a45cf8a..5e35181 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -245,15 +245,4 @@ mod test { assert_eq!(confirmed_balance, 1000000000u64); } - // #[test] - // #[cfg(feature = "regtest-bitcoin")] - // fn test_basic_wallet_op_bitcoind() { - // basic_wallet_ops("regtest-bitcoin") - // } - // - // #[test] - // #[cfg(feature = "regtest-electrum")] - // fn test_basic_wallet_op_electrum() { - // basic_wallet_ops("regtest-electrum") - // } }