#[derive(Parser)]
#[clap(author, version, about, long_about = None)]
#[clap(propagate_version = true)]
-pub struct Args<S: clap::Subcommand> {
+pub struct Args<CS: clap::Subcommand, S: clap::Args> {
#[clap(env = "DESCRIPTOR")]
pub descriptor: String,
#[clap(env = "CHANGE_DESCRIPTOR")]
pub cp_limit: usize,
#[clap(subcommand)]
- pub command: Commands<S>,
+ pub command: Commands<CS, S>,
}
#[allow(clippy::almost_swapped)]
#[derive(Subcommand, Debug, Clone)]
-pub enum Commands<S: clap::Subcommand> {
+pub enum Commands<CS: clap::Subcommand, S: clap::Args> {
#[clap(flatten)]
- ChainSpecific(S),
+ ChainSpecific(CS),
/// Address generation and inspection.
Address {
#[clap(subcommand)]
address: Address<address::NetworkUnchecked>,
#[clap(short, default_value = "bnb")]
coin_select: CoinSelectionAlgo,
+ #[clap(flatten)]
+ chain_specfic: S,
},
}
}
}
-pub fn run_address_cmd<A, C>(
- graph: &mut KeychainTxGraph<A>,
- db: &Mutex<Database<C>>,
- network: Network,
- cmd: AddressCmd,
-) -> anyhow::Result<()>
-where
- C: Default + Append + DeserializeOwned + Serialize + From<KeychainChangeSet<A>>,
-{
- let index = &mut graph.index;
-
- match cmd {
- AddressCmd::Next | AddressCmd::New => {
- let spk_chooser = match cmd {
- AddressCmd::Next => KeychainTxOutIndex::next_unused_spk,
- AddressCmd::New => KeychainTxOutIndex::reveal_next_spk,
- _ => unreachable!("only these two variants exist in match arm"),
- };
-
- let ((spk_i, spk), index_changeset) = spk_chooser(index, &Keychain::External);
- let db = &mut *db.lock().unwrap();
- db.stage(C::from((
- local_chain::ChangeSet::default(),
- indexed_tx_graph::ChangeSet::from(index_changeset),
- )));
- db.commit()?;
- let addr = Address::from_script(spk, network).context("failed to derive address")?;
- println!("[address @ {}] {}", spk_i, addr);
- Ok(())
- }
- AddressCmd::Index => {
- for (keychain, derivation_index) in index.last_revealed_indices() {
- println!("{:?}: {}", keychain, derivation_index);
- }
- Ok(())
- }
- AddressCmd::List { change } => {
- let target_keychain = match change {
- true => Keychain::Internal,
- false => Keychain::External,
- };
- for (spk_i, spk) in index.revealed_spks_of_keychain(&target_keychain) {
- let address = Address::from_script(spk, network)
- .expect("should always be able to derive address");
- println!(
- "{:?} {} used:{}",
- spk_i,
- address,
- index.is_used(&(target_keychain, spk_i))
- );
- }
- Ok(())
- }
- }
-}
-
-pub fn run_balance_cmd<A: Anchor, O: ChainOracle>(
- graph: &KeychainTxGraph<A>,
- chain: &O,
-) -> Result<(), O::Error> {
- fn print_balances<'a>(title_str: &'a str, items: impl IntoIterator<Item = (&'a str, u64)>) {
- println!("{}:", title_str);
- for (name, amount) in items.into_iter() {
- println!(" {:<10} {:>12} sats", name, amount)
- }
- }
-
- let balance = graph.graph().try_balance(
- chain,
- chain.get_chain_tip()?.unwrap_or_default(),
- graph.index.outpoints().iter().cloned(),
- |(k, _), _| k == &Keychain::Internal,
- )?;
-
- let confirmed_total = balance.confirmed + balance.immature;
- let unconfirmed_total = balance.untrusted_pending + balance.trusted_pending;
-
- print_balances(
- "confirmed",
- [
- ("total", confirmed_total),
- ("spendable", balance.confirmed),
- ("immature", balance.immature),
- ],
- );
- print_balances(
- "unconfirmed",
- [
- ("total", unconfirmed_total),
- ("trusted", balance.trusted_pending),
- ("untrusted", balance.untrusted_pending),
- ],
- );
-
- Ok(())
-}
-
-pub fn run_txo_cmd<A: Anchor, O: ChainOracle>(
- graph: &KeychainTxGraph<A>,
- chain: &O,
- network: Network,
- cmd: TxOutCmd,
-) -> anyhow::Result<()>
-where
- O::Error: std::error::Error + Send + Sync + 'static,
-{
- let chain_tip = chain.get_chain_tip()?.unwrap_or_default();
- let outpoints = graph.index.outpoints().iter().cloned();
-
- match cmd {
- TxOutCmd::List {
- spent,
- unspent,
- confirmed,
- unconfirmed,
- } => {
- let txouts = graph
- .graph()
- .try_filter_chain_txouts(chain, chain_tip, outpoints)
- .filter(|r| match r {
- Ok((_, full_txo)) => match (spent, unspent) {
- (true, false) => full_txo.spent_by.is_some(),
- (false, true) => full_txo.spent_by.is_none(),
- _ => true,
- },
- // always keep errored items
- Err(_) => true,
- })
- .filter(|r| match r {
- Ok((_, full_txo)) => match (confirmed, unconfirmed) {
- (true, false) => full_txo.chain_position.is_confirmed(),
- (false, true) => !full_txo.chain_position.is_confirmed(),
- _ => true,
- },
- // always keep errored items
- Err(_) => true,
- })
- .collect::<Result<Vec<_>, _>>()?;
-
- for (spk_i, full_txo) in txouts {
- let addr = Address::from_script(&full_txo.txout.script_pubkey, network)?;
- println!(
- "{:?} {} {} {} spent:{:?}",
- spk_i, full_txo.txout.value, full_txo.outpoint, addr, full_txo.spent_by
- )
- }
- Ok(())
- }
- }
-}
-
-#[allow(clippy::too_many_arguments)]
-pub fn run_send_cmd<A: Anchor, O: ChainOracle, C>(
- graph: &Mutex<KeychainTxGraph<A>>,
- db: &Mutex<Database<'_, C>>,
- chain: &O,
- keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
- cs_algorithm: CoinSelectionAlgo,
- address: Address,
- value: u64,
- broadcast: impl FnOnce(&Transaction) -> anyhow::Result<()>,
-) -> anyhow::Result<()>
-where
- O::Error: std::error::Error + Send + Sync + 'static,
- C: Default + Append + DeserializeOwned + Serialize + From<KeychainChangeSet<A>>,
-{
- let (transaction, change_index) = {
- let graph = &mut *graph.lock().unwrap();
- // take mutable ref to construct tx -- it is only open for a short time while building it.
- let (tx, change_info) = create_tx(graph, chain, keymap, cs_algorithm, address, value)?;
-
- if let Some((index_changeset, (change_keychain, index))) = change_info {
- // We must first persist to disk the fact that we've got a new address from the
- // change keychain so future scans will find the tx we're about to broadcast.
- // If we're unable to persist this, then we don't want to broadcast.
- {
- let db = &mut *db.lock().unwrap();
- db.stage(C::from((
- local_chain::ChangeSet::default(),
- indexed_tx_graph::ChangeSet::from(index_changeset),
- )));
- db.commit()?;
- }
-
- // We don't want other callers/threads to use this address while we're using it
- // but we also don't want to scan the tx we just created because it's not
- // technically in the blockchain yet.
- graph.index.mark_used(&change_keychain, index);
- (tx, Some((change_keychain, index)))
- } else {
- (tx, None)
- }
- };
-
- match (broadcast)(&transaction) {
- Ok(_) => {
- println!("Broadcasted Tx : {}", transaction.txid());
-
- let keychain_changeset = graph.lock().unwrap().insert_tx(&transaction, None, None);
-
- // We know the tx is at least unconfirmed now. Note if persisting here fails,
- // it's not a big deal since we can always find it again form
- // blockchain.
- db.lock().unwrap().stage(C::from((
- local_chain::ChangeSet::default(),
- keychain_changeset,
- )));
- Ok(())
- }
- Err(e) => {
- if let Some((keychain, index)) = change_index {
- // We failed to broadcast, so allow our change address to be used in the future
- graph.lock().unwrap().index.unmark_used(&keychain, index);
- }
- Err(e)
- }
- }
-}
-
#[allow(clippy::type_complexity)]
pub fn create_tx<A: Anchor, O: ChainOracle>(
graph: &mut KeychainTxGraph<A>,
.collect()
}
-pub fn handle_commands<S: clap::Subcommand, A: Anchor, O: ChainOracle, C>(
+pub fn handle_commands<CS: clap::Subcommand, S: clap::Args, A: Anchor, O: ChainOracle, C>(
graph: &Mutex<KeychainTxGraph<A>>,
db: &Mutex<Database<C>>,
chain: &Mutex<O>,
keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
network: Network,
- broadcast: impl FnOnce(&Transaction) -> anyhow::Result<()>,
- cmd: Commands<S>,
+ broadcast: impl FnOnce(S, &Transaction) -> anyhow::Result<()>,
+ cmd: Commands<CS, S>,
) -> anyhow::Result<()>
where
O::Error: std::error::Error + Send + Sync + 'static,
Commands::ChainSpecific(_) => unreachable!("example code should handle this!"),
Commands::Address { addr_cmd } => {
let graph = &mut *graph.lock().unwrap();
- run_address_cmd(graph, db, network, addr_cmd)
+ let index = &mut graph.index;
+
+ match addr_cmd {
+ AddressCmd::Next | AddressCmd::New => {
+ let spk_chooser = match addr_cmd {
+ AddressCmd::Next => KeychainTxOutIndex::next_unused_spk,
+ AddressCmd::New => KeychainTxOutIndex::reveal_next_spk,
+ _ => unreachable!("only these two variants exist in match arm"),
+ };
+
+ let ((spk_i, spk), index_changeset) = spk_chooser(index, &Keychain::External);
+ let db = &mut *db.lock().unwrap();
+ db.stage(C::from((
+ local_chain::ChangeSet::default(),
+ indexed_tx_graph::ChangeSet::from(index_changeset),
+ )));
+ db.commit()?;
+ let addr =
+ Address::from_script(spk, network).context("failed to derive address")?;
+ println!("[address @ {}] {}", spk_i, addr);
+ Ok(())
+ }
+ AddressCmd::Index => {
+ for (keychain, derivation_index) in index.last_revealed_indices() {
+ println!("{:?}: {}", keychain, derivation_index);
+ }
+ Ok(())
+ }
+ AddressCmd::List { change } => {
+ let target_keychain = match change {
+ true => Keychain::Internal,
+ false => Keychain::External,
+ };
+ for (spk_i, spk) in index.revealed_spks_of_keychain(&target_keychain) {
+ let address = Address::from_script(spk, network)
+ .expect("should always be able to derive address");
+ println!(
+ "{:?} {} used:{}",
+ spk_i,
+ address,
+ index.is_used(&(target_keychain, spk_i))
+ );
+ }
+ Ok(())
+ }
+ }
}
Commands::Balance => {
let graph = &*graph.lock().unwrap();
let chain = &*chain.lock().unwrap();
- run_balance_cmd(graph, chain).map_err(anyhow::Error::from)
+ fn print_balances<'a>(
+ title_str: &'a str,
+ items: impl IntoIterator<Item = (&'a str, u64)>,
+ ) {
+ println!("{}:", title_str);
+ for (name, amount) in items.into_iter() {
+ println!(" {:<10} {:>12} sats", name, amount)
+ }
+ }
+
+ let balance = graph.graph().try_balance(
+ chain,
+ chain.get_chain_tip()?.unwrap_or_default(),
+ graph.index.outpoints().iter().cloned(),
+ |(k, _), _| k == &Keychain::Internal,
+ )?;
+
+ let confirmed_total = balance.confirmed + balance.immature;
+ let unconfirmed_total = balance.untrusted_pending + balance.trusted_pending;
+
+ print_balances(
+ "confirmed",
+ [
+ ("total", confirmed_total),
+ ("spendable", balance.confirmed),
+ ("immature", balance.immature),
+ ],
+ );
+ print_balances(
+ "unconfirmed",
+ [
+ ("total", unconfirmed_total),
+ ("trusted", balance.trusted_pending),
+ ("untrusted", balance.untrusted_pending),
+ ],
+ );
+
+ Ok(())
}
Commands::TxOut { txout_cmd } => {
let graph = &*graph.lock().unwrap();
let chain = &*chain.lock().unwrap();
- run_txo_cmd(graph, chain, network, txout_cmd)
+ let chain_tip = chain.get_chain_tip()?.unwrap_or_default();
+ let outpoints = graph.index.outpoints().iter().cloned();
+
+ match txout_cmd {
+ TxOutCmd::List {
+ spent,
+ unspent,
+ confirmed,
+ unconfirmed,
+ } => {
+ let txouts = graph
+ .graph()
+ .try_filter_chain_txouts(chain, chain_tip, outpoints)
+ .filter(|r| match r {
+ Ok((_, full_txo)) => match (spent, unspent) {
+ (true, false) => full_txo.spent_by.is_some(),
+ (false, true) => full_txo.spent_by.is_none(),
+ _ => true,
+ },
+ // always keep errored items
+ Err(_) => true,
+ })
+ .filter(|r| match r {
+ Ok((_, full_txo)) => match (confirmed, unconfirmed) {
+ (true, false) => full_txo.chain_position.is_confirmed(),
+ (false, true) => !full_txo.chain_position.is_confirmed(),
+ _ => true,
+ },
+ // always keep errored items
+ Err(_) => true,
+ })
+ .collect::<Result<Vec<_>, _>>()?;
+
+ for (spk_i, full_txo) in txouts {
+ let addr = Address::from_script(&full_txo.txout.script_pubkey, network)?;
+ println!(
+ "{:?} {} {} {} spent:{:?}",
+ spk_i, full_txo.txout.value, full_txo.outpoint, addr, full_txo.spent_by
+ )
+ }
+ Ok(())
+ }
+ }
}
Commands::Send {
value,
address,
coin_select,
+ chain_specfic,
} => {
let chain = &*chain.lock().unwrap();
let address = address.require_network(network)?;
- run_send_cmd(
- graph,
- db,
- chain,
- keymap,
- coin_select,
- address,
- value,
- broadcast,
- )
+ let (transaction, change_index) = {
+ let graph = &mut *graph.lock().unwrap();
+ // take mutable ref to construct tx -- it is only open for a short time while building it.
+ let (tx, change_info) =
+ create_tx(graph, chain, keymap, coin_select, address, value)?;
+
+ if let Some((index_changeset, (change_keychain, index))) = change_info {
+ // We must first persist to disk the fact that we've got a new address from the
+ // change keychain so future scans will find the tx we're about to broadcast.
+ // If we're unable to persist this, then we don't want to broadcast.
+ {
+ let db = &mut *db.lock().unwrap();
+ db.stage(C::from((
+ local_chain::ChangeSet::default(),
+ indexed_tx_graph::ChangeSet::from(index_changeset),
+ )));
+ db.commit()?;
+ }
+
+ // We don't want other callers/threads to use this address while we're using it
+ // but we also don't want to scan the tx we just created because it's not
+ // technically in the blockchain yet.
+ graph.index.mark_used(&change_keychain, index);
+ (tx, Some((change_keychain, index)))
+ } else {
+ (tx, None)
+ }
+ };
+
+ match (broadcast)(chain_specfic, &transaction) {
+ Ok(_) => {
+ println!("Broadcasted Tx : {}", transaction.txid());
+
+ let keychain_changeset =
+ graph.lock().unwrap().insert_tx(&transaction, None, None);
+
+ // We know the tx is at least unconfirmed now. Note if persisting here fails,
+ // it's not a big deal since we can always find it again form
+ // blockchain.
+ db.lock().unwrap().stage(C::from((
+ local_chain::ChangeSet::default(),
+ keychain_changeset,
+ )));
+ Ok(())
+ }
+ Err(e) => {
+ if let Some((keychain, index)) = change_index {
+ // We failed to broadcast, so allow our change address to be used in the future
+ graph.lock().unwrap().index.unmark_used(&keychain, index);
+ }
+ Err(e)
+ }
+ }
}
}
}
#[allow(clippy::type_complexity)]
-pub fn init<'m, S: clap::Subcommand, C>(
+pub fn init<'m, CS: clap::Subcommand, S: clap::Args, C>(
db_magic: &'m [u8],
db_default_path: &str,
) -> anyhow::Result<(
- Args<S>,
+ Args<CS, S>,
KeyMap,
KeychainTxOutIndex<Keychain>,
Mutex<Database<'m, C>>,
if std::env::var("BDK_DB_PATH").is_err() {
std::env::set_var("BDK_DB_PATH", db_default_path);
}
- let args = Args::<S>::parse();
+ let args = Args::<CS, S>::parse();
let secp = Secp256k1::default();
let mut index = KeychainTxOutIndex::<Keychain>::default();
Append, ConfirmationHeightAnchor,
};
use bdk_electrum::{
- electrum_client::{self, ElectrumApi},
+ electrum_client::{self, Client, ElectrumApi},
ElectrumExt, ElectrumUpdate,
};
use example_cli::{
stop_gap: usize,
#[clap(flatten)]
scan_options: ScanOptions,
+ #[clap(flatten)]
+ electrum_args: ElectrumArgs,
},
/// Scans particular addresses using the electrum API.
Sync {
unconfirmed: bool,
#[clap(flatten)]
scan_options: ScanOptions,
+ #[clap(flatten)]
+ electrum_args: ElectrumArgs,
},
}
+impl ElectrumCommands {
+ fn electrum_args(&self) -> ElectrumArgs {
+ match self {
+ ElectrumCommands::Scan { electrum_args, .. } => electrum_args.clone(),
+ ElectrumCommands::Sync { electrum_args, .. } => electrum_args.clone(),
+ }
+ }
+}
+
+#[derive(clap::Args, Debug, Clone)]
+pub struct ElectrumArgs {
+ /// The electrum url to use to connect to. If not provided it will use a default electrum server
+ /// for your chosen network.
+ electrum_url: Option<String>,
+}
+
+impl ElectrumArgs {
+ pub fn client(&self, network: Network) -> anyhow::Result<Client> {
+ let electrum_url = self.electrum_url.as_deref().unwrap_or(match network {
+ Network::Bitcoin => "ssl://electrum.blockstream.info:50002",
+ Network::Testnet => "ssl://electrum.blockstream.info:60002",
+ Network::Regtest => "tcp://localhost:60401",
+ Network::Signet => "tcp://signet-electrumx.wakiyamap.dev:50001",
+ _ => panic!("Unknown network"),
+ });
+ let config = electrum_client::Config::builder()
+ .validate_domain(matches!(network, Network::Bitcoin))
+ .build();
+
+ Ok(electrum_client::Client::from_config(electrum_url, config)?)
+ }
+}
+
#[derive(Parser, Debug, Clone, PartialEq)]
pub struct ScanOptions {
/// Set batch size for each script_history call to electrum client.
fn main() -> anyhow::Result<()> {
let (args, keymap, index, db, (disk_local_chain, disk_tx_graph)) =
- example_cli::init::<ElectrumCommands, ChangeSet>(DB_MAGIC, DB_PATH)?;
+ example_cli::init::<ElectrumCommands, ElectrumArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
let graph = Mutex::new({
let mut graph = IndexedTxGraph::new(index);
let chain = Mutex::new(LocalChain::from_changeset(disk_local_chain));
- let electrum_url = match args.network {
- Network::Bitcoin => "ssl://electrum.blockstream.info:50002",
- Network::Testnet => "ssl://electrum.blockstream.info:60002",
- Network::Regtest => "tcp://localhost:60401",
- Network::Signet => "tcp://signet-electrumx.wakiyamap.dev:50001",
- _ => panic!("Unknown network"),
- };
- let config = electrum_client::Config::builder()
- .validate_domain(matches!(args.network, Network::Bitcoin))
- .build();
-
- let client = electrum_client::Client::from_config(electrum_url, config)?;
-
let electrum_cmd = match &args.command {
example_cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd,
general_cmd => {
&chain,
&keymap,
args.network,
- |tx| {
- client
- .transaction_broadcast(tx)
- .map(|_| ())
- .map_err(anyhow::Error::from)
+ |electrum_args, tx| {
+ let client = electrum_args.client(args.network)?;
+ client.transaction_broadcast(tx)?;
+ Ok(())
},
general_cmd.clone(),
);
}
};
+ let client = electrum_cmd.electrum_args().client(args.network)?;
+
let response = match electrum_cmd.clone() {
ElectrumCommands::Scan {
stop_gap,
scan_options,
+ ..
} => {
let (keychain_spks, tip) = {
let graph = &*graph.lock().unwrap();
mut utxos,
mut unconfirmed,
scan_options,
+ ..
} => {
// Get a short lock on the tracker to get the spks we're interested in
let graph = graph.lock().unwrap();