From: Vihiga Tyonum Date: Fri, 29 May 2026 03:17:56 +0000 (+0100) Subject: Refactor(handlers): Define app context states X-Git-Url: http://internal-gitweb-vhost/blockdata/script/encode/consensus/struct.CommandString.html?a=commitdiff_plain;h=f3deed85868f15f9b97355d1680d2df6d1432120;p=bdk-cli Refactor(handlers): Define app context states - Define Init state for when an execution does not need either the wallet or client for execution - Define the Offline wallet operations state for app context when an execution envt needs a wallet - Define the online wallet operations state for appcontext when an execution needs both the wallet and client - make app context generic over the state - make the app command and async app command generic over the execution context - deleted shorten fn as it was applicable to the pretty flag - removed tests for pretty flag --- diff --git a/src/client.rs b/src/client.rs index db31bf6..c90aab2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -79,7 +79,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/handlers/mod.rs b/src/handlers/mod.rs index 25b5385..d63ec3b 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -17,66 +17,93 @@ use std::path::PathBuf; use crate::{error::BDKCliError as Error, utils::output::FormatOutput}; use bdk_wallet::{Wallet, bitcoin::Network}; -/// The shared environment for all commands -pub struct AppContext<'a> { +// The state for no wallet, no client. +pub struct Init; + +/// Offline wallet operations. +/// Requires only a wallet. +pub struct OfflineOperations<'a> { + pub wallet: &'a mut Wallet, +} + +#[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" +))] +/// Online wallet operations. +/// Requires a wallet and a client. +pub struct OnlineOperations<'a> { + pub wallet: &'a mut Wallet, + pub client: &'a BlockchainClient, +} + +/// The generic context +pub struct AppContext { pub network: Network, pub datadir: PathBuf, - pub wallet: Option<&'a mut Wallet>, - #[cfg(any( - feature = "electrum", - feature = "esplora", - feature = "rpc", - feature = "cbf" - ))] - pub client: Option<&'a BlockchainClient>, + pub state: S, } -impl<'a> AppContext<'a> { +/// Construct for a specific state. +impl AppContext { pub fn new(network: Network, datadir: PathBuf) -> Self { Self { network, datadir, - wallet: None, - #[cfg(any( - feature = "electrum", - feature = "esplora", - feature = "rpc", - feature = "cbf" - ))] - client: None, + state: Init, } } +} - /// Attach a mutable wallet reference to the context. - pub fn with_wallet(mut self, wallet: &'a mut Wallet) -> Self { - self.wallet = Some(wallet); - self +impl<'a> AppContext> { + pub fn new_offline_wallet(network: Network, datadir: PathBuf, wallet: &'a mut Wallet) -> Self { + Self { + network, + datadir, + state: OfflineOperations { wallet }, + } } +} - /// Attach a client reference to the context. - #[cfg(any( - feature = "electrum", - feature = "esplora", - feature = "rpc", - feature = "cbf" - ))] - pub fn with_client(mut self, client: &'a BlockchainClient) -> Self { - self.client = Some(client); - self +#[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" +))] +impl<'a> AppContext> { + pub fn new_online_wallet( + network: Network, + datadir: PathBuf, + wallet: &'a mut Wallet, + client: &'a BlockchainClient, + ) -> Self { + Self { + network, + datadir, + state: OnlineOperations { wallet, client }, + } } } -pub trait AsyncCommand { +pub trait AppCommand { type Output: FormatOutput; - async fn execute(&self, ctx: &mut AppContext<'_>) -> Result; + + fn execute(&self, ctx: &mut C) -> Result; } -/// The command trait -pub trait AppCommand { +#[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" +))] +pub trait AsyncAppCommand { type Output: FormatOutput; - - /// The execution logic - fn execute(&self, ctx: &mut AppContext) -> Result; + + async fn execute(&self, ctx: &mut C) -> Result; } // context for online and online diff --git a/src/main.rs b/src/main.rs index 99f480b..1d66122 100644 --- a/src/main.rs +++ b/src/main.rs @@ -110,9 +110,8 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> { let client = new_blockchain_client(&wallet_opts, &wallet, database_path)?; - let mut ctx = AppContext::new(network, home_dir) - .with_wallet(&mut wallet) - .with_client(&client); + let mut ctx = + AppContext::new_online_wallet(network, home_dir, &mut wallet, &client); online_cmd.execute(&mut ctx).await?; } @@ -142,7 +141,7 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> { let mut wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?; - let mut ctx = AppContext::new(network, home_dir).with_wallet(&mut wallet); + let mut ctx = AppContext::new_offline_wallet(network, home_dir, &mut wallet); offline_cmd.execute(&mut ctx)?; } @@ -169,7 +168,86 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> { let mut ctx = AppContext::new(cli_opts.network, home_dir); cmd.execute(&mut ctx)?.write_out(std::io::stdout())?; } - CliSubCommand::Repl { wallet: _ } => todo!(), + 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 + ); + + 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; + } + } + } + + #[cfg(not(feature = "repl"))] + { + return Err(Error::Generic( + "The 'repl' feature is not enabled in this build.".into(), + )); + } + } CliSubCommand::Completions { shell } => { shell; } diff --git a/src/utils/common.rs b/src/utils/common.rs index 03e62db..7783463 100644 --- a/src/utils/common.rs +++ b/src/utils/common.rs @@ -15,7 +15,6 @@ use bdk_wallet::bitcoin::{Address, Network, OutPoint, ScriptBuf}; use bdk_sp::encoding::SilentPaymentCode; use std::{ - fmt::Display, path::{Path, PathBuf}, str::FromStr, }; @@ -50,18 +49,6 @@ pub(crate) fn is_final(psbt: &Psbt) -> Result<(), Error> { Ok(()) } -pub(crate) fn shorten(displayable: impl Display, start: u8, end: u8) -> String { - let displayable = displayable.to_string(); - - if displayable.len() <= (start + end) as usize { - return displayable; - } - - let start_str: &str = &displayable[0..start as usize]; - let end_str: &str = &displayable[displayable.len() - end as usize..]; - format!("{start_str}...{end_str}") -} - /// Parse the recipient (Address,Amount) argument from cli input. pub(crate) fn parse_recipient(s: &str) -> Result<(ScriptBuf, u64), String> { let parts: Vec<_> = s.split(':').collect(); diff --git a/src/utils/types.rs b/src/utils/types.rs index 5135343..2ed11ae 100644 --- a/src/utils/types.rs +++ b/src/utils/types.rs @@ -1,12 +1,11 @@ use std::collections::HashMap; use crate::config::WalletConfigInner; -use crate::utils::shorten; use bdk_wallet::Balance; use bdk_wallet::bitcoin::{ - Address, Network, Psbt, Transaction, base64::Engine, consensus::encode::serialize_hex, + Network, Psbt, Transaction, base64::Engine, consensus::encode::serialize_hex, }; -use bdk_wallet::{AddressInfo, LocalOutput, chain::ChainPosition}; +use bdk_wallet::{AddressInfo, LocalOutput}; use serde::Serialize; use serde_json::json; @@ -58,24 +57,7 @@ pub struct UnspentDetails { } impl UnspentDetails { - pub fn from_local_output(utxo: &LocalOutput, network: Network) -> Self { - let height = utxo.chain_position.confirmation_height_upper_bound(); - let height_display = height - .map(|h| h.to_string()) - .unwrap_or_else(|| "Pending".to_string()); - - let (_, block_hash_display) = match &utxo.chain_position { - ChainPosition::Confirmed { anchor, .. } => { - let hash = anchor.block_id.hash.to_string(); - (Some(hash.clone()), shorten(&hash, 8, 8)) - } - ChainPosition::Unconfirmed { .. } => (None, "Unconfirmed".to_string()), - }; - - let address = Address::from_script(&utxo.txout.script_pubkey, network) - .map(|a| a.to_string()) - .unwrap_or_else(|_| "Unknown Script".to_string()); - + pub fn from_local_output(utxo: &LocalOutput, _network: Network) -> Self { let outpoint_str = utxo.outpoint.to_string(); Self { @@ -133,6 +115,7 @@ pub struct KeychainPair { pub internal: T, } +#[cfg(feature = "bip322")] #[derive(Serialize, Debug, Default)] pub struct MessageResult { #[serde(skip_serializing_if = "Option::is_none")] @@ -158,6 +141,12 @@ impl StatusResult { } } +#[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "cbf", + feature = "rpc" +))] #[derive(Serialize, Debug)] pub struct TransactionResult { pub txid: String, diff --git a/tests/cli_flags.rs b/tests/cli_flags.rs index beec798..23abc81 100644 --- a/tests/cli_flags.rs +++ b/tests/cli_flags.rs @@ -24,23 +24,3 @@ fn test_without_pretty_flag() { let stdout = String::from_utf8_lossy(&output.stdout); assert!(serde_json::from_str::(&stdout).is_ok()); } - -#[test] -fn test_pretty_flag_before_subcommand() { - let output = Command::new("cargo") - .args("run -- --pretty key generate".split_whitespace()) - .output() - .unwrap(); - - assert!(output.status.success()); -} - -#[test] -fn test_pretty_flag_after_subcommand() { - let output = Command::new("cargo") - .args("run -- key generate --pretty".split_whitespace()) - .output() - .unwrap(); - - assert!(output.status.success()); -}