Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
5c286d6
ref(commands): mv commands & clients into subdirs
tvpeter Apr 21, 2026
739dec3
ref(config): move config into config subdir
tvpeter Apr 21, 2026
2482044
ref(utils): refactor utils into utils subdir
tvpeter Apr 21, 2026
eb3a6aa
ref(persister): mv persister into wallet subdir
tvpeter Apr 21, 2026
03307c9
ref(handlers): split handlers into config, key
tvpeter Apr 21, 2026
bf0551b
ref(handlers):mv handler fns into offline & others
tvpeter Apr 21, 2026
0bf3e66
ref(handlers): split handler fns into repl
tvpeter Apr 21, 2026
3e2eec8
ref(main): update main entry point
tvpeter Apr 21, 2026
a973273
refactor(handlers): Add types for outputting data
tvpeter Apr 24, 2026
055e1fd
ref(handlers): add types for desc, key & wallets
tvpeter Apr 29, 2026
990f30a
ref(types): add simple table helper
tvpeter Apr 29, 2026
90017cd
ref(handlers): rebase bip322 feature
tvpeter Apr 29, 2026
ada3485
ref(utils): use types in desc output in utils
tvpeter Apr 29, 2026
eaa4e11
revert mod names for command and clients
tvpeter May 14, 2026
7a0e68b
update namespace and update types
tvpeter May 14, 2026
cf9d13b
ref(persister): collapse wallet subdir to persister
tvpeter May 21, 2026
528956d
ref(pretty): remove `--pretty` flag
tvpeter May 21, 2026
f77ddcc
ref(main): add run to handle routing
tvpeter May 21, 2026
dcaeb55
ref(handlers): refactor config, descr and key mods
tvpeter May 21, 2026
1b05c03
ref(handlers): fix offline, online and desc mod
tvpeter May 21, 2026
5ced5aa
refactor(output): apply generics to output mod
tvpeter May 26, 2026
f3deed8
Refactor(handlers): Define app context states
tvpeter May 29, 2026
63162be
refactor(handlers): Apply app context
tvpeter May 29, 2026
aa8876d
refactor(main): add runtime wallet module
tvpeter May 31, 2026
75b5141
refactor(sp-payment): Rebase silent payment feat
tvpeter Jun 6, 2026
188e68d
refactor(runtime): Add address and createtx to db
tvpeter Jun 22, 2026
cb9634f
ref(create_tx): enforce single recp for sendall
tvpeter Jul 4, 2026
6a6390f
feat(WalletEvents): Add wallet events to clients
tvpeter Jul 4, 2026
249948e
ref(compile):compile tr desc with rand unspend key
tvpeter Jul 4, 2026
c2ca2dd
ref(payjoin): add payjoin persistence
tvpeter Jul 5, 2026
fcf881c
Merge 'master' into refactor/restructure-project
tvpeter Jul 5, 2026
b3fe860
feat(sp): fix command_requires_db fn match arm
tvpeter Jul 5, 2026
77a891b
Merge branch 'master' into ref/restructure-project
tvpeter Jul 8, 2026
5951c17
feat(dns-payment): port dns payment instruction
tvpeter Jul 8, 2026
5da7660
feat(handlers): mv feats into subdirs in handlers
tvpeter Jul 8, 2026
8dc0503
drop verbose flag
tvpeter Jul 8, 2026
63f769a
revert parallel request wallet parameter
tvpeter Jul 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
328 changes: 141 additions & 187 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ tracing = "0.1.44"
tracing-subscriber = "0.3.20"
toml = "1.1.0"
serde= {version = "1.0", features = ["derive"]}
shlex = "1.3.0"
tap = "1.0.1"

