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..58d65468f --- /dev/null +++ b/example-crates/indexed_tx_graph_example_cli/src/lib.rs @@ -0,0 +1,703 @@ +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}, 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}; +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: 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_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_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())?; + + 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 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, + 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, 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!") + } + } +} + +#[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: Anchor, +{ + let args = Args::parse(); //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: 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)?;