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())),
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
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?;
}
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)?;
}
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;
}
use bdk_sp::encoding::SilentPaymentCode;
use std::{
- fmt::Display,
path::{Path, PathBuf},
str::FromStr,
};
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();
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;
}
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 {
pub internal: T,
}
+#[cfg(feature = "bip322")]
#[derive(Serialize, Debug, Default)]
pub struct MessageResult {
#[serde(skip_serializing_if = "Option::is_none")]
}
}
+#[cfg(any(
+ feature = "electrum",
+ feature = "esplora",
+ feature = "cbf",
+ feature = "rpc"
+))]
#[derive(Serialize, Debug)]
pub struct TransactionResult {
pub txid: String,
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());
-}