# Optional dependencies
Expand All @@ -36,6 +35,7 @@ bdk_esplora = { version = "0.22.2", features = ["async-https", "tokio"], optiona
bdk_kyoto = { version = "0.15.4", optional = true }
bdk_redb = { version = "0.1.1", optional = true }
bdk_sp = { version = "0.1.0", optional = true, git = "https://github.com/bitcoindevkit/bdk-sp", tag = "v0.1.0" }
shlex = { version = "1.3.0", optional = true }
payjoin = { version = "0.25.0", features = ["v1", "v2", "io", "_test-utils"], optional = true}
reqwest = { version = "0.13.2", default-features = false, features = ["rustls"], optional = true }
url = { version = "2.5.8", optional = true }
Expand All @@ -46,7 +46,7 @@ bitcoin-payment-instructions = { version = "0.7.0", optional = true}
default = ["repl", "sqlite"]

# To use the app in a REPL mode
repl = []
repl = ["shlex"]

# Available database options
sqlite = ["bdk_wallet/rusqlite"]
Expand Down
9 changes: 0 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,15 +282,6 @@ Note: You can modify the `Justfile` to reflect your nodes' configuration values.
cargo run --features rpc -- wallet -w regtest1 balance
```

## Formatting Responses using `--pretty` flag

You can optionally return outputs of commands in human-readable, tabular format instead of `JSON`. To enable this option, simply add the `--pretty` flag as a top level flag. For instance, you wallet's balance in a pretty format, you can run:

```shell
cargo run -- --pretty -n signet wallet -w {wallet_name} balance
```
This is available for wallet, key, repl and compile features. When ommitted, outputs default to `JSON`.

## Shell Completions

`bdk-cli` supports generating shell completions for Bash, Zsh, Fish, Elvish, and PowerShell. For setup instructions, run:
Expand Down
330 changes: 330 additions & 0 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
#[cfg(feature = "esplora")]
use bdk_esplora::EsploraAsyncExt;
#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "rpc",
feature = "cbf"
))]
use {
crate::commands::WalletOpts,
crate::error::BDKCliError as Error,
bdk_wallet::{
Wallet,
bitcoin::{Transaction, Txid},
},
clap::ValueEnum,
std::path::PathBuf,
};
#[cfg(feature = "rpc")]
use {
bdk_bitcoind_rpc::{Emitter, bitcoincore_rpc::RpcApi},
bdk_wallet::chain::CanonicalizationParams,
};

#[cfg(feature = "cbf")]
use {
crate::utils::trace_logger,
bdk_kyoto::{BuilderExt, LightClient},
};

#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "rpc",
feature = "cbf"
))]
#[derive(Clone, ValueEnum, Debug, Eq, PartialEq)]
pub enum ClientType {
#[cfg(feature = "electrum")]
Electrum,
#[cfg(feature = "esplora")]
Esplora,
#[cfg(feature = "rpc")]
Rpc,
#[cfg(feature = "cbf")]
Cbf,
}

#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "rpc",
feature = "cbf"
))]
pub(crate) enum BlockchainClient {
#[cfg(feature = "electrum")]
Electrum {
client: Box<bdk_electrum::BdkElectrumClient<bdk_electrum::electrum_client::Client>>,
batch_size: usize,
},
#[cfg(feature = "esplora")]
Esplora {
client: Box<bdk_esplora::esplora_client::AsyncClient>,
parallel_requests: usize,
},
#[cfg(feature = "rpc")]
RpcClient {
client: Box<bdk_bitcoind_rpc::bitcoincore_rpc::Client>,
},

#[cfg(feature = "cbf")]
KyotoClient { client: Box<KyotoClientHandle> },
}

#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "rpc",
feature = "cbf"
))]
impl BlockchainClient {
pub async fn broadcast(&self, tx: Transaction) -> Result<Txid, Error> {
match self {
#[cfg(feature = "electrum")]
Self::Electrum { client, .. } => client
.transaction_broadcast(&tx)
.map_err(|e| Error::Generic(e.to_string())),

#[cfg(feature = "esplora")]
Self::Esplora { client, .. } => client
.broadcast(&tx)
.await
.map(|()| tx.compute_txid())
.map_err(|e| Error::Generic(e.to_string())),

#[cfg(feature = "rpc")]
Self::RpcClient { client } => client
.send_raw_transaction(&tx)
.map_err(|e| Error::Generic(e.to_string())),

#[cfg(feature = "cbf")]
Self::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)
}
}
}

pub async fn sync_wallet(&self, 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 self {
#[cfg(feature = "electrum")]
Self::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")]
Self::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")]
Self::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,
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);
Ok(())
}
#[cfg(feature = "cbf")]
Self::KyotoClient { client } => sync_kyoto_client(wallet, client)
.await
.map_err(|e| Error::Generic(e.to_string())),
}
}
}

/// Handle for the Kyoto client after the node has been started.
/// Contains only the components needed for sync and broadcast operations.
#[cfg(feature = "cbf")]
pub struct KyotoClientHandle {
pub requester: bdk_kyoto::Requester,
pub update_subscriber: tokio::sync::Mutex<bdk_kyoto::UpdateSubscriber>,
}

#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "rpc",
feature = "cbf",
))]
/// Create a new blockchain from the wallet configuration options.
pub(crate) fn new_blockchain_client(
wallet_opts: &WalletOpts,
_wallet: &Wallet,
_datadir: PathBuf,
) -> Result<BlockchainClient, Error> {
#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
let url = &wallet_opts.url;
let client = match wallet_opts.client_type {
#[cfg(feature = "electrum")]
ClientType::Electrum => {
let client = bdk_electrum::electrum_client::Client::new(url)
.map(bdk_electrum::BdkElectrumClient::new)?;
BlockchainClient::Electrum {
client: Box::new(client),
batch_size: wallet_opts.batch_size,
}
}
#[cfg(feature = "esplora")]
ClientType::Esplora => {
let client = bdk_esplora::esplora_client::Builder::new(url).build_async()?;
BlockchainClient::Esplora {
client: Box::new(client),
parallel_requests: wallet_opts.parallel_requests,
}
}

#[cfg(feature = "rpc")]
ClientType::Rpc => {
let auth = match &wallet_opts.cookie {
Some(cookie) => bdk_bitcoind_rpc::bitcoincore_rpc::Auth::CookieFile(cookie.into()),
None => bdk_bitcoind_rpc::bitcoincore_rpc::Auth::UserPass(
wallet_opts.basic_auth.0.clone(),
wallet_opts.basic_auth.1.clone(),
),
};
let client = bdk_bitcoind_rpc::bitcoincore_rpc::Client::new(url, auth)
.map_err(|e| Error::Generic(e.to_string()))?;
BlockchainClient::RpcClient {
client: Box::new(client),
}
}

#[cfg(feature = "cbf")]
ClientType::Cbf => {
let scan_type = bdk_kyoto::ScanType::Sync;
let builder = bdk_kyoto::builder::Builder::new(_wallet.network());

let light_client = builder
.required_peers(wallet_opts.compactfilter_opts.conn_count)
.data_dir(&_datadir)
.build_with_wallet(_wallet, scan_type)?;

let LightClient {
requester,
info_subscriber,
warning_subscriber,
update_subscriber,
node,
} = light_client;

let subscriber = tracing_subscriber::FmtSubscriber::new();
let _ = tracing::subscriber::set_global_default(subscriber);

tokio::task::spawn(async move { node.run().await });
tokio::task::spawn(
async move { trace_logger(info_subscriber, warning_subscriber).await },
);

BlockchainClient::KyotoClient {
client: Box::new(KyotoClientHandle {
requester,
update_subscriber: tokio::sync::Mutex::new(update_subscriber),
}),
}
}
};
Ok(client)
}

// Handle Kyoto Client sync
#[cfg(feature = "cbf")]
pub async fn sync_kyoto_client(
wallet: &mut Wallet,
handle: &KyotoClientHandle,
) -> Result<(), Error> {
if !handle.requester.is_running() {
tracing::error!("Kyoto node is not running");
return Err(Error::Generic("Kyoto node failed to start".to_string()));
}
tracing::info!("Kyoto node is running");

let update = handle.update_subscriber.lock().await.update().await?;
tracing::info!("Received update: applying to wallet");
let events = wallet
.apply_update_events(update)
.map_err(|e| Error::Generic(format!("Failed to apply update: {e}")))?;
crate::utils::print_wallet_events(&events);

tracing::info!(
"Chain tip: {}, Transactions: {}, Balance: {}",
wallet.local_chain().tip().height(),
wallet.transactions().count(),
wallet.balance().total().to_sat()
);

tracing::info!(
"Sync completed: tx_count={}, balance={}",
wallet.transactions().count(),
wallet.balance().total().to_sat()
);

Ok(())
}
Loading
Loading