]> Untitled Git - bdk-cli/commitdiff
Refactor(handlers): Define app context states
authorVihiga Tyonum <withtvpeter@gmail.com>
Fri, 29 May 2026 03:17:56 +0000 (04:17 +0100)
committerVihiga Tyonum <withtvpeter@gmail.com>
Fri, 5 Jun 2026 16:32:14 +0000 (17:32 +0100)
- 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

src/client.rs
src/handlers/mod.rs
src/main.rs
src/utils/common.rs
src/utils/types.rs
tests/cli_flags.rs

index db31bf69a7d184a4cfc29a21dae4edca3d407a29..c90aab2e2d02825fa8d8dc1f73ce5de4fe4dbe03 100644 (file)
@@ -79,7 +79,7 @@ pub(crate) enum BlockchainClient {
 impl BlockchainClient {
     pub async fn broadcast(&self, tx: Transaction) -> Result<Txid, Error> {
         match self {
-            #[cfg(feature = "electrum")]
+            // #[cfg(feature = "electrum")]
             Self::Electrum { client, .. } => client
                 .transaction_broadcast(&tx)
                 .map_err(|e| Error::Generic(e.to_string())),
index 25b53852c3553002c8f7306c477e4fb4927e2c93..d63ec3b554723ee10d8143d0230da8f155291a4f 100644 (file)
@@ -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<S> {
     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<Init> {
     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<OfflineOperations<'a>> {
+    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<OnlineOperations<'a>> {
+    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<C> {
     type Output: FormatOutput;
-    async fn execute(&self, ctx: &mut AppContext<'_>) -> Result<Self::Output, Error>;
+
+    fn execute(&self, ctx: &mut C) -> Result<Self::Output, Error>;
 }
 
-/// The command trait
-pub trait AppCommand {
+#[cfg(any(
+    feature = "electrum",
+    feature = "esplora",
+    feature = "rpc",
+    feature = "cbf"
+))]
+pub trait AsyncAppCommand<C> {
     type Output: FormatOutput;
-
-    /// The execution logic
-    fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error>;
+    
+    async fn execute(&self, ctx: &mut C) -> Result<Self::Output, Error>;
 }
 
 // context for online and online
index 99f480b6df5d0f698a36364d4f1510dacb2e378b..1d661223eb9c9ab94b5ff80c1b395c9feead193a 100644 (file)
@@ -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;
         }
index 03e62db35512efffb48c262cb2731e6c49ae3d6a..7783463b70c49b3879ac52da4f2cf974f208d4e2 100644 (file)
@@ -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();
index 5135343907d1ab2e39e449e1a9ca8a046b77d00f..2ed11ae19918b9cdd20e6489415b00b3f40437b2 100644 (file)
@@ -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<T> {
     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,
index beec79832e54c7ba20d4883ece65ff3363b9b11f..23abc81ab458362be7d0e4d909f541870cc25d0b 100644 (file)
@@ -24,23 +24,3 @@ fn test_without_pretty_flag() {
     let stdout = String::from_utf8_lossy(&output.stdout);
     assert!(serde_json::from_str::<serde_json::Value>(&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());
-}