FinalizePsbtCommand, NewAddressCommand, PoliciesCommand, PublicDescriptorCommand,
SignCommand, TransactionsCommand, UnspentCommand, UnusedAddressCommand,
},
- online::{
- BroadcastCommand, FullScanCommand, ReceivePayjoinCommand, SendPayjoinCommand, SyncCommand,
- },
};
#[cfg(any(
feature = "rpc",
feature = "cbf"
))]
-use crate::client::ClientType;
+use crate::{
+ client::ClientType,
+ online::{
+ BroadcastCommand, FullScanCommand, ReceivePayjoinCommand, SendPayjoinCommand, SyncCommand,
+ },
+};
#[cfg(feature = "compiler")]
use crate::handlers::descriptor::CompileCommand;
use clap::{Args, Parser, Subcommand, value_parser};
use clap_complete::Shell;
-// #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
+#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
use crate::utils::parse_proxy_auth;
/// The BDK Command Line Wallet App
#[cfg(any(
feature = "electrum",
feature = "esplora",
- feature = "rpc",
- feature = "cbf"
+ feature = "cbf",
+ feature = "rpc"
))]
#[command(flatten)]
OnlineWalletSubCommand(OnlineWalletSubCommand),
}
/// Options to configure a SOCKS5 proxy for a blockchain client connection.
-// #[cfg(any(feature = "electrum", feature = "esplora"))]
+#[cfg(any(feature = "electrum", feature = "esplora"))]
#[derive(Debug, Args, Clone, PartialEq, Eq)]
pub struct ProxyOpts {
/// Sets the SOCKS5 proxy for a blockchain client.
}
/// Options to configure a BIP157 Compact Filter backend.
-// #[cfg(feature = "cbf")]
+#[cfg(feature = "cbf")]
#[derive(Debug, Args, Clone, PartialEq, Eq)]
pub struct CompactFilterOpts {
/// Sets the number of parallel node connections.
CombinePsbt(CombinePsbtCommand),
/// Sign a message using BIP322
#[cfg(feature = "bip322")]
- // SignMessage {
- // /// The message to sign
- // #[arg(long)]
- // message: String,
- // /// The signature format (e.g., Legacy, Simple, Full)
- // #[arg(long, default_value = "simple")]
- // signature_type: String,
- // /// Address to sign
- // #[arg(long)]
- // address: String,
- // /// Optional list of specific UTXOs for proof-of-funds (only for `FullWithProofOfFunds`)
- // #[arg(long)]
- // utxos: Option<Vec<OutPoint>>,
- // },
SignMessage(SignMessageCommand),
/// Verify a BIP322 signature
#[cfg(feature = "bip322")]
- // VerifyMessage {
- // /// The signature proof to verify
- // #[arg(long)]
- // proof: String,
- // /// The message that was signed
- // #[arg(long)]
- // message: String,
- // /// The address associated with the signature
- // #[arg(long)]
- // address: String,
- // },
VerifyMessage(VerifyMessageCommand),
}
/// Wallet subcommands that needs a blockchain backend.
#[derive(Debug, Subcommand, Clone, PartialEq, Eq)]
#[command(rename_all = "snake")]
-// #[cfg(any(
-// feature = "electrum",
-// feature = "esplora",
-// feature = "cbf",
-// feature = "rpc"
-// ))]
+#[cfg(any(
+ feature = "electrum",
+ feature = "esplora",
+ feature = "cbf",
+ feature = "rpc"
+))]
pub enum OnlineWalletSubCommand {
/// Full Scan with the chosen blockchain server.
FullScan(FullScanCommand),
/// Broadcasts a transaction to the network. Takes either a raw transaction or a PSBT to extract.
Broadcast(BroadcastCommand),
/// Generates a Payjoin receive URI and processes the sender's Payjoin proposal.
- // ReceivePayjoin {
- // /// Amount to be received in sats.
- // #[arg(env = "PAYJOIN_AMOUNT", long = "amount", required = true)]
- // amount: u64,
- // /// Payjoin directory which will be used to store the PSBTs which are pending action
- // /// from one of the parties.
- // #[arg(env = "PAYJOIN_DIRECTORY", long = "directory", required = true)]
- // directory: String,
- // /// URL of the Payjoin OHTTP relay. Can be repeated multiple times to attempt the
- // /// operation with multiple relays for redundancy.
- // #[arg(env = "PAYJOIN_OHTTP_RELAY", long = "ohttp_relay", required = true)]
- // ohttp_relay: Vec<String>,
- // /// Maximum effective fee rate the receiver is willing to pay for their own input/output contributions.
- // #[arg(env = "PAYJOIN_RECEIVER_MAX_FEE_RATE", long = "max_fee_rate")]
- // max_fee_rate: Option<u64>,
- // },
ReceivePayjoin(ReceivePayjoinCommand),
/// Sends an original PSBT to a BIP 21 URI and broadcasts the returned Payjoin PSBT.
- // SendPayjoin {
- // /// BIP 21 URI for the Payjoin.
- // #[arg(env = "PAYJOIN_URI", long = "uri", required = true)]
- // uri: String,
- // /// URL of the Payjoin OHTTP relay. Can be repeated multiple times to attempt the
- // /// operation with multiple relays for redundancy.
- // #[arg(env = "PAYJOIN_OHTTP_RELAY", long = "ohttp_relay", required = true)]
- // ohttp_relay: Vec<String>,
- // /// Fee rate to use in sat/vbyte.
- // #[arg(
- // env = "PAYJOIN_SENDER_FEE_RATE",
- // short = 'f',
- // long = "fee_rate",
- // required = true
- // )]
- // fee_rate: u64,
- // },
SendPayjoin(SendPayjoinCommand),
}
use bdk_wallet::miniscript::{self, Segwitv0};
use clap::Parser;
-
-
impl KeySubCommand {
pub fn execute(&self, ctx: &mut AppContext) -> Result<(), Error> {
match self {
pub mod offline;
pub mod online;
pub mod repl;
-pub mod types;
-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",
+ 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<String, Error> {
- let pretty = cli_opts.pretty;
- let subcommand = cli_opts.subcommand.clone();
-
- let result: Result<String, Error> = 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)?;
+use crate::client::BlockchainClient;
+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> {
+ 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>,
+}
- 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)
+impl<'a> AppContext<'a> {
+ 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,
}
- 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;
- }
+ }
+
+ /// Attach a mutable wallet reference to the context.
+ pub fn with_wallet(mut self, wallet: &'a mut Wallet) -> Self {
+ self.wallet = Some(wallet);
+ self
+ }
+
+ /// 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
+ }
+}
- 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)?;
+pub trait AsyncCommand {
+ type Output: FormatOutput;
+ async fn execute(&self, ctx: &mut AppContext<'_>) -> Result<Self::Output, Error>;
+}
- 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(),
- );
+/// The command trait
+pub trait AppCommand {
+ type Output: FormatOutput;
- Ok("".to_string())
- }
- };
- result
+ /// The execution logic
+ fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error>;
}
+
+// context for online and online
+// => cli.rs
+// handlers/{mod for commands}
+// wallet subdir /
+// wallet-offline and wallet-online (client mod)
-use crate::commands::OfflineWalletSubCommand::*;
-use crate::commands::{CliOpts, OfflineWalletSubCommand, WalletOpts};
+use crate::commands::OfflineWalletSubCommand;
use crate::error::BDKCliError as Error;
-use crate::handlers::types::{
+use crate::handlers::{AppCommand, AppContext};
+use crate::utils::output::FormatOutput;
+use crate::utils::parse_address;
+use crate::utils::types::{
AddressResult, BalanceResult, KeychainPair, PsbtResult, RawPsbt, TransactionDetails,
TransactionListResult, UnspentDetails, UnspentListResult,
};
-use crate::utils::output::FormatOutput;
+use crate::utils::{parse_outpoint, parse_recipient};
use bdk_wallet::bitcoin::base64::Engine;
use bdk_wallet::bitcoin::base64::prelude::BASE64_STANDARD;
use bdk_wallet::bitcoin::script::PushBytesBuf;
-use bdk_wallet::bitcoin::{Amount, FeeRate, Psbt, Sequence, Txid};
-use bdk_wallet::{KeychainKind, SignOptions, Wallet};
+use bdk_wallet::bitcoin::{Address, Amount, FeeRate, OutPoint, Psbt, ScriptBuf, Sequence, Txid};
+use bdk_wallet::{KeychainKind, SignOptions};
+use clap::Parser;
use serde_json::json;
use std::collections::BTreeMap;
use std::str::FromStr;
#[cfg(feature = "bip322")]
use {
- crate::utils::{parse_address, parse_signature_format},
- bdk_bip322::{BIP322, MessageProof, MessageVerificationResult},
- bdk_wallet::bitcoin::Address,
+ crate::utils::parse_signature_format,
+ crate::utils::types::MessageResult,
+ bdk_bip322::{BIP322, MessageProof},
};
-/// Execute an offline wallet sub-command
-///
-/// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`].
-pub fn handle_offline_wallet_subcommand(
- wallet: &mut Wallet,
- wallet_opts: &WalletOpts,
- cli_opts: &CliOpts,
- offline_subcommand: OfflineWalletSubCommand,
-) -> Result<String, Error> {
- let pretty = cli_opts.pretty;
- match offline_subcommand {
- NewAddress => {
- let addr = wallet.reveal_next_address(KeychainKind::External);
- let result: AddressResult = addr.into();
- result.format(pretty)
- }
- UnusedAddress => {
- let addr = wallet.next_unused_address(KeychainKind::External);
- let result: AddressResult = addr.into();
- result.format(pretty)
+impl OfflineWalletSubCommand {
+ pub fn execute(&self, ctx: &mut AppContext<'_>) -> Result<(), Error> {
+ match self {
+ Self::NewAddress(new_address) => new_address.execute(ctx)?.print(),
+ Self::Balance(balance) => balance.execute(ctx)?.print(),
+ Self::UnusedAddress(unused_address_command) => {
+ unused_address_command.execute(ctx)?.print()
+ }
+ Self::Unspent(unspent_command) => unspent_command.execute(ctx)?.print(),
+ Self::Transactions(transactions_command) => transactions_command.execute(ctx)?.print(),
+ Self::CreateTx(createtx_command) => createtx_command.execute(ctx)?.print(),
+ Self::BumpFee(bumpfee_command) => bumpfee_command.execute(ctx)?.print(),
+ Self::Policies(policies_command) => policies_command.execute(ctx)?.print(),
+ Self::PublicDescriptor(public_descriptor_command) => {
+ public_descriptor_command.execute(ctx)?.print()
+ }
+ Self::Sign(sign_command) => sign_command.execute(ctx)?.print(),
+ Self::ExtractPsbt(extract_psbt_command) => extract_psbt_command.execute(ctx)?.print(),
+ Self::FinalizePsbt(finalize_psbt_command) => {
+ finalize_psbt_command.execute(ctx)?.print()
+ }
+ Self::CombinePsbt(combine_psbt_command) => combine_psbt_command.execute(ctx)?.print(),
+ #[cfg(feature = "bip322")]
+ Self::SignMessage(sign_message_command) => sign_message_command.execute(ctx)?.print(),
+ #[cfg(feature = "bip322")]
+ Self::VerifyMessage(verify_message_command) => {
+ verify_message_command.execute(ctx)?.print()
+ }
}
- Unspent => {
- let utxos: Vec<UnspentDetails> = wallet
- .list_unspent()
- .map(|utxo| UnspentDetails::from_local_output(&utxo, cli_opts.network))
- .collect();
+ }
+}
- let result = UnspentListResult(utxos);
- result.format(pretty)
- }
- Transactions => {
- let transactions = wallet.transactions();
-
- let txns: Vec<TransactionDetails> = transactions
- .map(|tx| {
- let total_value = tx
- .tx_node
- .output
- .iter()
- .map(|output| output.value.to_sat())
- .sum::<u64>();
-
- TransactionDetails {
- txid: tx.tx_node.txid.to_string(),
- is_coinbase: tx.tx_node.is_coinbase(),
- wtxid: tx.tx_node.compute_wtxid().to_string(),
- version: serde_json::to_value(tx.tx_node.version).unwrap_or(json!(1)),
- version_display: tx.tx_node.version.to_string(),
- is_rbf: tx.tx_node.is_explicitly_rbf(),
- inputs: serde_json::to_value(&tx.tx_node.input).unwrap_or_default(),
- outputs: serde_json::to_value(&tx.tx_node.output).unwrap_or_default(),
- input_count: tx.tx_node.input.len(),
- output_count: tx.tx_node.output.len(),
- total_value,
- }
- })
- .collect();
+#[derive(Parser, Debug, Clone, PartialEq)]
+pub struct NewAddressCommand {}
- let result = TransactionListResult(txns);
- result.format(pretty)
- }
- Balance => {
- let balance = wallet.balance();
- let result: BalanceResult = balance.into();
- result.format(pretty)
- }
+impl AppCommand for NewAddressCommand {
+ type Output = AddressResult;
- CreateTx {
- recipients,
- send_all,
- enable_rbf,
- offline_signer,
- utxos,
- unspendable,
- fee_rate,
- external_policy,
- internal_policy,
- add_data,
- add_string,
- } => {
- let mut tx_builder = wallet.build_tx();
-
- if send_all {
- tx_builder.drain_wallet().drain_to(recipients[0].0.clone());
- } else {
- let recipients = recipients
- .into_iter()
- .map(|(script, amount)| (script, Amount::from_sat(amount)))
- .collect();
- tx_builder.set_recipients(recipients);
- }
+ fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error> {
+ let wallet = ctx
+ .wallet
+ .as_deref_mut()
+ .ok_or(Error::Generic("Wallet required".to_string()))?;
+ let address_info = wallet.reveal_next_address(KeychainKind::External);
+ Ok(AddressResult::from(address_info))
+ }
+}
- if !enable_rbf {
- tx_builder.set_exact_sequence(Sequence::MAX);
- }
+#[derive(Parser, Debug, PartialEq, Clone)]
+pub struct UnusedAddressCommand {}
- if offline_signer {
- tx_builder.include_output_redeem_witness_script();
- }
+impl AppCommand for UnusedAddressCommand {
+ type Output = AddressResult;
- if let Some(fee_rate) = fee_rate
- && let Some(fee_rate) = FeeRate::from_sat_per_vb(fee_rate as u64)
- {
- tx_builder.fee_rate(fee_rate);
- }
+ fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error> {
+ let wallet = ctx
+ .wallet
+ .as_deref_mut()
+ .ok_or(Error::Generic("wallet is required".to_string()))?;
+ let address_info = wallet.next_unused_address(KeychainKind::External);
+ Ok(AddressResult::from(address_info))
+ }
+}
- if let Some(utxos) = utxos {
- tx_builder.add_utxos(&utxos[..]).unwrap();
- }
+#[derive(Parser, Debug, PartialEq, Clone)]
+pub struct UnspentCommand {}
- if let Some(unspendable) = unspendable {
- tx_builder.unspendable(unspendable);
- }
+impl AppCommand for UnspentCommand {
+ type Output = UnspentListResult;
- if let Some(base64_data) = add_data {
- let op_return_data = BASE64_STANDARD.decode(base64_data).unwrap();
- tx_builder.add_data(&PushBytesBuf::try_from(op_return_data).unwrap());
- } else if let Some(string_data) = add_string {
- let data = PushBytesBuf::try_from(string_data.as_bytes().to_vec()).unwrap();
- tx_builder.add_data(&data);
- }
+ fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error> {
+ let wallet = ctx
+ .wallet
+ .as_deref_mut()
+ .ok_or(Error::Generic("Wallet is required".to_string()))?;
+ let utxos = wallet
+ .list_unspent()
+ .map(|utxo| UnspentDetails::from_local_output(&utxo, ctx.network))
+ .collect();
- let policies = vec![
- external_policy.map(|p| (p, KeychainKind::External)),
- internal_policy.map(|p| (p, KeychainKind::Internal)),
- ];
+ Ok(UnspentListResult(utxos))
+ }
+}
- for (policy, keychain) in policies.into_iter().flatten() {
- let policy = serde_json::from_str::<BTreeMap<String, Vec<usize>>>(&policy)?;
- tx_builder.policy_path(policy, keychain);
- }
+#[derive(Parser, Debug, PartialEq, Clone)]
+pub struct TransactionsCommand {}
+
+impl AppCommand for TransactionsCommand {
+ type Output = TransactionListResult;
+
+ fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error> {
+ let wallet = ctx
+ .wallet
+ .as_deref_mut()
+ .ok_or(Error::Generic("wallet required".to_string()))?;
+
+ let transactions = wallet.transactions();
+
+ let txns: Vec<TransactionDetails> = transactions
+ .map(|tx| {
+ let total_value = tx
+ .tx_node
+ .output
+ .iter()
+ .map(|output| output.value.to_sat())
+ .sum::<u64>();
+
+ TransactionDetails {
+ txid: tx.tx_node.txid.to_string(),
+ is_coinbase: tx.tx_node.is_coinbase(),
+ wtxid: tx.tx_node.compute_wtxid().to_string(),
+ version: serde_json::to_value(tx.tx_node.version).unwrap_or(json!(1)),
+ version_display: tx.tx_node.version.to_string(),
+ is_rbf: tx.tx_node.is_explicitly_rbf(),
+ inputs: serde_json::to_value(&tx.tx_node.input).unwrap_or_default(),
+ outputs: serde_json::to_value(&tx.tx_node.output).unwrap_or_default(),
+ input_count: tx.tx_node.input.len(),
+ output_count: tx.tx_node.output.len(),
+ total_value,
+ }
+ })
+ .collect();
- let psbt = tx_builder.finish()?;
+ Ok(TransactionListResult(txns))
+ }
+}
- let result = PsbtResult::new(&psbt, wallet_opts.verbose, None);
- result.format(pretty)
- }
- BumpFee {
- txid,
- shrink_address,
- offline_signer,
- utxos,
- unspendable,
- fee_rate,
- } => {
- let txid = Txid::from_str(txid.as_str())?;
-
- let mut tx_builder = wallet.build_fee_bump(txid)?;
- let fee_rate =
- FeeRate::from_sat_per_vb(fee_rate as u64).unwrap_or(FeeRate::BROADCAST_MIN);
- tx_builder.fee_rate(fee_rate);
+#[derive(Parser, Debug, PartialEq, Clone)]
+pub struct BalanceCommand {}
- if let Some(address) = shrink_address {
- let script_pubkey = address.script_pubkey();
- tx_builder.drain_to(script_pubkey);
- }
+impl AppCommand for BalanceCommand {
+ type Output = BalanceResult;
- if offline_signer {
- tx_builder.include_output_redeem_witness_script();
- }
+ fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error> {
+ let wallet = ctx
+ .wallet
+ .as_deref_mut()
+ .ok_or_else(|| Error::Generic("Wallet required".into()))?;
+ let balance = wallet.balance();
+ Ok(BalanceResult::from(balance))
+ }
+}
+#[derive(Debug, Parser, Clone, PartialEq)]
+pub struct CreateTxCommand {
+ /// Adds a recipient to the transaction.
+ #[arg(env = "ADDRESS:SAT", long = "to", required = true, value_parser = parse_recipient)]
+ pub recipients: Vec<(ScriptBuf, u64)>,
+
+ /// Sends all the funds (or all the selected utxos). Requires only one recipient with value 0.
+ #[arg(long = "send_all", short = 'a')]
+ pub send_all: bool,
+
+ /// Enables Replace-By-Fee (BIP125).
+ #[arg(long = "enable_rbf", short = 'r', default_value_t = true)]
+ pub enable_rbf: bool,
+
+ /// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output.
+ #[arg(long = "offline_signer")]
+ pub offline_signer: bool,
+
+ /// Selects which utxos *must* be spent.
+ #[arg(env = "MUST_SPEND_TXID:VOUT", long = "utxos", value_parser = parse_outpoint)]
+ pub utxos: Option<Vec<OutPoint>>,
+
+ /// Marks a utxo as unspendable.
+ #[arg(env = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)]
+ pub unspendable: Option<Vec<OutPoint>>,
+
+ /// Fee rate to use in sat/vbyte.
+ #[arg(env = "SATS_VBYTE", short = 'f', long = "fee_rate")]
+ pub fee_rate: Option<f32>,
+
+ /// Selects which policy should be used to satisfy the external descriptor.
+ #[arg(env = "EXT_POLICY", long = "external_policy")]
+ pub external_policy: Option<String>,
+
+ /// Selects which policy should be used to satisfy the internal descriptor.
+ #[arg(env = "INT_POLICY", long = "internal_policy")]
+ pub internal_policy: Option<String>,
+
+ /// Optionally create an OP_RETURN output containing given String in utf8 encoding (max 80 bytes)
+ #[arg(
+ env = "ADD_STRING",
+ long = "add_string",
+ short = 's',
+ conflicts_with = "add_data"
+ )]
+ pub add_string: Option<String>,
+
+ /// Optionally create an OP_RETURN output containing given base64 encoded String. (max 80 bytes)
+ #[arg(
+ env = "ADD_DATA",
+ long = "add_data",
+ short = 'o',
+ conflicts_with = "add_string"
+ )]
+ pub add_data: Option<String>,
+}
- if let Some(utxos) = utxos {
- tx_builder.add_utxos(&utxos[..]).unwrap();
- }
+impl AppCommand for CreateTxCommand {
+ type Output = PsbtResult;
+
+ fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error> {
+ let wallet = ctx
+ .wallet
+ .as_deref_mut()
+ .ok_or_else(|| Error::Generic("Wallet required".into()))?;
+
+ let mut tx_builder = wallet.build_tx();
+
+ if self.send_all {
+ tx_builder
+ .drain_wallet()
+ .drain_to(self.recipients[0].0.clone());
+ } else {
+ let recipients = self
+ .recipients
+ .clone()
+ .into_iter()
+ .map(|(script, amount)| (script, Amount::from_sat(amount)))
+ .collect();
+ tx_builder.set_recipients(recipients);
+ }
- if let Some(unspendable) = unspendable {
- tx_builder.unspendable(unspendable);
- }
+ if !self.enable_rbf {
+ tx_builder.set_exact_sequence(Sequence::MAX);
+ }
- let psbt = tx_builder.finish()?;
+ if self.offline_signer {
+ tx_builder.include_output_redeem_witness_script();
+ }
- let result = PsbtResult::new(&psbt, wallet_opts.verbose, None);
- result.format(pretty)
+ if let Some(fee_rate) = self.fee_rate
+ && let Some(fee_rate) = FeeRate::from_sat_per_vb(fee_rate as u64)
+ {
+ tx_builder.fee_rate(fee_rate);
}
- Policies => {
- let external_policy = wallet.policies(KeychainKind::External)?;
- let internal_policy = wallet.policies(KeychainKind::Internal)?;
- let result = KeychainPair::<serde_json::Value> {
- external: serde_json::to_value(&external_policy).unwrap_or(json!(null)),
- internal: serde_json::to_value(&internal_policy).unwrap_or(json!(null)),
- };
- result.format(cli_opts.pretty)
+
+ if let Some(utxos) = &self.utxos {
+ tx_builder.add_utxos(&utxos[..]).unwrap();
}
- PublicDescriptor => {
- let result = KeychainPair::<String> {
- external: wallet.public_descriptor(KeychainKind::External).to_string(),
- internal: wallet.public_descriptor(KeychainKind::Internal).to_string(),
- };
- result.format(cli_opts.pretty)
+
+ if let Some(unspendable) = &self.unspendable {
+ tx_builder.unspendable(unspendable.to_vec());
}
- Sign {
- psbt,
- assume_height,
- trust_witness_utxo,
- } => {
- let psbt_bytes = BASE64_STANDARD.decode(psbt)?;
- let mut psbt = Psbt::deserialize(&psbt_bytes)?;
- let signopt = SignOptions {
- assume_height,
- trust_witness_utxo: trust_witness_utxo.unwrap_or(false),
- ..Default::default()
- };
- let finalized = wallet.sign(&mut psbt, signopt)?;
-
- let result = PsbtResult::new(&psbt, wallet_opts.verbose, Some(finalized));
- result.format(pretty)
+
+ if let Some(base64_data) = &self.add_data {
+ let op_return_data = BASE64_STANDARD.decode(base64_data).unwrap();
+ tx_builder.add_data(&PushBytesBuf::try_from(op_return_data).unwrap());
+ } else if let Some(string_data) = &self.add_string {
+ let data = PushBytesBuf::try_from(string_data.as_bytes().to_vec()).unwrap();
+ tx_builder.add_data(&data);
}
- ExtractPsbt { psbt } => {
- let psbt_serialized = BASE64_STANDARD.decode(psbt)?;
- let psbt = Psbt::deserialize(&psbt_serialized)?;
- let raw_tx = psbt.extract_tx()?;
- let result = RawPsbt::new(&raw_tx);
- result.format(pretty)
+
+ let policies = vec![
+ self.external_policy
+ .clone()
+ .map(|p| (p, KeychainKind::External)),
+ self.internal_policy
+ .clone()
+ .map(|p| (p, KeychainKind::Internal)),
+ ];
+
+ for (policy, keychain) in policies.into_iter().flatten() {
+ let policy = serde_json::from_str::<BTreeMap<String, Vec<usize>>>(&policy)?;
+ tx_builder.policy_path(policy, keychain);
}
- FinalizePsbt {
- psbt,
- assume_height,
- trust_witness_utxo,
- } => {
- let psbt_bytes = BASE64_STANDARD.decode(psbt)?;
- let mut psbt: Psbt = Psbt::deserialize(&psbt_bytes)?;
-
- let signopt = SignOptions {
- assume_height,
- trust_witness_utxo: trust_witness_utxo.unwrap_or(false),
- ..Default::default()
- };
- let finalized = wallet.finalize_psbt(&mut psbt, signopt)?;
-
- let result = PsbtResult::new(&psbt, wallet_opts.verbose, Some(finalized));
- result.format(pretty)
+
+ let psbt = tx_builder.finish()?;
+
+ // let psbt_base64 = BASE64_STANDARD.encode(psbt.serialize());
+
+ Ok(PsbtResult::new(&psbt, false, Some(false)))
+ }
+}
+
+#[derive(Debug, Parser, Clone, PartialEq)]
+pub struct BumpFeeCommand {
+ /// TXID of the transaction to update.
+ #[arg(env = "TXID", long = "txid")]
+ pub txid: String,
+
+ /// Allows the wallet to reduce the amount to the specified address in order to increase fees.
+ #[arg(env = "SHRINK_ADDRESS", long = "shrink", value_parser = parse_address)]
+ pub shrink_address: Option<Address>,
+
+ /// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output.
+ #[arg(long = "offline_signer")]
+ pub offline_signer: bool,
+
+ /// Selects which utxos *must* be added to the tx. Unconfirmed utxos cannot be used.
+ #[arg(env = "MUST_SPEND_TXID:VOUT", long = "utxos", value_parser = parse_outpoint)]
+ pub utxos: Option<Vec<OutPoint>>,
+
+ /// Marks an utxo as unspendable, in case more inputs are needed to cover the extra fees.
+ #[arg(env = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)]
+ pub unspendable: Option<Vec<OutPoint>>,
+
+ /// The new targeted fee rate in sat/vbyte.
+ #[arg(
+ env = "SATS_VBYTE",
+ short = 'f',
+ long = "fee_rate",
+ default_value = "1.0"
+ )]
+ pub fee_rate: f32,
+}
+
+impl AppCommand for BumpFeeCommand {
+ type Output = PsbtResult;
+
+ fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error> {
+ let wallet = ctx
+ .wallet
+ .as_deref_mut()
+ .ok_or_else(|| Error::Generic("Wallet required".into()))?;
+
+ let txid = Txid::from_str(self.txid.as_str())?;
+
+ let mut tx_builder = wallet.build_fee_bump(txid)?;
+ let fee_rate =
+ FeeRate::from_sat_per_vb(self.fee_rate as u64).unwrap_or(FeeRate::BROADCAST_MIN);
+ tx_builder.fee_rate(fee_rate);
+
+ if let Some(address) = &self.shrink_address {
+ let script_pubkey = address.script_pubkey();
+ tx_builder.drain_to(script_pubkey);
}
- CombinePsbt { psbt } => {
- let mut psbts = psbt
- .iter()
- .map(|s| {
- let psbt = BASE64_STANDARD.decode(s)?;
- Ok(Psbt::deserialize(&psbt)?)
- })
- .collect::<Result<Vec<_>, Error>>()?;
-
- let init_psbt = psbts
- .pop()
- .ok_or_else(|| Error::Generic("Invalid PSBT input".to_string()))?;
- let final_psbt = psbts.into_iter().try_fold::<_, _, Result<Psbt, Error>>(
- init_psbt,
- |mut acc, x| {
- let _ = acc.combine(x);
- Ok(acc)
- },
- )?;
- let result = PsbtResult::new(&final_psbt, wallet_opts.verbose, None);
- result.format(pretty)
+
+ if self.offline_signer {
+ tx_builder.include_output_redeem_witness_script();
}
- #[cfg(feature = "bip322")]
- SignMessage {
- message,
- signature_type,
- address,
- utxos,
- } => {
- let address: Address = parse_address(&address)?;
- let signature_format = parse_signature_format(&signature_type)?;
-
- if !wallet.is_mine(address.script_pubkey()) {
- return Err(Error::Generic(format!(
- "Address {} does not belong to this wallet.",
- address
- )));
- }
- let proof: MessageProof =
- wallet.sign_message(message.as_str(), signature_format, &address, utxos)?;
+ if let Some(utxos) = &self.utxos {
+ tx_builder.add_utxos(&utxos[..]).unwrap();
+ }
- Ok(json!({"proof": proof.to_base64()}).to_string())
+ if let Some(unspendable) = &self.unspendable {
+ tx_builder.unspendable(unspendable.to_vec());
}
- #[cfg(feature = "bip322")]
- VerifyMessage {
- proof,
- message,
- address,
- } => {
- let address: Address = parse_address(&address)?;
- let parsed_proof: MessageProof = MessageProof::from_base64(&proof)
- .map_err(|e| Error::Generic(format!("Invalid proof: {e}")))?;
-
- let is_valid: MessageVerificationResult =
- wallet.verify_message(&parsed_proof, &message, &address)?;
-
- Ok(json!({
- "valid": is_valid.valid,
- "proven_amount": is_valid.proven_amount.map(|a| a.to_sat()) // optional field
+
+ let psbt = tx_builder.finish()?;
+
+ // let psbt_base64 = BASE64_STANDARD.encode(psbt.serialize());
+
+ Ok(PsbtResult::new(&psbt, false, Some(false)))
+ }
+}
+
+#[derive(Parser, Debug, PartialEq, Clone)]
+pub struct PoliciesCommand {}
+
+impl AppCommand for PoliciesCommand {
+ type Output = KeychainPair<serde_json::Value>;
+
+ fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error> {
+ let wallet = ctx
+ .wallet
+ .as_deref_mut()
+ .ok_or(Error::Generic("Wallet required".into()))?;
+ let external_policy = wallet.policies(KeychainKind::External)?;
+ let internal_policy = wallet.policies(KeychainKind::Internal)?;
+
+ Ok(KeychainPair {
+ external: serde_json::to_value(&external_policy).unwrap_or(serde_json::json!(null)),
+ internal: serde_json::to_value(&internal_policy).unwrap_or(serde_json::json!(null)),
+ })
+ }
+}
+
+#[derive(Parser, Debug, PartialEq, Clone)]
+pub struct PublicDescriptorCommand {}
+
+impl AppCommand for PublicDescriptorCommand {
+ type Output = KeychainPair<String>;
+
+ fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error> {
+ let wallet = ctx
+ .wallet
+ .as_deref_mut()
+ .ok_or(Error::Generic("Wallet required".into()))?;
+ Ok(KeychainPair {
+ external: wallet.public_descriptor(KeychainKind::External).to_string(),
+ internal: wallet.public_descriptor(KeychainKind::Internal).to_string(),
+ })
+ }
+}
+
+#[derive(Debug, Parser, Clone, PartialEq)]
+pub struct SignCommand {
+ /// Sets the PSBT to sign.
+ #[arg(env = "BASE64_PSBT")]
+ pub psbt: String,
+
+ /// Assume the blockchain has reached a specific height. This affects the transaction finalization, if there are timelocks in the descriptor.
+ #[arg(env = "HEIGHT", long = "assume_height")]
+ pub assume_height: Option<u32>,
+
+ /// Whether the signer should trust the witness_utxo, if the non_witness_utxo hasn’t been provided.
+ #[arg(env = "WITNESS", long = "trust_witness_utxo")]
+ pub trust_witness_utxo: Option<bool>,
+}
+
+impl AppCommand for SignCommand {
+ type Output = PsbtResult;
+
+ fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error> {
+ let wallet = ctx
+ .wallet
+ .as_deref_mut()
+ .ok_or(Error::Generic("Wallet required".into()))?;
+ let psbt_bytes = BASE64_STANDARD
+ .decode(&self.psbt)
+ .map_err(|e| Error::Generic(e.to_string()))?;
+ let mut psbt = Psbt::deserialize(&psbt_bytes).map_err(|e| Error::Generic(e.to_string()))?;
+
+ let signopt = SignOptions {
+ assume_height: self.assume_height,
+ trust_witness_utxo: self.trust_witness_utxo.unwrap_or(false),
+ ..Default::default()
+ };
+ let finalized = wallet.sign(&mut psbt, signopt)?;
+ Ok(PsbtResult::new(&psbt, false, Some(finalized)))
+ }
+}
+
+#[derive(Debug, Parser, Clone, PartialEq)]
+pub struct ExtractPsbtCommand {
+ /// Sets the PSBT to extract
+ #[arg(env = "BASE64_PSBT")]
+ pub psbt: String,
+}
+
+impl AppCommand for ExtractPsbtCommand {
+ type Output = RawPsbt;
+
+ fn execute(&self, _ctx: &mut AppContext) -> Result<Self::Output, Error> {
+ let psbt_serialized = BASE64_STANDARD.decode(self.psbt.clone())?;
+ let psbt = Psbt::deserialize(&psbt_serialized)?;
+ let raw_tx = psbt.extract_tx()?;
+
+ Ok(RawPsbt::new(&raw_tx))
+ }
+}
+
+#[derive(Debug, Parser, Clone, PartialEq)]
+pub struct FinalizePsbtCommand {
+ /// Sets the PSBT to finalize.
+ #[arg(env = "BASE64_PSBT")]
+ pub psbt: String,
+
+ /// Assume the blockchain has reached a specific height.
+ #[arg(env = "HEIGHT", long = "assume_height")]
+ pub assume_height: Option<u32>,
+
+ /// Whether the signer should trust the witness_utxo, if the non_witness_utxo hasn’t been provided.
+ #[arg(env = "WITNESS", long = "trust_witness_utxo")]
+ pub trust_witness_utxo: Option<bool>,
+}
+
+impl AppCommand for FinalizePsbtCommand {
+ type Output = PsbtResult;
+
+ fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error> {
+ let wallet = ctx
+ .wallet
+ .as_deref_mut()
+ .ok_or(Error::Generic("Wallet required".into()))?;
+ let psbt_bytes = BASE64_STANDARD
+ .decode(&self.psbt)
+ .map_err(|e| Error::Generic(e.to_string()))?;
+ let mut psbt = Psbt::deserialize(&psbt_bytes).map_err(|e| Error::Generic(e.to_string()))?;
+
+ let signopt = SignOptions {
+ assume_height: self.assume_height,
+ trust_witness_utxo: self.trust_witness_utxo.unwrap_or(false),
+ ..Default::default()
+ };
+
+ let finalized = wallet.finalize_psbt(&mut psbt, signopt)?;
+
+ Ok(PsbtResult::new(&psbt, false, Some(finalized)))
+ }
+}
+
+#[derive(Debug, Parser, Clone, PartialEq)]
+pub struct CombinePsbtCommand {
+ /// Add one PSBT to combine. This option can be repeated multiple times, one for each PSBT.
+ #[arg(env = "BASE64_PSBT", required = true)]
+ pub psbt: Vec<String>,
+}
+
+impl AppCommand for CombinePsbtCommand {
+ type Output = PsbtResult;
+
+ fn execute(&self, _ctx: &mut AppContext) -> Result<Self::Output, Error> {
+ let mut psbts = self
+ .psbt
+ .iter()
+ .map(|s| {
+ let psbt = BASE64_STANDARD.decode(s)?;
+ Ok(Psbt::deserialize(&psbt)?)
})
- .to_string())
+ .collect::<Result<Vec<_>, Error>>()?;
+
+ let init_psbt = psbts
+ .pop()
+ .ok_or_else(|| Error::Generic("Invalid PSBT input".to_string()))?;
+ let final_psbt =
+ psbts
+ .into_iter()
+ .try_fold::<_, _, Result<Psbt, Error>>(init_psbt, |mut acc, x| {
+ let _ = acc.combine(x);
+ Ok(acc)
+ })?;
+
+ Ok(PsbtResult::new(&final_psbt, false, None))
+ }
+}
+
+#[cfg(feature = "bip322")]
+#[derive(Debug, Parser, Clone, PartialEq)]
+pub struct SignMessageCommand {
+ /// The message to sign
+ #[arg(long)]
+ pub message: String,
+
+ /// The signature format (e.g., Legacy, Simple, Full)
+ #[arg(long, default_value = "simple")]
+ pub signature_type: String,
+
+ /// Address to sign
+ #[arg(long)]
+ pub address: String,
+
+ /// Optional list of specific UTXOs for proof-of-funds (only for `FullWithProofOfFunds`)
+ #[arg(long)]
+ pub utxos: Option<Vec<OutPoint>>,
+}
+
+#[cfg(feature = "bip322")]
+impl AppCommand for SignMessageCommand {
+ type Output = MessageResult;
+
+ fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error> {
+ let wallet = ctx
+ .wallet
+ .as_deref_mut()
+ .ok_or(Error::Generic("Wallet required".into()))?;
+ let address: Address = parse_address(&self.address)?;
+ let signature_format = parse_signature_format(&self.signature_type)?;
+
+ if !wallet.is_mine(address.script_pubkey()) {
+ return Err(Error::Generic(format!(
+ "Address {} does not belong to this wallet.",
+ address
+ )));
}
+
+ let proof = wallet.sign_message(
+ self.message.as_str(),
+ signature_format,
+ &address,
+ self.utxos.clone(),
+ )?;
+
+ Ok(MessageResult {
+ proof: Some(proof.to_base64()),
+ ..Default::default()
+ })
+ }
+}
+
+// #[cfg(feature = "bip322")]
+#[derive(Debug, Parser, Clone, PartialEq)]
+pub struct VerifyMessageCommand {
+ /// The signature proof to verify
+ #[arg(long)]
+ pub proof: String,
+
+ /// The message that was signed
+ #[arg(long)]
+ pub message: String,
+
+ /// The address associated with the signature
+ #[arg(long)]
+ pub address: String,
+}
+
+#[cfg(feature = "bip322")]
+impl AppCommand for VerifyMessageCommand {
+ type Output = MessageResult;
+
+ fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error> {
+ let wallet = ctx
+ .wallet
+ .as_deref_mut()
+ .ok_or(Error::Generic("Wallet required".into()))?;
+
+ let address: Address = parse_address(&self.address)?;
+
+ let parsed_proof = MessageProof::from_base64(&self.proof)
+ .map_err(|e| Error::Generic(format!("Invalid proof format: {e}")))?;
+
+ let is_valid = wallet.verify_message(&parsed_proof, &self.message, &address)?;
+
+ Ok(MessageResult {
+ valid: Some(is_valid.valid),
+ proven_amount: is_valid.proven_amount.map(|a| a.to_sat()),
+ ..Default::default()
+ })
}
}
+use clap::Parser;
+
#[cfg(feature = "electrum")]
-use crate::backend::BlockchainClient::Electrum;
-#[cfg(feature = "cbf")]
-use crate::backend::BlockchainClient::KyotoClient;
+use crate::client::BlockchainClient::Electrum;
#[cfg(feature = "rpc")]
-use crate::backend::BlockchainClient::RpcClient;
+use crate::client::BlockchainClient::RpcClient;
+#[cfg(feature = "cbf")]
+use crate::client::{BlockchainClient::KyotoClient, sync_kyoto_client};
#[cfg(feature = "esplora")]
-use {crate::backend::BlockchainClient::Esplora, bdk_esplora::EsploraAsyncExt};
+use {crate::client::BlockchainClient::Esplora, bdk_esplora::EsploraAsyncExt};
#[cfg(feature = "rpc")]
use {
bdk_bitcoind_rpc::{Emitter, NO_EXPECTED_MEMPOOL_TXS, bitcoincore_rpc::RpcApi},
feature = "rpc"
))]
use {
- crate::backend::{BlockchainClient, sync_kyoto_client},
- crate::commands::OnlineWalletSubCommand::*,
+ crate::commands::OnlineWalletSubCommand,
crate::error::BDKCliError as Error,
- crate::payjoin::PayjoinManager,
- crate::payjoin::ohttp::RelayManager,
+ crate::handlers::{AppContext, AsyncCommand},
+ crate::payjoin::{PayjoinManager, ohttp::RelayManager},
crate::utils::is_final,
- bdk_wallet::Wallet,
+ crate::utils::output::FormatOutput,
+ crate::utils::types::{StatusResult, TransactionResult},
bdk_wallet::bitcoin::{
- Psbt, Transaction, Txid, base64::Engine, base64::prelude::BASE64_STANDARD,
- consensus::Decodable, hex::FromHex,
+ Psbt, Transaction, base64::Engine, base64::prelude::BASE64_STANDARD, consensus::Decodable,
+ hex::FromHex,
},
- serde_json::json,
std::sync::{Arc, Mutex},
};
+#[cfg(any(
+ feature = "electrum",
+ feature = "esplora",
+ feature = "cbf",
+ feature = "rpc"
+))]
+impl OnlineWalletSubCommand {
+ pub async fn execute(&self, ctx: &mut AppContext<'_>) -> Result<(), Error> {
+ match self {
+ OnlineWalletSubCommand::FullScan(full_scan_command) => {
+ full_scan_command.execute(ctx).await?.print()
+ }
+ OnlineWalletSubCommand::Sync(sync_command) => sync_command.execute(ctx).await?.print(),
+ OnlineWalletSubCommand::Broadcast(broadcast_command) => {
+ broadcast_command.execute(ctx).await?.print()
+ }
+ OnlineWalletSubCommand::ReceivePayjoin(receive_payjoin_command) => {
+ receive_payjoin_command.execute(ctx).await?.print()
+ }
+ OnlineWalletSubCommand::SendPayjoin(send_payjoin_command) => {
+ send_payjoin_command.execute(ctx).await?.print()
+ }
+ }
+ }
+}
+
+#[derive(Parser, Debug, PartialEq, Clone, Eq)]
+pub struct FullScanCommand {
+ /// Stop searching addresses for transactions after finding an unused gap of this length.
+ #[arg(env = "STOP_GAP", long = "scan-stop-gap", default_value = "20")]
+ stop_gap: usize,
+ // #[clap(long, default_value = "5")]
+ // pub parallel_request: usize,
+}
-/// Execute an online wallet sub-command
-///
-/// Online wallet sub-commands are described in [`OnlineWalletSubCommand`].
#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "cbf",
feature = "rpc"
))]
-pub(crate) async fn handle_online_wallet_subcommand(
- wallet: &mut Wallet,
- client: &BlockchainClient,
- online_subcommand: crate::commands::OnlineWalletSubCommand,
-) -> Result<String, Error> {
- match online_subcommand {
- FullScan {
- stop_gap: _stop_gap,
- } => {
- #[cfg(any(feature = "electrum", feature = "esplora"))]
- let request = wallet.start_full_scan().inspect({
- let mut stdout = std::io::stdout();
- let mut once = HashSet::<KeychainKind>::new();
- move |k, spk_i, _| {
- if once.insert(k) {
- print!("\nScanning keychain [{k:?}]");
- }
- print!(" {spk_i:<3}");
- stdout.flush().expect("must flush");
- }
- });
- match client {
- #[cfg(feature = "electrum")]
- Electrum { client, batch_size } => {
- // Populate the electrum client's transaction cache so it doesn't re-download transaction we
- // already have.
- client
- .populate_tx_cache(wallet.tx_graph().full_txs().map(|tx_node| tx_node.tx));
-
- let update = client.full_scan(request, _stop_gap, *batch_size, false)?;
- wallet.apply_update(update)?;
- }
- #[cfg(feature = "esplora")]
- Esplora {
- client,
- parallel_requests,
- } => {
- let update = client
- .full_scan(request, _stop_gap, *parallel_requests)
- .await
- .map_err(|e| *e)?;
- wallet.apply_update(update)?;
+impl AsyncCommand for FullScanCommand {
+ type Output = StatusResult;
+
+ async fn execute(&self, ctx: &mut AppContext<'_>) -> Result<Self::Output, Error> {
+ let wallet = ctx
+ .wallet
+ .as_deref_mut()
+ .ok_or(Error::Generic("Wallet required".into()))?;
+ let client = ctx
+ .client
+ .ok_or(Error::Generic("Online client required".into()))?;
+
+ #[cfg(any(feature = "electrum", feature = "esplora"))]
+ let request = wallet.start_full_scan().inspect({
+ let mut stdout = std::io::stdout();
+ let mut once = HashSet::<KeychainKind>::new();
+ move |k, spk_i, _| {
+ if once.insert(k) {
+ print!("\nScanning keychain [{k:?}]");
}
+ print!(" {spk_i:<3}");
+ stdout.flush().expect("must flush");
+ }
+ });
- #[cfg(feature = "rpc")]
- RpcClient { client } => {
- let blockchain_info = client.get_blockchain_info()?;
-
- let genesis_block =
- bdk_wallet::bitcoin::constants::genesis_block(wallet.network());
- let genesis_cp = CheckPoint::new(BlockId {
- height: 0,
- hash: genesis_block.block_hash(),
- });
- let mut emitter = Emitter::new(
- client.as_ref(),
- genesis_cp.clone(),
- genesis_cp.height(),
- NO_EXPECTED_MEMPOOL_TXS,
- );
-
- while let Some(block_event) = emitter.next_block()? {
- if block_event.block_height() % 10_000 == 0 {
- let percent_done = f64::from(block_event.block_height())
- / f64::from(blockchain_info.headers as u32)
- * 100f64;
- println!(
- "Applying block at height: {}, {:.2}% done.",
- block_event.block_height(),
- percent_done
- );
- }
-
- wallet.apply_block_connected_to(
- &block_event.block,
+ match client {
+ #[cfg(feature = "electrum")]
+ Electrum { client, batch_size } => {
+ client.populate_tx_cache(wallet.tx_graph().full_txs().map(|tx_node| tx_node.tx));
+ let update = client.full_scan(request, self.stop_gap, *batch_size, false)?;
+ wallet.apply_update(update)?;
+ }
+
+ #[cfg(feature = "esplora")]
+ Esplora {
+ client,
+ parallel_requests,
+ } => {
+ let update = client
+ .full_scan(request, self.stop_gap, *parallel_requests)
+ .await
+ .map_err(|e| *e)?;
+ wallet.apply_update(update)?;
+ }
+ #[cfg(feature = "rpc")]
+ RpcClient { client } => {
+ let blockchain_info = client.get_blockchain_info()?;
+
+ let genesis_block = bdk_wallet::bitcoin::constants::genesis_block(wallet.network());
+ let genesis_cp = CheckPoint::new(BlockId {
+ height: 0,
+ hash: genesis_block.block_hash(),
+ });
+ let mut emitter = Emitter::new(
+ client.as_ref(),
+ genesis_cp.clone(),
+ genesis_cp.height(),
+ NO_EXPECTED_MEMPOOL_TXS,
+ );
+
+ while let Some(block_event) = emitter.next_block()? {
+ if block_event.block_height() % 10_000 == 0 {
+ let percent_done = f64::from(block_event.block_height())
+ / f64::from(blockchain_info.headers as u32)
+ * 100f64;
+ println!(
+ "Applying block at height: {}, {:.2}% done.",
block_event.block_height(),
- block_event.connected_to(),
- )?;
+ percent_done
+ );
}
- let mempool_txs = emitter.mempool()?;
- wallet.apply_unconfirmed_txs(mempool_txs.update);
- }
- #[cfg(feature = "cbf")]
- KyotoClient { client } => {
- sync_kyoto_client(wallet, client).await?;
+ wallet.apply_block_connected_to(
+ &block_event.block,
+ block_event.block_height(),
+ block_event.connected_to(),
+ )?;
}
+
+ let mempool_txs = emitter.mempool()?;
+ wallet.apply_unconfirmed_txs(mempool_txs.update);
+ }
+
+ #[cfg(feature = "cbf")]
+ KyotoClient { client } => {
+ sync_kyoto_client(wallet, client).await?;
}
- Ok(serde_json::to_string_pretty(&json!({}))?)
- }
- Sync => {
- sync_wallet(client, wallet).await?;
- Ok(serde_json::to_string_pretty(&json!({}))?)
- }
- Broadcast { psbt, tx } => {
- let tx = match (psbt, tx) {
- (Some(psbt), None) => {
- let psbt = BASE64_STANDARD
- .decode(psbt)
- .map_err(|e| Error::Generic(e.to_string()))?;
- let psbt: Psbt = Psbt::deserialize(&psbt)?;
- is_final(&psbt)?;
- psbt.extract_tx()?
- }
- (None, Some(tx)) => {
- let tx_bytes = Vec::<u8>::from_hex(&tx)?;
- Transaction::consensus_decode(&mut tx_bytes.as_slice())?
- }
- (Some(_), Some(_)) => panic!("Both `psbt` and `tx` options not allowed"),
- (None, None) => panic!("Missing `psbt` and `tx` option"),
- };
- let txid = broadcast_transaction(client, tx).await?;
- Ok(serde_json::to_string_pretty(&json!({ "txid": txid }))?)
- }
- ReceivePayjoin {
- amount,
- directory,
- ohttp_relay,
- max_fee_rate,
- } => {
- let relay_manager = Arc::new(Mutex::new(RelayManager::new()));
- let mut payjoin_manager = PayjoinManager::new(wallet, relay_manager);
- return payjoin_manager
- .receive_payjoin(amount, directory, max_fee_rate, ohttp_relay, client)
- .await;
- }
- SendPayjoin {
- uri,
- ohttp_relay,
- fee_rate,
- } => {
- let relay_manager = Arc::new(Mutex::new(RelayManager::new()));
- let mut payjoin_manager = PayjoinManager::new(wallet, relay_manager);
- return payjoin_manager
- .send_payjoin(uri, fee_rate, ohttp_relay, client)
- .await;
}
+ Ok(StatusResult::new("Full scan completed successfully."))
}
}
+#[derive(Parser, Debug, PartialEq, Eq, Clone)]
+pub struct SyncCommand {}
+
#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "cbf",
feature = "rpc"
))]
-/// Syncs a given wallet using the blockchain client.
-pub async fn sync_wallet(client: &BlockchainClient, wallet: &mut Wallet) -> Result<(), Error> {
- // #[cfg(any(feature = "electrum", feature = "esplora"))]
- let request = wallet
- .start_sync_with_revealed_spks()
- .inspect(|item, progress| {
- let pc = (100 * progress.consumed()) as f32 / progress.total() as f32;
- eprintln!("[ SCANNING {pc:03.0}% ] {item}");
- });
- match client {
- #[cfg(feature = "electrum")]
- Electrum { client, batch_size } => {
- // Populate the electrum client's transaction cache so it doesn't re-download transaction we
- // already have.
- client.populate_tx_cache(wallet.tx_graph().full_txs().map(|tx_node| tx_node.tx));
-
- let update = client.sync(request, *batch_size, false)?;
- wallet
- .apply_update(update)
- .map_err(|e| Error::Generic(e.to_string()))
- }
- #[cfg(feature = "esplora")]
- Esplora {
- client,
- parallel_requests,
- } => {
- let update = client
- .sync(request, *parallel_requests)
- .await
- .map_err(|e| *e)?;
- wallet
- .apply_update(update)
- .map_err(|e| Error::Generic(e.to_string()))
- }
- #[cfg(feature = "rpc")]
- RpcClient { client } => {
- let blockchain_info = client.get_blockchain_info()?;
- let wallet_cp = wallet.latest_checkpoint();
-
- // reload the last 200 blocks in case of a reorg
- let emitter_height = wallet_cp.height().saturating_sub(200);
- let mut emitter = Emitter::new(
- client.as_ref(),
- wallet_cp,
- emitter_height,
+impl AsyncCommand for SyncCommand {
+ type Output = StatusResult;
+
+ async fn execute(&self, ctx: &mut AppContext<'_>) -> Result<Self::Output, Error> {
+ let wallet = ctx
+ .wallet
+ .as_deref_mut()
+ .ok_or(Error::Generic("Wallet required".to_string()))?;
+ let client = ctx
+ .client
+ .ok_or(Error::Generic("Client is required".to_string()))?;
+ #[cfg(any(feature = "electrum", feature = "esplora"))]
+ let request = wallet
+ .start_sync_with_revealed_spks()
+ .inspect(|item, progress| {
+ let pc = (100 * progress.consumed()) as f32 / progress.total() as f32;
+ eprintln!("[ SCANNING {pc:03.0}% ] {item}");
+ });
+
+ match client {
+ #[cfg(feature = "electrum")]
+ Electrum { client, batch_size } => {
+ // Populate the electrum client's transaction cache so it doesn't re-download transaction we
+ // already have.
+ client.populate_tx_cache(wallet.tx_graph().full_txs().map(|tx_node| tx_node.tx));
+
+ let update = client.sync(request, *batch_size, false)?;
+ wallet
+ .apply_update(update)
+ .map_err(|e| Error::Generic(e.to_string()))?;
+ }
+ #[cfg(feature = "esplora")]
+ Esplora {
+ client,
+ parallel_requests,
+ } => {
+ let update = client
+ .sync(request, *parallel_requests)
+ .await
+ .map_err(|e| *e)?;
wallet
- .tx_graph()
- .list_canonical_txs(
- wallet.local_chain(),
- wallet.local_chain().tip().block_id(),
- CanonicalizationParams::default(),
- )
- .filter(|tx| tx.chain_position.is_unconfirmed()),
- );
-
- while let Some(block_event) = emitter.next_block()? {
- if block_event.block_height() % 10_000 == 0 {
- let percent_done = f64::from(block_event.block_height())
- / f64::from(blockchain_info.headers as u32)
- * 100f64;
- println!(
- "Applying block at height: {}, {:.2}% done.",
+ .apply_update(update)
+ .map_err(|e| Error::Generic(e.to_string()))?;
+ }
+ #[cfg(feature = "rpc")]
+ RpcClient { client } => {
+ let blockchain_info = client.get_blockchain_info()?;
+ let wallet_cp = wallet.latest_checkpoint();
+
+ let emitter_height = wallet_cp.height().saturating_sub(200);
+ let mut emitter = Emitter::new(
+ client.as_ref(),
+ wallet_cp,
+ emitter_height,
+ wallet
+ .tx_graph()
+ .list_canonical_txs(
+ wallet.local_chain(),
+ wallet.local_chain().tip().block_id(),
+ CanonicalizationParams::default(),
+ )
+ .filter(|tx| tx.chain_position.is_unconfirmed()),
+ );
+
+ while let Some(block_event) = emitter.next_block()? {
+ if block_event.block_height() % 10_000 == 0 {
+ let percent_done = f64::from(block_event.block_height())
+ / f64::from(blockchain_info.headers as u32)
+ * 100f64;
+ println!(
+ "Applying block at height: {}, {:.2}% done.",
+ block_event.block_height(),
+ percent_done
+ );
+ }
+
+ wallet.apply_block_connected_to(
+ &block_event.block,
block_event.block_height(),
- percent_done
- );
+ block_event.connected_to(),
+ )?;
}
- wallet.apply_block_connected_to(
- &block_event.block,
- block_event.block_height(),
- block_event.connected_to(),
- )?;
+ let mempool_txs = emitter.mempool()?;
+ wallet.apply_unconfirmed_txs(mempool_txs.update);
}
-
- let mempool_txs = emitter.mempool()?;
- wallet.apply_unconfirmed_txs(mempool_txs.update);
- Ok(())
+ #[cfg(feature = "cbf")]
+ KyotoClient { client } => sync_kyoto_client(wallet, client)
+ .await
+ .map_err(|e| Error::Generic(e.to_string()))?,
}
- #[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."))
}
}
+#[derive(Parser, Debug, Clone, PartialEq, Eq)]
+pub struct BroadcastCommand {
+ /// Sets the PSBT to sign.
+ #[arg(
+ env = "BASE64_PSBT",
+ long = "psbt",
+ required_unless_present = "tx",
+ conflicts_with = "tx"
+ )]
+ psbt: Option<String>,
+ /// Sets the raw transaction to broadcast.
+ #[arg(
+ env = "RAWTX",
+ long = "tx",
+ required_unless_present = "psbt",
+ conflicts_with = "psbt"
+ )]
+ tx: Option<String>,
+}
+
#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "cbf",
feature = "rpc"
))]
-/// Broadcasts a given transaction using the blockchain client.
-pub async fn broadcast_transaction(
- client: &BlockchainClient,
- tx: Transaction,
-) -> Result<Txid, Error> {
- match client {
- #[cfg(feature = "electrum")]
- Electrum {
- client,
- batch_size: _,
- } => client
- .transaction_broadcast(&tx)
- .map_err(|e| Error::Generic(e.to_string())),
- #[cfg(feature = "esplora")]
- Esplora {
- client,
- parallel_requests: _,
- } => client
- .broadcast(&tx)
- .await
- .map(|()| tx.compute_txid())
- .map_err(|e| Error::Generic(e.to_string())),
- #[cfg(feature = "rpc")]
- RpcClient { client } => client
- .send_raw_transaction(&tx)
- .map_err(|e| Error::Generic(e.to_string())),
-
- #[cfg(feature = "cbf")]
- KyotoClient { client } => {
- let txid = tx.compute_txid();
- let wtxid = client
- .requester
- .broadcast_random(tx.clone())
- .await
- .map_err(|_| {
- tracing::warn!("Broadcast was unsuccessful");
- Error::Generic("Transaction broadcast timed out after 30 seconds".into())
- })?;
- tracing::info!("Successfully broadcast WTXID: {wtxid}");
- Ok(txid)
- }
+
+impl AsyncCommand for BroadcastCommand {
+ type Output = TransactionResult;
+
+ async fn execute(&self, ctx: &mut AppContext<'_>) -> Result<Self::Output, Error> {
+ let client = ctx
+ .client
+ .ok_or(Error::Generic("Online client required".into()))?;
+
+ let tx = match (&self.psbt, &self.tx) {
+ (Some(psbt), None) => {
+ let psbt = BASE64_STANDARD
+ .decode(psbt)
+ .map_err(|e| Error::Generic(e.to_string()))?;
+ let psbt: Psbt = Psbt::deserialize(&psbt)?;
+ is_final(&psbt)?;
+ psbt.extract_tx()?
+ }
+ (None, Some(tx)) => {
+ let tx_bytes = Vec::<u8>::from_hex(&tx)?;
+ Transaction::consensus_decode(&mut tx_bytes.as_slice())?
+ }
+ (Some(_), Some(_)) => {
+ return Err(Error::Generic(
+ "Both `psbt` and `tx` options are not allowed".into(),
+ ));
+ }
+ (None, None) => {
+ return Err(Error::Generic(
+ "Must provide either a `psbt` or `tx` to broadcast".into(),
+ ));
+ }
+ };
+
+ let txid = client.broadcast(tx).await?;
+
+ Ok(TransactionResult {
+ txid: txid.to_string(),
+ })
+ }
+}
+
+#[derive(Parser, Debug, Clone, PartialEq, Eq)]
+pub struct ReceivePayjoinCommand {
+ // /// The amount to receive in satoshis.
+ // pub amount: u64,
+
+ // /// The payjoin directory URL.
+ // #[clap(long)]
+ // pub directory: String,
+
+ // /// Optional OHTTP relay URLs for privacy.
+ // #[clap(long)]
+ // pub ohttp_relays: Vec<String>,
+
+ // /// Maximum fee rate in sat/vB to pay for the payjoin transaction.
+ // #[clap(long)]
+ // pub max_fee_rate: u64,
+ /// Amount to be received in sats.
+ #[arg(env = "PAYJOIN_AMOUNT", long = "amount", required = true)]
+ amount: u64,
+ /// Payjoin directory which will be used to store the PSBTs which are pending action
+ /// from one of the parties.
+ #[arg(env = "PAYJOIN_DIRECTORY", long = "directory", required = true)]
+ directory: String,
+ /// URL of the Payjoin OHTTP relay. Can be repeated multiple times to attempt the
+ /// operation with multiple relays for redundancy.
+ #[arg(env = "PAYJOIN_OHTTP_RELAY", long = "ohttp_relay", required = true)]
+ ohttp_relay: Vec<String>,
+ /// Maximum effective fee rate the receiver is willing to pay for their own input/output contributions.
+ #[arg(env = "PAYJOIN_RECEIVER_MAX_FEE_RATE", long = "max_fee_rate")]
+ max_fee_rate: Option<u64>,
+}
+
+#[cfg(any(
+ feature = "electrum",
+ feature = "esplora",
+ feature = "cbf",
+ feature = "rpc"
+))]
+impl AsyncCommand for ReceivePayjoinCommand {
+ type Output = StatusResult;
+
+ async fn execute(&self, ctx: &mut AppContext<'_>) -> Result<Self::Output, Error> {
+ let wallet = ctx
+ .wallet
+ .as_deref_mut()
+ .ok_or(Error::Generic("Wallet required".into()))?;
+ let client = ctx
+ .client
+ .ok_or(Error::Generic("Online client required".into()))?;
+
+ let relay_manager = Arc::new(Mutex::new(RelayManager::new()));
+ let mut payjoin_manager = PayjoinManager::new(wallet, relay_manager);
+ let result = payjoin_manager
+ .receive_payjoin(
+ self.amount,
+ self.directory.clone(),
+ self.max_fee_rate,
+ self.ohttp_relay.clone(),
+ client,
+ )
+ .await?;
+
+ Ok(StatusResult { message: result })
+ }
+}
+
+#[derive(Parser, Debug, Clone, PartialEq, Eq)]
+pub struct SendPayjoinCommand {
+ // /// The Payjoin URI string to send to.
+ // pub uri: String,
+
+ // /// Optional OHTTP relay URLs for privacy.
+ // #[clap(long)]
+ // pub ohttp_relays: Vec<String>,
+
+ // /// Fee rate in sat/vB for the transaction.
+ // #[clap(long, short)]
+ // pub fee_rate: u64,
+ /// BIP 21 URI for the Payjoin.
+ #[arg(env = "PAYJOIN_URI", long = "uri", required = true)]
+ uri: String,
+ /// URL of the Payjoin OHTTP relay. Can be repeated multiple times to attempt the
+ /// operation with multiple relays for redundancy.
+ #[arg(env = "PAYJOIN_OHTTP_RELAY", long = "ohttp_relay", required = true)]
+ ohttp_relay: Vec<String>,
+ /// Fee rate to use in sat/vbyte.
+ #[arg(
+ env = "PAYJOIN_SENDER_FEE_RATE",
+ short = 'f',
+ long = "fee_rate",
+ required = true
+ )]
+ fee_rate: u64,
+}
+#[cfg(any(
+ feature = "electrum",
+ feature = "esplora",
+ feature = "cbf",
+ feature = "rpc"
+))]
+impl AsyncCommand for SendPayjoinCommand {
+ type Output = StatusResult;
+
+ async fn execute(&self, ctx: &mut AppContext<'_>) -> Result<Self::Output, Error> {
+ let wallet = ctx
+ .wallet
+ .as_deref_mut()
+ .ok_or(Error::Generic("Wallet required".into()))?;
+ let client = ctx.client.ok_or(Error::Generic("client required".into()))?;
+
+ let relay_manager = Arc::new(Mutex::new(RelayManager::new()));
+ let mut payjoin_manager = PayjoinManager::new(wallet, relay_manager);
+ let result = payjoin_manager
+ .send_payjoin(
+ self.uri.clone(),
+ self.fee_rate,
+ self.ohttp_relay.clone(),
+ client,
+ )
+ .await?;
+
+ Ok(StatusResult { message: result })
}
}
-#[cfg(any(
- feature = "electrum",
- feature = "esplora",
- feature = "cbf",
- feature = "rpc"
-))]
-use crate::{backend::new_blockchain_client, handlers::online::handle_online_wallet_subcommand};
+use bdk_wallet::{Wallet, bitcoin::Network};
+
+// #[cfg(feature = "repl")]
+use crate::handlers::{AppCommand, AppContext};
+use crate::utils::output::FormatOutput;
#[cfg(feature = "sqlite")]
use crate::commands::ReplSubCommand;
+use clap::Parser;
#[cfg(feature = "repl")]
+use std::io::Write;
use {
+ crate::commands::{CliOpts, WalletSubCommand},
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,
+ datadir: std::path::PathBuf,
cli_opts: &CliOpts,
) -> Result<bool, String> {
- 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)
+
+ let mut repl_args = vec!["repl".to_string()];
+ repl_args.extend(args);
+
+ let repl_subcommand = match ReplSubCommand::try_parse_from(&repl_args) {
+ Ok(cmd) => cmd,
+ Err(e) => {
+ writeln!(std::io::stdout(), "{}", e).map_err(|e| e.to_string())?;
+ return Ok(false);
}
- 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)
+ };
+
+ let mut ctx = AppContext::new(network, datadir.clone()).with_wallet(&mut *wallet);
+
+ let response = match repl_subcommand {
+ ReplSubCommand::Wallet { subcommand } => match subcommand {
+ WalletSubCommand::OfflineWalletSubCommand(cmd) => {
+ cmd.execute(&mut ctx).map_err(|e| e.to_string())?;
+ Some(())
+ }
+ #[cfg(any(
+ feature = "electrum",
+ feature = "esplora",
+ feature = "cbf",
+ feature = "rpc"
+ ))]
+ WalletSubCommand::OnlineWalletSubCommand(cmd) => {
+ let value = cmd.execute(&mut ctx).await.map_err(|e| e.to_string())?;
+ Some(())
+ // Some(value)
+ }
+ WalletSubCommand::Config(config_cmd) => {
+ let mut ctx = AppContext::new(network, datadir);
+ let res = config_cmd
+ .execute(&mut ctx)
+ .map_err(|e| e.to_string())?
+ .print();
+ Some(())
+ }
+ },
+
+ // Assuming your REPL Descriptor command is an inline struct based on commands.rs
+ ReplSubCommand::Descriptor(cmd) => {
+ let value = cmd.execute(&mut ctx).map_err(|e| e.to_string())?.print();
+ Some(())
}
+
ReplSubCommand::Exit => None,
+ _ => todo!(),
};
+
if let Some(value) = response {
- writeln!(std::io::stdout(), "{value}").map_err(|e| e.to_string())?;
+ // writeln!(std::io::stdout(), "{value}").map_err(|e| e.to_string())?;
std::io::stdout().flush().map_err(|e| e.to_string())?;
Ok(false)
} else {
}
CliSubCommand::Repl { wallet: _ } => todo!(),
CliSubCommand::Completions { shell } => {
- shell;
+ shell;
}
#[cfg(feature = "compiler")]
CliSubCommand::Compile(cmd) => {
Ok(to_string_pretty(&json!({}))?)
}
- // #[cfg(any(
- // feature = "electrum",
- // feature = "esplora",
- // feature = "rpc",
- // feature = "cbf"
- // ))]
+ #[cfg(any(
+ feature = "electrum",
+ feature = "esplora",
+ feature = "rpc",
+ feature = "cbf"
+ ))]
pub async fn send_payjoin(
&mut self,
uri: String,
Ok((addr.script_pubkey(), val))
}
-// #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
+#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
/// Parse the proxy (Socket:Port) argument from the cli input.
pub(crate) fn parse_proxy_auth(s: &str) -> Result<(String, String), Error> {
let parts: Vec<_> = s.split(':').collect();
};
use crate::error::BDKCliError as Error;
-use crate::handlers::types::{DescriptorResult, KeychainPair};
+use crate::utils::types::DescriptorResult;
+use crate::utils::types::KeychainPair;
pub fn generate_descriptors(
desc_type: &str,
pub mod descriptors;
pub mod output;
pub use common::*;
+pub mod types;
use crate::error::BDKCliError as Error;
-use cli_table::{CellStruct, Table};
use serde::Serialize;
/// A trait for types that can be presented to the user.
pub trait FormatOutput: Serialize {
- /// Return a pretty table representation.
- fn to_table(&self) -> Result<String, Error>;
-
- /// Formats the output based on the user's `--pretty` flag.
- fn format(&self, pretty: bool) -> Result<String, Error> {
- if pretty {
- self.to_table()
- } else {
- serde_json::to_string_pretty(self)
- .map_err(|e| Error::Generic(format!("JSON serialization failed: {e}")))
- }
+ fn format(&self) -> Result<String, Error> {
+ serde_json::to_string_pretty(self)
+ .map_err(|e| Error::Generic(format!("JSON serialization failed: {e}")))
}
-}
-/// Helper for building simple tables
-pub fn simple_table<R, C>(rows: R, title: Option<Vec<CellStruct>>) -> Result<String, Error>
-where
- R: IntoIterator<Item = C>,
- C: IntoIterator<Item = CellStruct>,
-{
- let mut table = rows.table();
- if let Some(title) = title {
- table = table.title(title);
+ fn print(&self) -> Result<(), Error> {
+ let output = self.format()?;
+ println!("{}", output);
+ Ok(())
}
- table
- .display()
- .map_err(|e| Error::Generic(e.to_string()))
- .map(|t| t.to_string())
}
pub is_spent: bool,
pub derivation_index: u32,
pub chain_position: serde_json::Value,
-
}
impl UnspentDetails {
impl FormatOutput for TransactionResult {}
-
-
/// Return type definition
#[derive(Serialize)]
#[serde(transparent)]
impl FormatOutput for WalletsListResult {}
-
/// return type
#[derive(Serialize)]
pub struct DescriptorResult {
impl FormatOutput for KeyResult {}
-
/// Balance representation
#[derive(Serialize)]
pub struct BalanceResult {