From e9b8215b5ffaa6c38c9a3342f1b448bf90235835 Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Tue, 4 Apr 2023 17:23:07 +0200 Subject: [PATCH 1/2] WIP: implement indexed_tx_graph_example_cli --- Cargo.toml | 1 + .../indexed_tx_graph_example_cli/.gitignore | 1 + .../indexed_tx_graph_example_cli/Cargo.toml | 16 + .../indexed_tx_graph_example_cli/README.md | 1 + .../indexed_tx_graph_example_cli/src/lib.rs | 709 ++++++++++++++++++ 5 files changed, 728 insertions(+) create mode 100644 example-crates/indexed_tx_graph_example_cli/.gitignore create mode 100644 example-crates/indexed_tx_graph_example_cli/Cargo.toml create mode 100644 example-crates/indexed_tx_graph_example_cli/README.md create mode 100644 example-crates/indexed_tx_graph_example_cli/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 2104196be..047ad9781 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "example-crates/keychain_tracker_electrum", "example-crates/keychain_tracker_esplora", "example-crates/keychain_tracker_example_cli", + "example-crates/indexed_tx_graph_example_cli", "example-crates/wallet_electrum", "example-crates/wallet_esplora", "example-crates/wallet_esplora_async", diff --git a/example-crates/indexed_tx_graph_example_cli/.gitignore b/example-crates/indexed_tx_graph_example_cli/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/example-crates/indexed_tx_graph_example_cli/.gitignore @@ -0,0 +1 @@ +/target diff --git a/example-crates/indexed_tx_graph_example_cli/Cargo.toml b/example-crates/indexed_tx_graph_example_cli/Cargo.toml new file mode 100644 index 000000000..e61f025c1 --- /dev/null +++ b/example-crates/indexed_tx_graph_example_cli/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "indexed_tx_graph_example_cli" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[dependencies] +bdk_chain = { path = "../../crates/chain", features = ["serde", "miniscript"]} +bdk_file_store = { path = "../../crates/file_store" } +bdk_tmp_plan = { path = "../../nursery/tmp_plan" } +bdk_coin_select = { path = "../../nursery/coin_select" } + +clap = { version = "3.2.23", features = ["derive", "env"] } +anyhow = "1" +serde = { version = "1", features = ["derive"] } +serde_json = { version = "^1.0" } diff --git a/example-crates/indexed_tx_graph_example_cli/README.md b/example-crates/indexed_tx_graph_example_cli/README.md new file mode 100644 index 000000000..1d9370d1a --- /dev/null +++ b/example-crates/indexed_tx_graph_example_cli/README.md @@ -0,0 +1 @@ +Provides common command line processing logic between examples using the `KeychainTracker` diff --git a/example-crates/indexed_tx_graph_example_cli/src/lib.rs b/example-crates/indexed_tx_graph_example_cli/src/lib.rs new file mode 100644 index 000000000..88c0535d8 --- /dev/null +++ b/example-crates/indexed_tx_graph_example_cli/src/lib.rs @@ -0,0 +1,709 @@ +pub extern crate anyhow; +use anyhow::{anyhow, Result}; +use bdk_chain::{ + bitcoin::{ + secp256k1::Secp256k1, + util::sighash::{Prevouts, SighashCache}, + Address, LockTime, Network, Sequence, Transaction, TxIn, TxOut, + }, + indexed_tx_graph::IndexedTxGraph, + keychain::{DerivationAdditions, KeychainChangeSet, KeychainTracker, KeychainTxOutIndex}, + miniscript::{ + descriptor::{DescriptorSecretKey, KeyMap}, + Descriptor, DescriptorPublicKey, + }, + sparse_chain::{ChainPosition}, + BlockAnchor, ChainOracle, DescriptorExt, FullTxOut, ObservedIn, BlockId, +}; +use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue}; +use bdk_file_store::KeychainStore; +use clap::{Parser, Subcommand}; +use std::{ + cmp::Reverse, collections::HashMap, fmt::Debug, path::PathBuf, sync::Mutex, time::Duration, + ops::Deref, +}; + +pub use bdk_file_store; +pub use clap; + +#[derive(Parser)] +#[clap(author, version, about, long_about = None)] +#[clap(propagate_version = true)] +pub struct Args { + #[clap(env = "DESCRIPTOR")] + pub descriptor: String, + #[clap(env = "CHANGE_DESCRIPTOR")] + pub change_descriptor: Option, + + #[clap(env = "BITCOIN_NETWORK", long, default_value = "signet")] + pub network: Network, + + #[clap(env = "BDK_DB_PATH", long, default_value = ".bdk_example_db")] + pub db_path: PathBuf, + + #[clap(env = "BDK_CP_LIMIT", long, default_value = "20")] + pub cp_limit: usize, + + #[clap(subcommand)] + pub command: Commands, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum Commands { + #[clap(flatten)] + ChainSpecific(C), + /// Address generation and inspection. + Address { + #[clap(subcommand)] + addr_cmd: AddressCmd, + }, + /// Get the wallet balance. + Balance, + /// TxOut related commands. + #[clap(name = "txout")] + TxOut { + #[clap(subcommand)] + txout_cmd: TxOutCmd, + }, + /// Send coins to an address. + Send { + value: u64, + address: Address, + #[clap(short, default_value = "largest-first")] + coin_select: CoinSelectionAlgo, + }, +} + +#[derive(Clone, Debug)] +pub enum CoinSelectionAlgo { + LargestFirst, + SmallestFirst, + OldestFirst, + NewestFirst, + BranchAndBound, +} + +impl Default for CoinSelectionAlgo { + fn default() -> Self { + Self::LargestFirst + } +} + +impl core::str::FromStr for CoinSelectionAlgo { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + use CoinSelectionAlgo::*; + Ok(match s { + "largest-first" => LargestFirst, + "smallest-first" => SmallestFirst, + "oldest-first" => OldestFirst, + "newest-first" => NewestFirst, + "bnb" => BranchAndBound, + unknown => return Err(anyhow!("unknown coin selection algorithm '{}'", unknown)), + }) + } +} + +impl core::fmt::Display for CoinSelectionAlgo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use CoinSelectionAlgo::*; + write!( + f, + "{}", + match self { + LargestFirst => "largest-first", + SmallestFirst => "smallest-first", + OldestFirst => "oldest-first", + NewestFirst => "newest-first", + BranchAndBound => "bnb", + } + ) + } +} + +#[derive(Subcommand, Debug, Clone)] +pub enum AddressCmd { + /// Get the next unused address. + Next, + /// Get a new address regardless of the existing unused addresses. + New, + /// List all addresses + List { + #[clap(long)] + change: bool, + }, + Index, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum TxOutCmd { + List { + /// Return only spent outputs. + #[clap(short, long)] + spent: bool, + /// Return only unspent outputs. + #[clap(short, long)] + unspent: bool, + /// Return only confirmed outputs. + #[clap(long)] + confirmed: bool, + /// Return only unconfirmed outputs. + #[clap(long)] + unconfirmed: bool, + }, +} + +#[derive( + Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, serde::Deserialize, serde::Serialize, +)] +pub enum Keychain { + External, + Internal, +} + +impl core::fmt::Display for Keychain { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Keychain::External => write!(f, "external"), + Keychain::Internal => write!(f, "internal"), + } + } +} + +/// A structure defining the output of an [`AddressCmd`]` execution. +#[derive(serde::Serialize, serde::Deserialize)] +pub struct AddrsOutput { + keychain: String, + index: u32, + addrs: Address, + used: bool, +} + +pub fn run_address_cmd( + indexed_tx_graph: &Mutex>>, + addr_cmd: AddressCmd, + network: Network, +) -> Result<()> +where + A: bdk_chain::BlockAnchor, + KeychainChangeSet: serde::Serialize + serde::de::DeserializeOwned, +{ + let mut indexed_tx_graph = indexed_tx_graph.lock().unwrap(); + let txout_index = &mut indexed_tx_graph.index; + let addr_cmmd_output = match addr_cmd { + AddressCmd::Next => Some(txout_index.next_unused_spk(&Keychain::External)), + AddressCmd::New => Some(txout_index.reveal_next_spk(&Keychain::External)), + _ => None, + }; + + if let Some(((index, spk), additions)) = addr_cmmd_output { + //let mut db = db.lock().unwrap(); + // update database since we're about to give out a new address + //db.append_changeset(&additions.into())?; + + let spk = spk.clone(); + let address = + Address::from_script(&spk, network).expect("should always be able to derive address"); + eprintln!("This is the address at index {}", index); + println!("{}", address); + } + + match addr_cmd { + AddressCmd::Next | AddressCmd::New => { + /* covered */ + Ok(()) + } + AddressCmd::Index => { + for (keychain, derivation_index) in txout_index.last_revealed_indices() { + println!("{:?}: {}", keychain, derivation_index); + } + Ok(()) + } + AddressCmd::List { change } => { + let target_keychain = match change { + true => Keychain::Internal, + false => Keychain::External, + }; + for (index, spk) in txout_index.revealed_spks_of_keychain(&target_keychain) { + let address = Address::from_script(spk, network) + .expect("should always be able to derive address"); + println!( + "{:?} {} used:{}", + index, + address, + txout_index.is_used(&(target_keychain, index)) + ); + } + Ok(()) + } + } +} + +pub fn run_balance_cmd( + indexed_tx_graph: &Mutex>>, + chain_oracle: &Mutex>, +) where + E: std::fmt::Debug, +{ + let indexed_tx_graph = indexed_tx_graph.lock().unwrap(); + let chain_oracle = chain_oracle.lock().unwrap(); + let (confirmed, unconfirmed) = indexed_tx_graph + .try_list_chain_utxos(chain_oracle.deref()) + .fold((0, 0), |(confirmed, unconfirmed), res| { + let utxo = res.unwrap(); + if utxo.chain_position.height().is_confirmed() { + (confirmed + utxo.txout.value, unconfirmed) + } else { + (confirmed, unconfirmed + utxo.txout.value) + } + }); + + println!("confirmed: {}", confirmed); + println!("unconfirmed: {}", unconfirmed); +} + +pub fn run_txo_cmd( + txout_cmd: TxOutCmd, + indexed_tx_graph: &Mutex>>, + chain_oracle: &Mutex>, + network: Network, +) +where ObservedIn: ChainPosition, + E: std::fmt::Debug, +{ + match txout_cmd { + TxOutCmd::List { + unspent, + spent, + confirmed, + unconfirmed, + } => { + let indexed_tx_graph = indexed_tx_graph.lock().unwrap(); + let chain_oracle = chain_oracle.lock().unwrap(); + #[allow(clippy::type_complexity)] // FIXME + let txouts: Box>>> = match (unspent, spent) + { + (false, true) => Box::new( + indexed_tx_graph + .try_list_chain_utxos(chain_oracle.deref()) + .map(|u| u.unwrap()) + ), + _ => Box::new(indexed_tx_graph.try_list_chain_txouts(chain_oracle.deref()).map(|u| u.unwrap())), + }; + + #[allow(clippy::type_complexity)] // FIXME + let txouts: Box>>> = + match (confirmed, unconfirmed) { + (true, false) => Box::new( + txouts.filter(|txout| txout.chain_position.height().is_confirmed()), + ), + (false, true) => Box::new( + txouts.filter(|txout| !txout.chain_position.height().is_confirmed()), + ), + _ => txouts, + }; + + for full_txout in txouts { + let address = + Address::from_script(&full_txout.txout.script_pubkey, network).unwrap(); + let spk_index = indexed_tx_graph.index.index_of_spk(&full_txout.txout.script_pubkey); + + println!( + "{:?} {} {} {} spent:{:?}", + spk_index, + full_txout.txout.value, + full_txout.outpoint, + address, + full_txout.spent_by + ) + } + } + } +} + +#[allow(clippy::type_complexity)] // FIXME +pub fn create_tx( + value: u64, + address: Address, + coin_select: CoinSelectionAlgo, + indexed_tx_graph: &mut IndexedTxGraph>, + chain_oracle: &impl ChainOracle, + keymap: &HashMap, +) -> Result<( + Transaction, + Option<(DerivationAdditions, (Keychain, u32))>, +)> where ObservedIn: ChainPosition { + let mut additions = DerivationAdditions::default(); + + let assets = bdk_tmp_plan::Assets { + keys: keymap.iter().map(|(pk, _)| pk.clone()).collect(), + ..Default::default() + }; + + // TODO use planning module + let mut candidates = planned_utxos(indexed_tx_graph, chain_oracle, &assets).collect::>(); + + // apply coin selection algorithm + match coin_select { + CoinSelectionAlgo::LargestFirst => { + candidates.sort_by_key(|(_, utxo)| Reverse(utxo.txout.value)) + } + CoinSelectionAlgo::SmallestFirst => candidates.sort_by_key(|(_, utxo)| utxo.txout.value), + CoinSelectionAlgo::OldestFirst => { + candidates.sort_by_key(|(_, utxo)| utxo.chain_position.clone()) + } + CoinSelectionAlgo::NewestFirst => { + candidates.sort_by_key(|(_, utxo)| Reverse(utxo.chain_position.clone())) + } + CoinSelectionAlgo::BranchAndBound => {} + } + + // turn the txos we chose into weight and value + let wv_candidates = candidates + .iter() + .map(|(plan, utxo)| { + WeightedValue::new( + utxo.txout.value, + plan.expected_weight() as _, + plan.witness_version().is_some(), + ) + }) + .collect(); + + let mut outputs = vec![TxOut { + value, + script_pubkey: address.script_pubkey(), + }]; + + let internal_keychain = if indexed_tx_graph + .index + .keychains() + .get(&Keychain::Internal) + .is_some() + { + Keychain::Internal + } else { + Keychain::External + }; + + let ((change_index, change_script), change_additions) = indexed_tx_graph + .index + .next_unused_spk(&internal_keychain); + additions.append(change_additions); + + // Clone to drop the immutable reference. + let change_script = change_script.clone(); + + let change_plan = bdk_tmp_plan::plan_satisfaction( + &indexed_tx_graph + .index + .keychains() + .get(&internal_keychain) + .expect("must exist") + .at_derivation_index(change_index), + &assets, + ) + .expect("failed to obtain change plan"); + + let mut change_output = TxOut { + value: 0, + script_pubkey: change_script, + }; + + let cs_opts = CoinSelectorOpt { + target_feerate: 0.5, + min_drain_value: indexed_tx_graph + .index + .keychains() + .get(&internal_keychain) + .expect("must exist") + .dust_value(), + ..CoinSelectorOpt::fund_outputs( + &outputs, + &change_output, + change_plan.expected_weight() as u32, + ) + }; + + // TODO: How can we make it easy to shuffle in order of inputs and outputs here? + // apply coin selection by saying we need to fund these outputs + let mut coin_selector = CoinSelector::new(&wv_candidates, &cs_opts); + + // just select coins in the order provided until we have enough + // only use the first result (least waste) + let selection = match coin_select { + CoinSelectionAlgo::BranchAndBound => { + coin_select_bnb(Duration::from_secs(10), coin_selector.clone()) + .map_or_else(|| coin_selector.select_until_finished(), |cs| cs.finish())? + } + _ => coin_selector.select_until_finished()?, + }; + let (_, selection_meta) = selection.best_strategy(); + + // get the selected utxos + let selected_txos = selection.apply_selection(&candidates).collect::>(); + + if let Some(drain_value) = selection_meta.drain_value { + change_output.value = drain_value; + // if the selection tells us to use change and the change value is sufficient, we add it as an output + outputs.push(change_output) + } + + let mut transaction = Transaction { + version: 0x02, + /* TODO: somehow ask to the chainoracle for the last block? + lock_time: keychain_tracker + .chain() + .latest_checkpoint() + .and_then(|block_id| LockTime::from_height(block_id.height).ok()) + .unwrap_or(LockTime::ZERO) + .into(), + */ + lock_time: LockTime::ZERO.into(), + input: selected_txos + .iter() + .map(|(_, utxo)| TxIn { + previous_output: utxo.outpoint, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..Default::default() + }) + .collect(), + output: outputs, + }; + + let prevouts = selected_txos + .iter() + .map(|(_, utxo)| utxo.txout.clone()) + .collect::>(); + let sighash_prevouts = Prevouts::All(&prevouts); + + // first, set tx values for the plan so that we don't change them while signing + for (i, (plan, _)) in selected_txos.iter().enumerate() { + if let Some(sequence) = plan.required_sequence() { + transaction.input[i].sequence = sequence + } + } + + // create a short lived transaction + let _sighash_tx = transaction.clone(); + let mut sighash_cache = SighashCache::new(&_sighash_tx); + + for (i, (plan, _)) in selected_txos.iter().enumerate() { + let requirements = plan.requirements(); + let mut auth_data = bdk_tmp_plan::SatisfactionMaterial::default(); + assert!( + !requirements.requires_hash_preimages(), + "can't have hash pre-images since we didn't provide any." + ); + assert!( + requirements.signatures.sign_with_keymap( + i, + keymap, + &sighash_prevouts, + None, + None, + &mut sighash_cache, + &mut auth_data, + &Secp256k1::default(), + )?, + "we should have signed with this input." + ); + + match plan.try_complete(&auth_data) { + bdk_tmp_plan::PlanState::Complete { + final_script_sig, + final_script_witness, + } => { + if let Some(witness) = final_script_witness { + transaction.input[i].witness = witness; + } + + if let Some(script_sig) = final_script_sig { + transaction.input[i].script_sig = script_sig; + } + } + bdk_tmp_plan::PlanState::Incomplete(_) => { + return Err(anyhow!( + "we weren't able to complete the plan with our keys." + )); + } + } + } + + let change_info = if selection_meta.drain_value.is_some() { + Some((additions, (internal_keychain, change_index))) + } else { + None + }; + + Ok((transaction, change_info)) +} + +pub fn handle_commands( + command: Commands, + broadcast: impl FnOnce(&Transaction) -> Result<()>, + // we Mutex around these not because we need them for a simple CLI app but to demonstrate how + // all the stuff we're doing can be made thread-safe and not keep locks up over an IO bound. + indexed_tx_graph: &Mutex>>, + chain_oracle: &Mutex>, + //store: &Mutex>, + network: Network, + keymap: &HashMap, +) -> Result<()> +where + E: std::fmt::Debug, + KeychainChangeSet: serde::Serialize + serde::de::DeserializeOwned, +{ + match command { + // TODO: Make these functions return stuffs + Commands::Address { addr_cmd } => run_address_cmd(indexed_tx_graph, addr_cmd, network), + Commands::Balance => { + run_balance_cmd(indexed_tx_graph, chain_oracle); + Ok(()) + } + Commands::TxOut { txout_cmd } => { + run_txo_cmd(txout_cmd, indexed_tx_graph, chain_oracle, network); + Ok(()) + } + Commands::Send { + value, + address, + coin_select, + } => { + let (transaction, change_index) = { + // take mutable ref to construct tx -- it is only open for a short time while building it. + let indexed_tx_graph = &mut *indexed_tx_graph.lock().unwrap(); + let chain_oracle = & *chain_oracle.lock().unwrap(); + let (transaction, change_info) = + create_tx(value, address, coin_select, indexed_tx_graph, chain_oracle, keymap)?; + + if let Some((change_derivation_changes, (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 store = &mut *store.lock().unwrap(); + //store.append_changeset(&change_derivation_changes.into())?; + + // 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. + indexed_tx_graph + .index + .mark_used(&change_keychain, index); + (transaction, Some((change_keychain, index))) + } else { + (transaction, None) + } + }; + + match (broadcast)(&transaction) { + Ok(_) => { + println!("Broadcasted Tx : {}", transaction.txid()); + let mut indexed_tx_graph = indexed_tx_graph.lock().unwrap(); + let changeset = indexed_tx_graph.insert_tx(&transaction, ObservedIn::Mempool(23 /* FIXME: time should be right now */)); + /* + let store = &mut *store.lock().unwrap(); + // 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. + store.append_changeset(&changeset)?; + */ + Ok(()) + } + Err(e) => { + let indexed_tx_graph = &mut *indexed_tx_graph.lock().unwrap(); + if let Some((keychain, index)) = change_index { + // We failed to broadcast, so allow our change address to be used in the future + indexed_tx_graph.index.unmark_used(&keychain, index); + } + Err(e) + } + } + } + Commands::ChainSpecific(_) => { + todo!("example code is meant to handle this!") + } + } +} + +#[allow(clippy::type_complexity)] // FIXME +pub fn init() -> anyhow::Result<( + Args, + KeyMap, + // These don't need to have mutexes around them, but we want the cli example code to make it obvious how they + // are thread-safe, forcing the example developers to show where they would lock and unlock things. + Mutex>>, +)> +where + A: BlockAnchor, +{ + let args = Args::::parse(); + let secp = Secp256k1::default(); + let (descriptor, mut keymap) = + Descriptor::::parse_descriptor(&secp, &args.descriptor)?; + + let mut indexed_tx_graph = IndexedTxGraph::>::default(); + + indexed_tx_graph + .index + .add_keychain(Keychain::External, descriptor); + + let internal = args + .change_descriptor + .clone() + .map(|descriptor| Descriptor::::parse_descriptor(&secp, &descriptor)) + .transpose()?; + if let Some((internal_descriptor, internal_keymap)) = internal { + keymap.extend(internal_keymap); + indexed_tx_graph + .index + .add_keychain(Keychain::Internal, internal_descriptor); + }; + + /* + let mut db = KeychainStore::::new_from_path(args.db_path.as_path())?; + + if let Err(e) = db.load_into_keychain_tracker(&mut tracker) { + match tracker.chain().latest_checkpoint() { + Some(checkpoint) => eprintln!("Failed to load all changesets from {}. Last checkpoint was at height {}. Error: {}", args.db_path.display(), checkpoint.height, e), + None => eprintln!("Failed to load any checkpoints from {}: {}", args.db_path.display(), e), + + } + eprintln!("⚠ Consider running a rescan of chain data."); + } + */ + + Ok(( + args, + keymap, + Mutex::new(indexed_tx_graph), /*Mutex::new(db)*/ + )) +} + +pub fn planned_utxos<'a, AK: bdk_tmp_plan::CanDerive + Clone, A: BlockAnchor, E>( + indexed_tx_graph: &'a IndexedTxGraph>, + chain_oracle: &'a impl ChainOracle, + assets: &'a bdk_tmp_plan::Assets, +) -> impl Iterator, FullTxOut>)> + 'a +where ObservedIn: ChainPosition { + indexed_tx_graph.index.txouts() + .filter_map(move |(keychain, derivation_index, full_txout)| { + /* + Some(( + bdk_tmp_plan::plan_satisfaction( + &indexed_tx_graph + .index + .keychains() + .get(keychain) + .expect("must exist since we have a utxo for it") + .at_derivation_index(*derivation_index), + assets, + )?, + full_txout.clone(), + )) + */ + todo!() + }, + ) +} From ef4927ea15730437138073b86aff033bd55e378c Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Sat, 29 Apr 2023 10:37:01 -0500 Subject: [PATCH 2/2] WIP: Update examples to use indexed_tx_graph_example_cli --- .../indexed_tx_graph_example_cli/src/lib.rs | 842 +++++++++--------- .../keychain_tracker_electrum/Cargo.toml | 2 +- .../keychain_tracker_electrum/src/main.rs | 46 +- .../keychain_tracker_esplora/Cargo.toml | 2 +- .../keychain_tracker_esplora/src/main.rs | 24 +- 5 files changed, 460 insertions(+), 456 deletions(-) diff --git a/example-crates/indexed_tx_graph_example_cli/src/lib.rs b/example-crates/indexed_tx_graph_example_cli/src/lib.rs index 88c0535d8..58d65468f 100644 --- a/example-crates/indexed_tx_graph_example_cli/src/lib.rs +++ b/example-crates/indexed_tx_graph_example_cli/src/lib.rs @@ -1,20 +1,13 @@ pub extern crate anyhow; use anyhow::{anyhow, Result}; -use bdk_chain::{ - bitcoin::{ - secp256k1::Secp256k1, - util::sighash::{Prevouts, SighashCache}, - Address, LockTime, Network, Sequence, Transaction, TxIn, TxOut, - }, - indexed_tx_graph::IndexedTxGraph, - keychain::{DerivationAdditions, KeychainChangeSet, KeychainTracker, KeychainTxOutIndex}, - miniscript::{ - descriptor::{DescriptorSecretKey, KeyMap}, - Descriptor, DescriptorPublicKey, - }, - sparse_chain::{ChainPosition}, - BlockAnchor, ChainOracle, DescriptorExt, FullTxOut, ObservedIn, BlockId, -}; +use bdk_chain::{bitcoin::{ + secp256k1::Secp256k1, + util::sighash::{Prevouts, SighashCache}, + Address, LockTime, Network, Sequence, Transaction, TxIn, TxOut, +}, indexed_tx_graph::IndexedTxGraph, keychain::{DerivationAdditions, KeychainChangeSet, KeychainTracker, KeychainTxOutIndex}, miniscript::{ + descriptor::{DescriptorSecretKey, KeyMap}, + Descriptor, DescriptorPublicKey, +}, sparse_chain::{ChainPosition}, ChainOracle, DescriptorExt, FullTxOut, BlockId, Append, Anchor, ObservedAs}; use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue}; use bdk_file_store::KeychainStore; use clap::{Parser, Subcommand}; @@ -32,6 +25,7 @@ pub use clap; pub struct Args { #[clap(env = "DESCRIPTOR")] pub descriptor: String, + #[clap(env = "CHANGE_DESCRIPTOR")] pub change_descriptor: Option, @@ -49,7 +43,7 @@ pub struct Args { } #[derive(Subcommand, Debug, Clone)] -pub enum Commands { +pub enum Commands { #[clap(flatten)] ChainSpecific(C), /// Address generation and inspection. @@ -57,21 +51,21 @@ pub enum Commands { #[clap(subcommand)] addr_cmd: AddressCmd, }, - /// Get the wallet balance. - Balance, - /// TxOut related commands. - #[clap(name = "txout")] - TxOut { - #[clap(subcommand)] - txout_cmd: TxOutCmd, - }, - /// Send coins to an address. - Send { - value: u64, - address: Address, - #[clap(short, default_value = "largest-first")] - coin_select: CoinSelectionAlgo, - }, +// /// Get the wallet balance. +// Balance, +// /// TxOut related commands. +// #[clap(name = "txout")] +// TxOut { +// #[clap(subcommand)] +// txout_cmd: TxOutCmd, +// }, +// /// Send coins to an address. +// Send { +// value: u64, +// address: Address, +// #[clap(short, default_value = "largest-first")] +// coin_select: CoinSelectionAlgo, +// }, } #[derive(Clone, Debug)] @@ -186,18 +180,18 @@ pub fn run_address_cmd( network: Network, ) -> Result<()> where - A: bdk_chain::BlockAnchor, + A: Anchor, KeychainChangeSet: serde::Serialize + serde::de::DeserializeOwned, { let mut indexed_tx_graph = indexed_tx_graph.lock().unwrap(); let txout_index = &mut indexed_tx_graph.index; - let addr_cmmd_output = match addr_cmd { + let addr_cmd_output = match addr_cmd { AddressCmd::Next => Some(txout_index.next_unused_spk(&Keychain::External)), AddressCmd::New => Some(txout_index.reveal_next_spk(&Keychain::External)), _ => None, }; - if let Some(((index, spk), additions)) = addr_cmmd_output { + if let Some(((index, spk), additions)) = addr_cmd_output { //let mut db = db.lock().unwrap(); // update database since we're about to give out a new address //db.append_changeset(&additions.into())?; @@ -240,305 +234,305 @@ where } } -pub fn run_balance_cmd( - indexed_tx_graph: &Mutex>>, - chain_oracle: &Mutex>, -) where - E: std::fmt::Debug, -{ - let indexed_tx_graph = indexed_tx_graph.lock().unwrap(); - let chain_oracle = chain_oracle.lock().unwrap(); - let (confirmed, unconfirmed) = indexed_tx_graph - .try_list_chain_utxos(chain_oracle.deref()) - .fold((0, 0), |(confirmed, unconfirmed), res| { - let utxo = res.unwrap(); - if utxo.chain_position.height().is_confirmed() { - (confirmed + utxo.txout.value, unconfirmed) - } else { - (confirmed, unconfirmed + utxo.txout.value) - } - }); - - println!("confirmed: {}", confirmed); - println!("unconfirmed: {}", unconfirmed); -} - -pub fn run_txo_cmd( - txout_cmd: TxOutCmd, - indexed_tx_graph: &Mutex>>, - chain_oracle: &Mutex>, - network: Network, -) -where ObservedIn: ChainPosition, - E: std::fmt::Debug, -{ - match txout_cmd { - TxOutCmd::List { - unspent, - spent, - confirmed, - unconfirmed, - } => { - let indexed_tx_graph = indexed_tx_graph.lock().unwrap(); - let chain_oracle = chain_oracle.lock().unwrap(); - #[allow(clippy::type_complexity)] // FIXME - let txouts: Box>>> = match (unspent, spent) - { - (false, true) => Box::new( - indexed_tx_graph - .try_list_chain_utxos(chain_oracle.deref()) - .map(|u| u.unwrap()) - ), - _ => Box::new(indexed_tx_graph.try_list_chain_txouts(chain_oracle.deref()).map(|u| u.unwrap())), - }; - - #[allow(clippy::type_complexity)] // FIXME - let txouts: Box>>> = - match (confirmed, unconfirmed) { - (true, false) => Box::new( - txouts.filter(|txout| txout.chain_position.height().is_confirmed()), - ), - (false, true) => Box::new( - txouts.filter(|txout| !txout.chain_position.height().is_confirmed()), - ), - _ => txouts, - }; - - for full_txout in txouts { - let address = - Address::from_script(&full_txout.txout.script_pubkey, network).unwrap(); - let spk_index = indexed_tx_graph.index.index_of_spk(&full_txout.txout.script_pubkey); - - println!( - "{:?} {} {} {} spent:{:?}", - spk_index, - full_txout.txout.value, - full_txout.outpoint, - address, - full_txout.spent_by - ) - } - } - } -} - -#[allow(clippy::type_complexity)] // FIXME -pub fn create_tx( - value: u64, - address: Address, - coin_select: CoinSelectionAlgo, - indexed_tx_graph: &mut IndexedTxGraph>, - chain_oracle: &impl ChainOracle, - keymap: &HashMap, -) -> Result<( - Transaction, - Option<(DerivationAdditions, (Keychain, u32))>, -)> where ObservedIn: ChainPosition { - let mut additions = DerivationAdditions::default(); - - let assets = bdk_tmp_plan::Assets { - keys: keymap.iter().map(|(pk, _)| pk.clone()).collect(), - ..Default::default() - }; - - // TODO use planning module - let mut candidates = planned_utxos(indexed_tx_graph, chain_oracle, &assets).collect::>(); - - // apply coin selection algorithm - match coin_select { - CoinSelectionAlgo::LargestFirst => { - candidates.sort_by_key(|(_, utxo)| Reverse(utxo.txout.value)) - } - CoinSelectionAlgo::SmallestFirst => candidates.sort_by_key(|(_, utxo)| utxo.txout.value), - CoinSelectionAlgo::OldestFirst => { - candidates.sort_by_key(|(_, utxo)| utxo.chain_position.clone()) - } - CoinSelectionAlgo::NewestFirst => { - candidates.sort_by_key(|(_, utxo)| Reverse(utxo.chain_position.clone())) - } - CoinSelectionAlgo::BranchAndBound => {} - } - - // turn the txos we chose into weight and value - let wv_candidates = candidates - .iter() - .map(|(plan, utxo)| { - WeightedValue::new( - utxo.txout.value, - plan.expected_weight() as _, - plan.witness_version().is_some(), - ) - }) - .collect(); - - let mut outputs = vec![TxOut { - value, - script_pubkey: address.script_pubkey(), - }]; - - let internal_keychain = if indexed_tx_graph - .index - .keychains() - .get(&Keychain::Internal) - .is_some() - { - Keychain::Internal - } else { - Keychain::External - }; - - let ((change_index, change_script), change_additions) = indexed_tx_graph - .index - .next_unused_spk(&internal_keychain); - additions.append(change_additions); - - // Clone to drop the immutable reference. - let change_script = change_script.clone(); - - let change_plan = bdk_tmp_plan::plan_satisfaction( - &indexed_tx_graph - .index - .keychains() - .get(&internal_keychain) - .expect("must exist") - .at_derivation_index(change_index), - &assets, - ) - .expect("failed to obtain change plan"); - - let mut change_output = TxOut { - value: 0, - script_pubkey: change_script, - }; - - let cs_opts = CoinSelectorOpt { - target_feerate: 0.5, - min_drain_value: indexed_tx_graph - .index - .keychains() - .get(&internal_keychain) - .expect("must exist") - .dust_value(), - ..CoinSelectorOpt::fund_outputs( - &outputs, - &change_output, - change_plan.expected_weight() as u32, - ) - }; - - // TODO: How can we make it easy to shuffle in order of inputs and outputs here? - // apply coin selection by saying we need to fund these outputs - let mut coin_selector = CoinSelector::new(&wv_candidates, &cs_opts); - - // just select coins in the order provided until we have enough - // only use the first result (least waste) - let selection = match coin_select { - CoinSelectionAlgo::BranchAndBound => { - coin_select_bnb(Duration::from_secs(10), coin_selector.clone()) - .map_or_else(|| coin_selector.select_until_finished(), |cs| cs.finish())? - } - _ => coin_selector.select_until_finished()?, - }; - let (_, selection_meta) = selection.best_strategy(); - - // get the selected utxos - let selected_txos = selection.apply_selection(&candidates).collect::>(); - - if let Some(drain_value) = selection_meta.drain_value { - change_output.value = drain_value; - // if the selection tells us to use change and the change value is sufficient, we add it as an output - outputs.push(change_output) - } - - let mut transaction = Transaction { - version: 0x02, - /* TODO: somehow ask to the chainoracle for the last block? - lock_time: keychain_tracker - .chain() - .latest_checkpoint() - .and_then(|block_id| LockTime::from_height(block_id.height).ok()) - .unwrap_or(LockTime::ZERO) - .into(), - */ - lock_time: LockTime::ZERO.into(), - input: selected_txos - .iter() - .map(|(_, utxo)| TxIn { - previous_output: utxo.outpoint, - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - ..Default::default() - }) - .collect(), - output: outputs, - }; - - let prevouts = selected_txos - .iter() - .map(|(_, utxo)| utxo.txout.clone()) - .collect::>(); - let sighash_prevouts = Prevouts::All(&prevouts); - - // first, set tx values for the plan so that we don't change them while signing - for (i, (plan, _)) in selected_txos.iter().enumerate() { - if let Some(sequence) = plan.required_sequence() { - transaction.input[i].sequence = sequence - } - } - - // create a short lived transaction - let _sighash_tx = transaction.clone(); - let mut sighash_cache = SighashCache::new(&_sighash_tx); - - for (i, (plan, _)) in selected_txos.iter().enumerate() { - let requirements = plan.requirements(); - let mut auth_data = bdk_tmp_plan::SatisfactionMaterial::default(); - assert!( - !requirements.requires_hash_preimages(), - "can't have hash pre-images since we didn't provide any." - ); - assert!( - requirements.signatures.sign_with_keymap( - i, - keymap, - &sighash_prevouts, - None, - None, - &mut sighash_cache, - &mut auth_data, - &Secp256k1::default(), - )?, - "we should have signed with this input." - ); - - match plan.try_complete(&auth_data) { - bdk_tmp_plan::PlanState::Complete { - final_script_sig, - final_script_witness, - } => { - if let Some(witness) = final_script_witness { - transaction.input[i].witness = witness; - } - - if let Some(script_sig) = final_script_sig { - transaction.input[i].script_sig = script_sig; - } - } - bdk_tmp_plan::PlanState::Incomplete(_) => { - return Err(anyhow!( - "we weren't able to complete the plan with our keys." - )); - } - } - } - - let change_info = if selection_meta.drain_value.is_some() { - Some((additions, (internal_keychain, change_index))) - } else { - None - }; - - Ok((transaction, change_info)) -} +//pub fn run_balance_cmd( +// indexed_tx_graph: &Mutex>>, +// chain_oracle: &Mutex>, +//) where +// E: std::fmt::Debug, +//{ +// let indexed_tx_graph = indexed_tx_graph.lock().unwrap(); +// let chain_oracle = chain_oracle.lock().unwrap(); +// let (confirmed, unconfirmed) = indexed_tx_graph +// .try_list_chain_utxos(chain_oracle.deref()) +// .fold((0, 0), |(confirmed, unconfirmed), res| { +// let utxo = res.unwrap(); +// if utxo.chain_position.height().is_confirmed() { +// (confirmed + utxo.txout.value, unconfirmed) +// } else { +// (confirmed, unconfirmed + utxo.txout.value) +// } +// }); +// +// println!("confirmed: {}", confirmed); +// println!("unconfirmed: {}", unconfirmed); +//} +// +//pub fn run_txo_cmd( +// txout_cmd: TxOutCmd, +// indexed_tx_graph: &Mutex>>, +// chain_oracle: &Mutex>, +// network: Network, +//) +//where ObservedAs: ChainPosition, +// E: Debug, +//{ +// match txout_cmd { +// TxOutCmd::List { +// unspent, +// spent, +// confirmed, +// unconfirmed, +// } => { +// let indexed_tx_graph = indexed_tx_graph.lock().unwrap(); +// let chain_oracle = chain_oracle.lock().unwrap(); +// #[allow(clippy::type_complexity)] // FIXME +// let txouts: Box>>> = match (unspent, spent) +// { +// (false, true) => Box::new( +// indexed_tx_graph +// .try_list_chain_utxos(chain_oracle.deref()) +// .map(|u| u.unwrap()) +// ), +// _ => Box::new(indexed_tx_graph.try_list_chain_txouts(chain_oracle.deref()).map(|u| u.unwrap())), +// }; +// +// #[allow(clippy::type_complexity)] // FIXME +// let txouts: Box>>> = +// match (confirmed, unconfirmed) { +// (true, false) => Box::new( +// txouts.filter(|txout| txout.chain_position.height().is_confirmed()), +// ), +// (false, true) => Box::new( +// txouts.filter(|txout| !txout.chain_position.height().is_confirmed()), +// ), +// _ => txouts, +// }; +// +// for full_txout in txouts { +// let address = +// Address::from_script(&full_txout.txout.script_pubkey, network).unwrap(); +// let spk_index = indexed_tx_graph.index.index_of_spk(&full_txout.txout.script_pubkey); +// +// println!( +// "{:?} {} {} {} spent:{:?}", +// spk_index, +// full_txout.txout.value, +// full_txout.outpoint, +// address, +// full_txout.spent_by +// ) +// } +// } +// } +//} +// +//#[allow(clippy::type_complexity)] // FIXME +//pub fn create_tx( +// value: u64, +// address: Address, +// coin_select: CoinSelectionAlgo, +// indexed_tx_graph: &mut IndexedTxGraph>, +// chain_oracle: &impl ChainOracle, +// keymap: &HashMap, +//) -> Result<( +// Transaction, +// Option<(DerivationAdditions, (Keychain, u32))>, +//)> where ObservedAs: ChainPosition { +// let mut additions = DerivationAdditions::default(); +// +// let assets = bdk_tmp_plan::Assets { +// keys: keymap.iter().map(|(pk, _)| pk.clone()).collect(), +// ..Default::default() +// }; +// +// // TODO use planning module +// let mut candidates = planned_utxos(indexed_tx_graph, chain_oracle, &assets).collect::>(); +// +// // apply coin selection algorithm +// match coin_select { +// CoinSelectionAlgo::LargestFirst => { +// candidates.sort_by_key(|(_, utxo)| Reverse(utxo.txout.value)) +// } +// CoinSelectionAlgo::SmallestFirst => candidates.sort_by_key(|(_, utxo)| utxo.txout.value), +// CoinSelectionAlgo::OldestFirst => { +// candidates.sort_by_key(|(_, utxo)| utxo.chain_position.clone()) +// } +// CoinSelectionAlgo::NewestFirst => { +// candidates.sort_by_key(|(_, utxo)| Reverse(utxo.chain_position.clone())) +// } +// CoinSelectionAlgo::BranchAndBound => {} +// } +// +// // turn the txos we chose into weight and value +// let wv_candidates = candidates +// .iter() +// .map(|(plan, utxo)| { +// WeightedValue::new( +// utxo.txout.value, +// plan.expected_weight() as _, +// plan.witness_version().is_some(), +// ) +// }) +// .collect(); +// +// let mut outputs = vec![TxOut { +// value, +// script_pubkey: address.script_pubkey(), +// }]; +// +// let internal_keychain = if indexed_tx_graph +// .index +// .keychains() +// .get(&Keychain::Internal) +// .is_some() +// { +// Keychain::Internal +// } else { +// Keychain::External +// }; +// +// let ((change_index, change_script), change_additions) = indexed_tx_graph +// .index +// .next_unused_spk(&internal_keychain); +// additions.append(change_additions); +// +// // Clone to drop the immutable reference. +// let change_script = change_script.clone(); +// +// let change_plan = bdk_tmp_plan::plan_satisfaction( +// &indexed_tx_graph +// .index +// .keychains() +// .get(&internal_keychain) +// .expect("must exist") +// .at_derivation_index(change_index), +// &assets, +// ) +// .expect("failed to obtain change plan"); +// +// let mut change_output = TxOut { +// value: 0, +// script_pubkey: change_script, +// }; +// +// let cs_opts = CoinSelectorOpt { +// target_feerate: 0.5, +// min_drain_value: indexed_tx_graph +// .index +// .keychains() +// .get(&internal_keychain) +// .expect("must exist") +// .dust_value(), +// ..CoinSelectorOpt::fund_outputs( +// &outputs, +// &change_output, +// change_plan.expected_weight() as u32, +// ) +// }; +// +// // TODO: How can we make it easy to shuffle in order of inputs and outputs here? +// // apply coin selection by saying we need to fund these outputs +// let mut coin_selector = CoinSelector::new(&wv_candidates, &cs_opts); +// +// // just select coins in the order provided until we have enough +// // only use the first result (least waste) +// let selection = match coin_select { +// CoinSelectionAlgo::BranchAndBound => { +// coin_select_bnb(Duration::from_secs(10), coin_selector.clone()) +// .map_or_else(|| coin_selector.select_until_finished(), |cs| cs.finish())? +// } +// _ => coin_selector.select_until_finished()?, +// }; +// let (_, selection_meta) = selection.best_strategy(); +// +// // get the selected utxos +// let selected_txos = selection.apply_selection(&candidates).collect::>(); +// +// if let Some(drain_value) = selection_meta.drain_value { +// change_output.value = drain_value; +// // if the selection tells us to use change and the change value is sufficient, we add it as an output +// outputs.push(change_output) +// } +// +// let mut transaction = Transaction { +// version: 0x02, +// /* TODO: somehow ask to the chainoracle for the last block? +// lock_time: keychain_tracker +// .chain() +// .latest_checkpoint() +// .and_then(|block_id| LockTime::from_height(block_id.height).ok()) +// .unwrap_or(LockTime::ZERO) +// .into(), +// */ +// lock_time: LockTime::ZERO.into(), +// input: selected_txos +// .iter() +// .map(|(_, utxo)| TxIn { +// previous_output: utxo.outpoint, +// sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, +// ..Default::default() +// }) +// .collect(), +// output: outputs, +// }; +// +// let prevouts = selected_txos +// .iter() +// .map(|(_, utxo)| utxo.txout.clone()) +// .collect::>(); +// let sighash_prevouts = Prevouts::All(&prevouts); +// +// // first, set tx values for the plan so that we don't change them while signing +// for (i, (plan, _)) in selected_txos.iter().enumerate() { +// if let Some(sequence) = plan.required_sequence() { +// transaction.input[i].sequence = sequence +// } +// } +// +// // create a short lived transaction +// let _sighash_tx = transaction.clone(); +// let mut sighash_cache = SighashCache::new(&_sighash_tx); +// +// for (i, (plan, _)) in selected_txos.iter().enumerate() { +// let requirements = plan.requirements(); +// let mut auth_data = bdk_tmp_plan::SatisfactionMaterial::default(); +// assert!( +// !requirements.requires_hash_preimages(), +// "can't have hash pre-images since we didn't provide any." +// ); +// assert!( +// requirements.signatures.sign_with_keymap( +// i, +// keymap, +// &sighash_prevouts, +// None, +// None, +// &mut sighash_cache, +// &mut auth_data, +// &Secp256k1::default(), +// )?, +// "we should have signed with this input." +// ); +// +// match plan.try_complete(&auth_data) { +// bdk_tmp_plan::PlanState::Complete { +// final_script_sig, +// final_script_witness, +// } => { +// if let Some(witness) = final_script_witness { +// transaction.input[i].witness = witness; +// } +// +// if let Some(script_sig) = final_script_sig { +// transaction.input[i].script_sig = script_sig; +// } +// } +// bdk_tmp_plan::PlanState::Incomplete(_) => { +// return Err(anyhow!( +// "we weren't able to complete the plan with our keys." +// )); +// } +// } +// } +// +// let change_info = if selection_meta.drain_value.is_some() { +// Some((additions, (internal_keychain, change_index))) +// } else { +// None +// }; +// +// Ok((transaction, change_info)) +//} pub fn handle_commands( command: Commands, @@ -558,69 +552,69 @@ where match command { // TODO: Make these functions return stuffs Commands::Address { addr_cmd } => run_address_cmd(indexed_tx_graph, addr_cmd, network), - Commands::Balance => { - run_balance_cmd(indexed_tx_graph, chain_oracle); - Ok(()) - } - Commands::TxOut { txout_cmd } => { - run_txo_cmd(txout_cmd, indexed_tx_graph, chain_oracle, network); - Ok(()) - } - Commands::Send { - value, - address, - coin_select, - } => { - let (transaction, change_index) = { - // take mutable ref to construct tx -- it is only open for a short time while building it. - let indexed_tx_graph = &mut *indexed_tx_graph.lock().unwrap(); - let chain_oracle = & *chain_oracle.lock().unwrap(); - let (transaction, change_info) = - create_tx(value, address, coin_select, indexed_tx_graph, chain_oracle, keymap)?; - - if let Some((change_derivation_changes, (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 store = &mut *store.lock().unwrap(); - //store.append_changeset(&change_derivation_changes.into())?; - - // 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. - indexed_tx_graph - .index - .mark_used(&change_keychain, index); - (transaction, Some((change_keychain, index))) - } else { - (transaction, None) - } - }; - - match (broadcast)(&transaction) { - Ok(_) => { - println!("Broadcasted Tx : {}", transaction.txid()); - let mut indexed_tx_graph = indexed_tx_graph.lock().unwrap(); - let changeset = indexed_tx_graph.insert_tx(&transaction, ObservedIn::Mempool(23 /* FIXME: time should be right now */)); - /* - let store = &mut *store.lock().unwrap(); - // 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. - store.append_changeset(&changeset)?; - */ - Ok(()) - } - Err(e) => { - let indexed_tx_graph = &mut *indexed_tx_graph.lock().unwrap(); - if let Some((keychain, index)) = change_index { - // We failed to broadcast, so allow our change address to be used in the future - indexed_tx_graph.index.unmark_used(&keychain, index); - } - Err(e) - } - } - } +// Commands::Balance => { +// run_balance_cmd(indexed_tx_graph, chain_oracle); +// Ok(()) +// } +// Commands::TxOut { txout_cmd } => { +// run_txo_cmd(txout_cmd, indexed_tx_graph, chain_oracle, network); +// Ok(()) +// } +// Commands::Send { +// value, +// address, +// coin_select, +// } => { +// let (transaction, change_index) = { +// // take mutable ref to construct tx -- it is only open for a short time while building it. +// let indexed_tx_graph = &mut *indexed_tx_graph.lock().unwrap(); +// let chain_oracle = & *chain_oracle.lock().unwrap(); +// let (transaction, change_info) = +// create_tx(value, address, coin_select, indexed_tx_graph, chain_oracle, keymap)?; +// +// if let Some((change_derivation_changes, (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 store = &mut *store.lock().unwrap(); +// //store.append_changeset(&change_derivation_changes.into())?; +// +// // 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. +// indexed_tx_graph +// .index +// .mark_used(&change_keychain, index); +// (transaction, Some((change_keychain, index))) +// } else { +// (transaction, None) +// } +// }; +// +// match (broadcast)(&transaction) { +// Ok(_) => { +// println!("Broadcasted Tx : {}", transaction.txid()); +// let mut indexed_tx_graph = indexed_tx_graph.lock().unwrap(); +// let changeset = indexed_tx_graph.insert_tx(&transaction, ObservedAs::Mempool(23 /* FIXME: time should be right now */)); +// /* +// let store = &mut *store.lock().unwrap(); +// // 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. +// store.append_changeset(&changeset)?; +// */ +// Ok(()) +// } +// Err(e) => { +// let indexed_tx_graph = &mut *indexed_tx_graph.lock().unwrap(); +// if let Some((keychain, index)) = change_index { +// // We failed to broadcast, so allow our change address to be used in the future +// indexed_tx_graph.index.unmark_used(&keychain, index); +// } +// Err(e) +// } +// } +// } Commands::ChainSpecific(_) => { todo!("example code is meant to handle this!") } @@ -636,9 +630,9 @@ pub fn init() -> anyhow::Result<( Mutex>>, )> where - A: BlockAnchor, + A: Anchor, { - let args = Args::::parse(); + let args = Args::parse(); //Args::::parse(); let secp = Secp256k1::default(); let (descriptor, mut keymap) = Descriptor::::parse_descriptor(&secp, &args.descriptor)?; @@ -681,29 +675,29 @@ where )) } -pub fn planned_utxos<'a, AK: bdk_tmp_plan::CanDerive + Clone, A: BlockAnchor, E>( - indexed_tx_graph: &'a IndexedTxGraph>, - chain_oracle: &'a impl ChainOracle, - assets: &'a bdk_tmp_plan::Assets, -) -> impl Iterator, FullTxOut>)> + 'a -where ObservedIn: ChainPosition { - indexed_tx_graph.index.txouts() - .filter_map(move |(keychain, derivation_index, full_txout)| { - /* - Some(( - bdk_tmp_plan::plan_satisfaction( - &indexed_tx_graph - .index - .keychains() - .get(keychain) - .expect("must exist since we have a utxo for it") - .at_derivation_index(*derivation_index), - assets, - )?, - full_txout.clone(), - )) - */ - todo!() - }, - ) -} +//pub fn planned_utxos<'a, AK: bdk_tmp_plan::CanDerive + Clone, A: Anchor, E>( +// indexed_tx_graph: &'a IndexedTxGraph>, +// chain_oracle: &'a impl ChainOracle, +// assets: &'a bdk_tmp_plan::Assets, +//) -> impl Iterator, FullTxOut>)> + 'a +//where ObservedAs: ChainPosition { +// indexed_tx_graph.index.txouts() +// .filter_map(move |(keychain, derivation_index, full_txout)| { +// /* +// Some(( +// bdk_tmp_plan::plan_satisfaction( +// &indexed_tx_graph +// .index +// .keychains() +// .get(keychain) +// .expect("must exist since we have a utxo for it") +// .at_derivation_index(*derivation_index), +// assets, +// )?, +// full_txout.clone(), +// )) +// */ +// todo!() +// }, +// ) +//} diff --git a/example-crates/keychain_tracker_electrum/Cargo.toml b/example-crates/keychain_tracker_electrum/Cargo.toml index 10226b43a..44093d136 100644 --- a/example-crates/keychain_tracker_electrum/Cargo.toml +++ b/example-crates/keychain_tracker_electrum/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" [dependencies] bdk_chain = { path = "../../crates/chain", features = ["serde"] } bdk_electrum = { path = "../../crates/electrum" } -keychain_tracker_example_cli = { path = "../keychain_tracker_example_cli"} +indexed_tx_graph_example_cli = { path = "../indexed_tx_graph_example_cli"} diff --git a/example-crates/keychain_tracker_electrum/src/main.rs b/example-crates/keychain_tracker_electrum/src/main.rs index c8b9e0684..6de5e850f 100644 --- a/example-crates/keychain_tracker_electrum/src/main.rs +++ b/example-crates/keychain_tracker_electrum/src/main.rs @@ -4,12 +4,14 @@ use bdk_electrum::{ electrum_client::{self, ElectrumApi}, ElectrumExt, ElectrumUpdate, }; -use keychain_tracker_example_cli::{ +use indexed_tx_graph_example_cli::{ self as cli, anyhow::{self, Context}, clap::{self, Parser, Subcommand}, }; use std::{collections::BTreeMap, fmt::Debug, io, io::Write}; +use std::sync::Mutex; +use bdk_chain::local_chain::LocalChain; #[derive(Subcommand, Debug, Clone)] enum ElectrumCommands { @@ -48,7 +50,7 @@ pub struct ScanOptions { } fn main() -> anyhow::Result<()> { - let (args, keymap, tracker, db) = cli::init::()?; + let (args, keymap, indexed_tx_graph/*, db*/) = cli::init::()?; let electrum_url = match args.network { Network::Bitcoin => "ssl://electrum.blockstream.info:50002", @@ -61,6 +63,8 @@ fn main() -> anyhow::Result<()> { .build(); let client = electrum_client::Client::from_config(electrum_url, config)?; + + let local_chain = Mutex::new(LocalChain::default()); let electrum_cmd = match args.command.clone() { cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd, @@ -71,8 +75,9 @@ fn main() -> anyhow::Result<()> { let _txid = client.transaction_broadcast(transaction)?; Ok(()) }, - &tracker, - &db, + &indexed_tx_graph, +// &db, + &local_chain, args.network, &keymap, ) @@ -84,12 +89,12 @@ fn main() -> anyhow::Result<()> { stop_gap, scan_options: scan_option, } => { - let (spk_iterators, local_chain) = { - // Get a short lock on the tracker to get the spks iterators + let (keychain_spks, tx_graph) = { + // Get a short lock on the indexed_tx_graph to get the spks iterators // and local chain state - let tracker = &*tracker.lock().unwrap(); - let spk_iterators = tracker - .txout_index + let locked_indexed_tx_graph = &*indexed_tx_graph.lock().unwrap(); + let spk_iterators = locked_indexed_tx_graph + .index .spks_of_all_keychains() .into_iter() .map(|(keychain, iter)| { @@ -106,14 +111,15 @@ fn main() -> anyhow::Result<()> { (keychain, spk_iter) }) .collect::>(); - let local_chain = tracker.chain().checkpoints().clone(); - (spk_iterators, local_chain) + let tx_graph = locked_indexed_tx_graph.graph().clone(); + (spk_iterators, tx_graph) }; + let unlocked_local_chain = *local_chain.lock().unwrap(); // we scan the spks **without** a lock on the tracker client.scan( - &local_chain, - spk_iterators, + &unlocked_local_chain, + keychain_spks, core::iter::empty(), core::iter::empty(), stop_gap, @@ -128,7 +134,7 @@ fn main() -> anyhow::Result<()> { scan_options, } => { // Get a short lock on the tracker to get the spks we're interested in - let tracker = tracker.lock().unwrap(); + let tracker = indexed_tx_graph.lock().unwrap(); if !(all_spks || unused_spks || utxos || unconfirmed) { unused_spks = true; @@ -223,7 +229,7 @@ fn main() -> anyhow::Result<()> { } }; - let missing_txids = response.missing_full_txs(&*tracker.lock().unwrap()); + let missing_txids = response.missing_full_txs(&*indexed_tx_graph.lock().unwrap()); // fetch the missing full transactions **without** a lock on the tracker let new_txs = client @@ -232,13 +238,13 @@ fn main() -> anyhow::Result<()> { { // Get a final short lock to apply the changes - let mut tracker = tracker.lock().unwrap(); + let mut locked_index_tx_graph = indexed_tx_graph.lock().unwrap(); let changeset = { - let scan = response.into_keychain_scan(new_txs, &*tracker)?; - tracker.determine_changeset(&scan)? + let scan = response.into_keychain_scan(new_txs, &*locked_index_tx_graph)?; + locked_index_tx_graph.determine_changeset(&scan)? }; - db.lock().unwrap().append_changeset(&changeset)?; - tracker.apply_changeset(changeset); +// db.lock().unwrap().append_changeset(&changeset)?; + locked_index_tx_graph.apply_changeset(changeset); }; Ok(()) diff --git a/example-crates/keychain_tracker_esplora/Cargo.toml b/example-crates/keychain_tracker_esplora/Cargo.toml index e0a1e62dc..49948b7e3 100644 --- a/example-crates/keychain_tracker_esplora/Cargo.toml +++ b/example-crates/keychain_tracker_esplora/Cargo.toml @@ -8,4 +8,4 @@ edition = "2021" [dependencies] bdk_chain = { path = "../../crates/chain", features = ["serde", "miniscript"] } bdk_esplora = { path = "../../crates/esplora" } -keychain_tracker_example_cli = { path = "../keychain_tracker_example_cli" } +indexed_tx_graph_example_cli = { path = "../indexed_tx_graph_example_cli" } diff --git a/example-crates/keychain_tracker_esplora/src/main.rs b/example-crates/keychain_tracker_esplora/src/main.rs index cae5e9601..547e6cfee 100644 --- a/example-crates/keychain_tracker_esplora/src/main.rs +++ b/example-crates/keychain_tracker_esplora/src/main.rs @@ -5,7 +5,7 @@ use bdk_esplora::EsploraExt; use std::io::{self, Write}; -use keychain_tracker_example_cli::{ +use indexed_tx_graph_example_cli::{ self as cli, anyhow::{self, Context}, clap::{self, Parser, Subcommand}, @@ -49,7 +49,8 @@ pub struct ScanOptions { } fn main() -> anyhow::Result<()> { - let (args, keymap, keychain_tracker, db) = cli::init::()?; + let (args, keymap, indexed_tx_graph/*, db*/) = cli::init::()?; + let esplora_url = match args.network { Network::Bitcoin => "https://mempool.space/api", Network::Testnet => "https://mempool.space/testnet/api", @@ -58,6 +59,8 @@ fn main() -> anyhow::Result<()> { }; let client = esplora_client::Builder::new(esplora_url).build_blocking()?; + + let chain_oracle = client; let esplora_cmd = match args.command { cli::Commands::ChainSpecific(esplora_cmd) => esplora_cmd, @@ -65,10 +68,11 @@ fn main() -> anyhow::Result<()> { return cli::handle_commands( general_command, |transaction| Ok(client.broadcast(transaction)?), - &keychain_tracker, - &db, - args.network, - &keymap, + &indexed_tx_graph, + &chain_oracle, +// &db, +// args.network, +// &keymap, ) } }; @@ -81,7 +85,7 @@ fn main() -> anyhow::Result<()> { let (spk_iterators, local_chain) = { // Get a short lock on the tracker to get the spks iterators // and local chain state - let tracker = &*keychain_tracker.lock().unwrap(); + let tracker = &*indexed_tx_graph.lock().unwrap(); let spk_iterators = tracker .txout_index .spks_of_all_keychains() @@ -122,7 +126,7 @@ fn main() -> anyhow::Result<()> { { // we take a short lock to apply results to tracker and db - let tracker = &mut *keychain_tracker.lock().unwrap(); + let tracker = &mut *indexed_tx_graph.lock().unwrap(); let db = &mut *db.lock().unwrap(); let changeset = tracker.apply_update(wallet_scan)?; db.append_changeset(&changeset)?; @@ -136,7 +140,7 @@ fn main() -> anyhow::Result<()> { scan_options, } => { // Get a short lock on the tracker to get the spks we're interested in - let tracker = keychain_tracker.lock().unwrap(); + let tracker = indexed_tx_graph.lock().unwrap(); if !(all_spks || unused_spks || utxos || unconfirmed) { unused_spks = true; @@ -229,7 +233,7 @@ fn main() -> anyhow::Result<()> { { // we take a short lock to apply the results to the tracker and db - let tracker = &mut *keychain_tracker.lock().unwrap(); + let tracker = &mut *indexed_tx_graph.lock().unwrap(); let changeset = tracker.apply_update(scan.into())?; let db = &mut *db.lock().unwrap(); db.append_changeset(&changeset)?;