From f795a43cc72fdb4ef26ca349c4cb4f4bfd3b90b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Wed, 4 Oct 2023 16:45:57 +0800 Subject: [PATCH] feat(example_cli): allow chain specific args in examples So you can pass in the esplora/electrum/bitcoind_rpc server details in the example. Co-authored-by: LLFourn --- example-crates/example_cli/src/lib.rs | 435 +++++++++----------- example-crates/example_electrum/src/main.rs | 67 ++- example-crates/example_esplora/src/main.rs | 52 ++- 3 files changed, 280 insertions(+), 274 deletions(-) diff --git a/example-crates/example_cli/src/lib.rs b/example-crates/example_cli/src/lib.rs index c9459c35..1982c30c 100644 --- a/example-crates/example_cli/src/lib.rs +++ b/example-crates/example_cli/src/lib.rs @@ -34,7 +34,7 @@ pub type Database<'m, C> = Persist, C>; #[derive(Parser)] #[clap(author, version, about, long_about = None)] #[clap(propagate_version = true)] -pub struct Args { +pub struct Args { #[clap(env = "DESCRIPTOR")] pub descriptor: String, #[clap(env = "CHANGE_DESCRIPTOR")] @@ -50,14 +50,14 @@ pub struct Args { pub cp_limit: usize, #[clap(subcommand)] - pub command: Commands, + pub command: Commands, } #[allow(clippy::almost_swapped)] #[derive(Subcommand, Debug, Clone)] -pub enum Commands { +pub enum Commands { #[clap(flatten)] - ChainSpecific(S), + ChainSpecific(CS), /// Address generation and inspection. Address { #[clap(subcommand)] @@ -77,6 +77,8 @@ pub enum Commands { address: Address, #[clap(short, default_value = "bnb")] coin_select: CoinSelectionAlgo, + #[clap(flatten)] + chain_specfic: S, }, } @@ -183,225 +185,6 @@ impl core::fmt::Display for Keychain { } } -pub fn run_address_cmd( - graph: &mut KeychainTxGraph, - db: &Mutex>, - network: Network, - cmd: AddressCmd, -) -> anyhow::Result<()> -where - C: Default + Append + DeserializeOwned + Serialize + From>, -{ - 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( - graph: &KeychainTxGraph, - chain: &O, -) -> Result<(), O::Error> { - fn print_balances<'a>(title_str: &'a str, items: impl IntoIterator) { - 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( - graph: &KeychainTxGraph, - 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::, _>>()?; - - 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( - graph: &Mutex>, - db: &Mutex>, - chain: &O, - keymap: &HashMap, - 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>, -{ - 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( graph: &mut KeychainTxGraph, @@ -647,14 +430,14 @@ pub fn planned_utxos( +pub fn handle_commands( graph: &Mutex>, db: &Mutex>, chain: &Mutex, keymap: &HashMap, network: Network, - broadcast: impl FnOnce(&Transaction) -> anyhow::Result<()>, - cmd: Commands, + broadcast: impl FnOnce(S, &Transaction) -> anyhow::Result<()>, + cmd: Commands, ) -> anyhow::Result<()> where O::Error: std::error::Error + Send + Sync + 'static, @@ -664,45 +447,213 @@ where 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, + ) { + 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::, _>>()?; + + 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, + Args, KeyMap, KeychainTxOutIndex, Mutex>, @@ -714,7 +665,7 @@ where if std::env::var("BDK_DB_PATH").is_err() { std::env::set_var("BDK_DB_PATH", db_default_path); } - let args = Args::::parse(); + let args = Args::::parse(); let secp = Secp256k1::default(); let mut index = KeychainTxOutIndex::::default(); diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index a05e85c5..be5ffc7b 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -12,7 +12,7 @@ use bdk_chain::{ Append, ConfirmationHeightAnchor, }; use bdk_electrum::{ - electrum_client::{self, ElectrumApi}, + electrum_client::{self, Client, ElectrumApi}, ElectrumExt, ElectrumUpdate, }; use example_cli::{ @@ -33,6 +33,8 @@ enum ElectrumCommands { stop_gap: usize, #[clap(flatten)] scan_options: ScanOptions, + #[clap(flatten)] + electrum_args: ElectrumArgs, }, /// Scans particular addresses using the electrum API. Sync { @@ -50,9 +52,44 @@ enum ElectrumCommands { 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, +} + +impl ElectrumArgs { + pub fn client(&self, network: Network) -> anyhow::Result { + 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. @@ -67,7 +104,7 @@ type ChangeSet = ( fn main() -> anyhow::Result<()> { let (args, keymap, index, db, (disk_local_chain, disk_tx_graph)) = - example_cli::init::(DB_MAGIC, DB_PATH)?; + example_cli::init::(DB_MAGIC, DB_PATH)?; let graph = Mutex::new({ let mut graph = IndexedTxGraph::new(index); @@ -77,19 +114,6 @@ fn main() -> anyhow::Result<()> { 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 => { @@ -99,11 +123,10 @@ fn main() -> anyhow::Result<()> { &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(), ); @@ -113,10 +136,13 @@ fn main() -> anyhow::Result<()> { } }; + 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(); @@ -162,6 +188,7 @@ fn main() -> anyhow::Result<()> { 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(); diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index 5791fe61..d2ba62d0 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -37,6 +37,8 @@ enum EsploraCommands { stop_gap: usize, #[clap(flatten)] scan_options: ScanOptions, + #[clap(flatten)] + esplora_args: EsploraArgs, }, /// Scan for particular addresses and unconfirmed transactions using the esplora API. Sync { @@ -54,8 +56,40 @@ enum EsploraCommands { unconfirmed: bool, #[clap(flatten)] scan_options: ScanOptions, + #[clap(flatten)] + esplora_args: EsploraArgs, }, } +impl EsploraCommands { + fn esplora_args(&self) -> EsploraArgs { + match self { + EsploraCommands::Scan { esplora_args, .. } => esplora_args.clone(), + EsploraCommands::Sync { esplora_args, .. } => esplora_args.clone(), + } + } +} + +#[derive(clap::Args, Debug, Clone)] +pub struct EsploraArgs { + /// The esplora url endpoint to connect to e.g. `` + /// If not provided it'll be set to a default for the network provided + esplora_url: Option, +} + +impl EsploraArgs { + pub fn client(&self, network: Network) -> anyhow::Result { + let esplora_url = self.esplora_url.as_deref().unwrap_or(match network { + Network::Bitcoin => "https://blockstream.info/api", + Network::Testnet => "https://blockstream.info/testnet/api", + Network::Regtest => "http://localhost:3002", + Network::Signet => "https://mempool.space/signet/api", + _ => panic!("unsupported network"), + }); + + let client = esplora_client::Builder::new(esplora_url).build_blocking()?; + Ok(client) + } +} #[derive(Parser, Debug, Clone, PartialEq)] pub struct ScanOptions { @@ -66,7 +100,7 @@ pub struct ScanOptions { fn main() -> anyhow::Result<()> { let (args, keymap, index, db, init_changeset) = - example_cli::init::(DB_MAGIC, DB_PATH)?; + example_cli::init::(DB_MAGIC, DB_PATH)?; let (init_chain_changeset, init_indexed_tx_graph_changeset) = init_changeset; @@ -84,16 +118,6 @@ fn main() -> anyhow::Result<()> { chain }); - let esplora_url = match args.network { - Network::Bitcoin => "https://blockstream.info/api", - Network::Testnet => "https://blockstream.info/testnet/api", - Network::Regtest => "http://localhost:3002", - Network::Signet => "https://mempool.space/signet/api", - _ => panic!("unsupported network"), - }; - - let client = esplora_client::Builder::new(esplora_url).build_blocking()?; - let esplora_cmd = match &args.command { // These are commands that are handled by this example (sync, scan). example_cli::Commands::ChainSpecific(esplora_cmd) => esplora_cmd, @@ -105,7 +129,8 @@ fn main() -> anyhow::Result<()> { &chain, &keymap, args.network, - |tx| { + |esplora_args, tx| { + let client = esplora_args.client(args.network)?; client .broadcast(tx) .map(|_| ()) @@ -119,6 +144,7 @@ fn main() -> anyhow::Result<()> { } }; + let client = esplora_cmd.esplora_args().client(args.network)?; // Prepare the `IndexedTxGraph` update based on whether we are scanning or syncing. // Scanning: We are iterating through spks of all keychains and scanning for transactions for // each spk. We start with the lowest derivation index spk and stop scanning after `stop_gap` @@ -131,6 +157,7 @@ fn main() -> anyhow::Result<()> { EsploraCommands::Scan { stop_gap, scan_options, + .. } => { let keychain_spks = graph .lock() @@ -184,6 +211,7 @@ fn main() -> anyhow::Result<()> { mut utxos, mut unconfirmed, scan_options, + .. } => { if !(*all_spks || unused_spks || utxos || unconfirmed) { // If nothing is specifically selected, we select everything (except all spks).