diff --git a/src/commands.rs b/src/commands.rs index 44b13f27..e2177ce0 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -12,7 +12,6 @@ //! All optional args are defined in the structs below. //! All subcommands are defined in the below enums. -#![allow(clippy::large_enum_variant)] use bdk_wallet::bitcoin::{ Address, Network, OutPoint, ScriptBuf, bip32::{DerivationPath, Xpriv}, @@ -270,6 +269,7 @@ pub struct WalletOpts { #[cfg(feature = "electrum")] #[arg(env = "ELECTRUM_BATCH_SIZE", short = 'b', long, default_value = "10")] pub batch_size: usize, + /// Esplora parallel requests. #[cfg(feature = "esplora")] #[arg( @@ -480,8 +480,11 @@ pub enum OnlineWalletSubCommand { stop_gap: usize, }, /// Syncs with the chosen blockchain server. - Sync, - /// Broadcasts a transaction to the network. Takes either a raw transaction or a PSBT to extract. + Sync { + #[command(flatten)] + wallet_opts: WalletOpts, + }, + Broadcast { /// Sets the PSBT to sign. #[arg( diff --git a/src/handlers.rs b/src/handlers.rs index a98b172d..3547df42 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -9,6 +9,7 @@ //! Command Handlers //! //! This module describes all the command handling logic used by bdk-cli. + use crate::commands::OfflineWalletSubCommand::*; use crate::commands::*; use crate::config::{WalletConfig, WalletConfigInner}; @@ -694,8 +695,85 @@ pub(crate) async fn handle_online_wallet_subcommand( } Ok(serde_json::to_string_pretty(&json!({}))?) } - Sync => { - sync_wallet(client, wallet).await?; + + #[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)?; + } + #[cfg(feature = "esplora")] + Esplora { + client, + parallel_requests, + } => { + let update = client + .sync(request, parallel_requests) + .await + .map_err(|e| *e)?; + wallet.apply_update(update)?; + } + #[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, + 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(), + 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!({}))?) } Broadcast { psbt, tx } => { @@ -715,7 +793,68 @@ pub(crate) async fn handle_online_wallet_subcommand( (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?; + + let txid = 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 LightClient { + requester, + mut info_subscriber, + mut warning_subscriber, + update_subscriber: _, + node, + } = *client; + + let subscriber = tracing_subscriber::FmtSubscriber::new(); + tracing::subscriber::set_global_default(subscriber) + .map_err(|e| Error::Generic(format!("SetGlobalDefault error: {e}")))?; + + tokio::task::spawn(async move { node.run().await }); + tokio::task::spawn(async move { + select! { + info = info_subscriber.recv() => { + if let Some(info) = info { + tracing::info!("{info}"); + } + }, + warn = warning_subscriber.recv() => { + if let Some(warn) = warn { + tracing::warn!("{warn}"); + } + } + } + }); + let txid = tx.compute_txid(); + let wtxid = 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}"); + txid + } + }; + Ok(serde_json::to_string_pretty(&json!({ "txid": txid }))?) } ReceivePayjoin { diff --git a/src/utils.rs b/src/utils.rs index 76e56a0f..330233ad 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -9,32 +9,21 @@ //! Utility Tools //! //! This module includes all the utility tools used by the App. -use crate::config::WalletConfig; -use crate::error::BDKCliError as Error; -use std::{ - fmt::Display, - path::{Path, PathBuf}, - str::FromStr, - sync::Arc, -}; use crate::commands::WalletOpts; +use crate::error::BDKCliError as Error; +use bdk_electrum::electrum_client::ConfigBuilder; #[cfg(feature = "cbf")] use bdk_kyoto::{ BuilderExt, Info, LightClient, Receiver, ScanType::Sync, UnboundedReceiver, Warning, builder::Builder, }; -use bdk_wallet::{ - KeychainKind, - bitcoin::bip32::{DerivationPath, Xpub}, - keys::DescriptorPublicKey, - miniscript::{ - Descriptor, Miniscript, Terminal, - descriptor::{DescriptorXKey, Wildcard}, - }, - template::DescriptorTemplate, -}; -use cli_table::{Cell, CellStruct, Style, Table}; + + +use bdk_wallet::bitcoin::{Address, Network, OutPoint, ScriptBuf}; +use std::fmt::Display; +use std::path::{Path, PathBuf}; +use std::str::FromStr; #[cfg(any( feature = "electrum", @@ -186,7 +175,8 @@ pub(crate) fn new_blockchain_client( let client = match wallet_opts.client_type { #[cfg(feature = "electrum")] ClientType::Electrum => { - let client = bdk_electrum::electrum_client::Client::new(url) + let config = ConfigBuilder::new().build(); + let client = bdk_electrum::electrum_client::Client::from_config(url, config) .map(bdk_electrum::BdkElectrumClient::new)?; BlockchainClient::Electrum { client: Box::new(client),