[examples_redesign] Introduce example_cli package
				
					
				
			This is the equivalent of `keychain_tracker_example_cli` that works with the redesigned structures.
This commit is contained in:
		
							parent
							
								
									2e3cee4bd0
								
							
						
					
					
						commit
						f55974a64b
					
				| @ -4,6 +4,7 @@ members = [ | ||||
|     "crates/chain", | ||||
|     "crates/file_store", | ||||
|     "crates/electrum", | ||||
|     "example-crates/example_cli", | ||||
|     "example-crates/keychain_tracker_electrum", | ||||
|     "example-crates/keychain_tracker_esplora", | ||||
|     "example-crates/keychain_tracker_example_cli", | ||||
|  | ||||
| @ -16,6 +16,13 @@ pub enum ObservedAs<A> { | ||||
|     Unconfirmed(u64), | ||||
| } | ||||
| 
 | ||||
| impl<A> ObservedAs<A> { | ||||
|     /// Returns whether [`ObservedAs`] is confirmed or not.
 | ||||
|     pub fn is_confirmed(&self) -> bool { | ||||
|         matches!(self, Self::Confirmed(_)) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<A: Clone> ObservedAs<&A> { | ||||
|     pub fn cloned(self) -> ObservedAs<A> { | ||||
|         match self { | ||||
|  | ||||
| @ -19,4 +19,7 @@ pub trait ChainOracle { | ||||
|         block: BlockId, | ||||
|         chain_tip: BlockId, | ||||
|     ) -> Result<Option<bool>, Self::Error>; | ||||
| 
 | ||||
|     /// Get the best chain's chain tip.
 | ||||
|     fn get_chain_tip(&self) -> Result<Option<BlockId>, Self::Error>; | ||||
| } | ||||
|  | ||||
| @ -203,6 +203,15 @@ impl<A: Anchor, IA: Append> Append for IndexedAdditions<A, IA> { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<A, IA: Default> From<Additions<A>> for IndexedAdditions<A, IA> { | ||||
|     fn from(graph_additions: Additions<A>) -> Self { | ||||
|         Self { | ||||
|             graph_additions, | ||||
|             ..Default::default() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Represents a structure that can index transaction data.
 | ||||
| pub trait Indexer { | ||||
|     /// The resultant "additions" when new transaction data is indexed.
 | ||||
|  | ||||
| @ -34,6 +34,10 @@ impl ChainOracle for LocalChain { | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     fn get_chain_tip(&self) -> Result<Option<BlockId>, Self::Error> { | ||||
|         Ok(self.tip()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl AsRef<BTreeMap<u32, BlockHash>> for LocalChain { | ||||
|  | ||||
| @ -130,7 +130,7 @@ impl ElectrumExt for Client { | ||||
|                     let mut scanned_spk_iter = scanned_spks | ||||
|                         .iter() | ||||
|                         .map(|(i, (spk, _))| (i.clone(), spk.clone())); | ||||
|                     match populate_with_spks::<K, _, _>( | ||||
|                     match populate_with_spks::<_, _>( | ||||
|                         self, | ||||
|                         &mut update, | ||||
|                         &mut scanned_spk_iter, | ||||
| @ -143,7 +143,7 @@ impl ElectrumExt for Client { | ||||
|                     }; | ||||
|                 } | ||||
|                 for (keychain, keychain_spks) in &mut request_spks { | ||||
|                     match populate_with_spks::<K, u32, _>( | ||||
|                     match populate_with_spks::<u32, _>( | ||||
|                         self, | ||||
|                         &mut update, | ||||
|                         keychain_spks, | ||||
| @ -529,7 +529,7 @@ fn populate_with_txids( | ||||
| 
 | ||||
| /// Populate an update [`SparseChain`] with transactions (and associated block positions) from
 | ||||
| /// the transaction history of the provided `spk`s.
 | ||||
| fn populate_with_spks<K, I, S>( | ||||
| fn populate_with_spks<I, S>( | ||||
|     client: &Client, | ||||
|     update: &mut SparseChain, | ||||
|     spks: &mut S, | ||||
| @ -537,7 +537,6 @@ fn populate_with_spks<K, I, S>( | ||||
|     batch_size: usize, | ||||
| ) -> Result<BTreeMap<I, (Script, bool)>, InternalError> | ||||
| where | ||||
|     K: Ord + Clone, | ||||
|     I: Ord + Clone, | ||||
|     S: Iterator<Item = (I, Script)>, | ||||
| { | ||||
|  | ||||
							
								
								
									
										17
									
								
								example-crates/example_cli/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								example-crates/example_cli/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| [package] | ||||
| name = "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" } | ||||
							
								
								
									
										775
									
								
								example-crates/example_cli/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										775
									
								
								example-crates/example_cli/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,775 @@ | ||||
| pub use anyhow; | ||||
| use anyhow::Context; | ||||
| use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue}; | ||||
| use bdk_file_store::Store; | ||||
| use serde::{de::DeserializeOwned, Serialize}; | ||||
| use std::{cmp::Reverse, collections::HashMap, path::PathBuf, sync::Mutex, time::Duration}; | ||||
| 
 | ||||
| use bdk_chain::{ | ||||
|     bitcoin::{ | ||||
|         psbt::Prevouts, | ||||
|         secp256k1::{self, Secp256k1}, | ||||
|         util::sighash::SighashCache, | ||||
|         Address, LockTime, Network, Script, Sequence, Transaction, TxIn, TxOut, | ||||
|     }, | ||||
|     indexed_tx_graph::{IndexedAdditions, IndexedTxGraph}, | ||||
|     keychain::{DerivationAdditions, KeychainTxOutIndex}, | ||||
|     miniscript::{ | ||||
|         descriptor::{DescriptorSecretKey, KeyMap}, | ||||
|         Descriptor, DescriptorPublicKey, | ||||
|     }, | ||||
|     Anchor, Append, ChainOracle, DescriptorExt, FullTxOut, ObservedAs, Persist, PersistBackend, | ||||
| }; | ||||
| pub use bdk_file_store; | ||||
| pub use clap; | ||||
| 
 | ||||
| use clap::{Parser, Subcommand}; | ||||
| 
 | ||||
| pub type KeychainTxGraph<A> = IndexedTxGraph<A, KeychainTxOutIndex<Keychain>>; | ||||
| pub type Database<'m, A, X> = Persist<Store<'m, ChangeSet<A, X>>, ChangeSet<A, X>>; | ||||
| 
 | ||||
| #[derive(Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)] | ||||
| #[serde(bound(
 | ||||
|     deserialize = "A: Ord + serde::Deserialize<'de>, X: serde::Deserialize<'de>", | ||||
|     serialize = "A: Ord + serde::Serialize, X: serde::Serialize", | ||||
| ))] | ||||
| pub struct ChangeSet<A, X> { | ||||
|     pub indexed_additions: IndexedAdditions<A, DerivationAdditions<Keychain>>, | ||||
|     pub extension: X, | ||||
| } | ||||
| 
 | ||||
| impl<A, X: Default> Default for ChangeSet<A, X> { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             indexed_additions: Default::default(), | ||||
|             extension: Default::default(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<A: Anchor, X: Append> Append for ChangeSet<A, X> { | ||||
|     fn append(&mut self, other: Self) { | ||||
|         Append::append(&mut self.indexed_additions, other.indexed_additions); | ||||
|         Append::append(&mut self.extension, other.extension) | ||||
|     } | ||||
| 
 | ||||
|     fn is_empty(&self) -> bool { | ||||
|         todo!() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Parser)] | ||||
| #[clap(author, version, about, long_about = None)] | ||||
| #[clap(propagate_version = true)] | ||||
| pub struct Args<C: clap::Subcommand> { | ||||
|     #[clap(env = "DESCRIPTOR")] | ||||
|     pub descriptor: String, | ||||
|     #[clap(env = "CHANGE_DESCRIPTOR")] | ||||
|     pub change_descriptor: Option<String>, | ||||
| 
 | ||||
|     #[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<C>, | ||||
| } | ||||
| 
 | ||||
| #[allow(clippy::almost_swapped)] | ||||
| #[derive(Subcommand, Debug, Clone)] | ||||
| pub enum Commands<C: clap::Subcommand> { | ||||
|     #[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<Self, Self::Err> { | ||||
|         use CoinSelectionAlgo::*; | ||||
|         Ok(match s { | ||||
|             "largest-first" => LargestFirst, | ||||
|             "smallest-first" => SmallestFirst, | ||||
|             "oldest-first" => OldestFirst, | ||||
|             "newest-first" => NewestFirst, | ||||
|             "bnb" => BranchAndBound, | ||||
|             unknown => { | ||||
|                 return Err(anyhow::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", | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[allow(clippy::almost_swapped)] | ||||
| #[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"), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub fn run_address_cmd<A, X>( | ||||
|     graph: &mut KeychainTxGraph<A>, | ||||
|     db: &Mutex<Database<'_, A, X>>, | ||||
|     network: Network, | ||||
|     cmd: AddressCmd, | ||||
| ) -> anyhow::Result<()> | ||||
| where | ||||
|     ChangeSet<A, X>: Default + Append + DeserializeOwned + Serialize, | ||||
| { | ||||
|     let process_spk = |spk_i: u32, spk: &Script, index_additions: DerivationAdditions<Keychain>| { | ||||
|         if !index_additions.is_empty() { | ||||
|             let db = &mut *db.lock().unwrap(); | ||||
|             db.stage(ChangeSet { | ||||
|                 indexed_additions: IndexedAdditions { | ||||
|                     index_additions, | ||||
|                     ..Default::default() | ||||
|                 }, | ||||
|                 ..Default::default() | ||||
|             }); | ||||
|             db.commit()?; | ||||
|         } | ||||
|         let addr = Address::from_script(spk, network).context("failed to derive address")?; | ||||
|         println!("[address @ {}] {}", spk_i, addr); | ||||
|         Ok(()) | ||||
|     }; | ||||
| 
 | ||||
|     let index = &mut graph.index; | ||||
| 
 | ||||
|     match cmd { | ||||
|         AddressCmd::Next => { | ||||
|             let ((spk_i, spk), index_additions) = index.next_unused_spk(&Keychain::External); | ||||
|             process_spk(spk_i, spk, index_additions) | ||||
|         } | ||||
|         AddressCmd::New => { | ||||
|             let ((spk_i, spk), index_additions) = index.reveal_next_spk(&Keychain::External); | ||||
|             process_spk(spk_i, spk, index_additions) | ||||
|         } | ||||
|         AddressCmd::Index => { | ||||
|             for (keychain, derivation_index) in index.last_revealed_indices() { | ||||
|                 println!("{:?}: {}", keychain, derivation_index); | ||||
|             } | ||||
|             Ok(()) | ||||
|         } | ||||
|         AddressCmd::List { change } => { | ||||
|             let target_keychain = match change { | ||||
|                 true => Keychain::Internal, | ||||
|                 false => Keychain::External, | ||||
|             }; | ||||
|             for (spk_i, spk) in index.revealed_spks_of_keychain(&target_keychain) { | ||||
|                 let address = Address::from_script(spk, network) | ||||
|                     .expect("should always be able to derive address"); | ||||
|                 println!( | ||||
|                     "{:?} {} used:{}", | ||||
|                     spk_i, | ||||
|                     address, | ||||
|                     index.is_used(&(target_keychain, spk_i)) | ||||
|                 ); | ||||
|             } | ||||
|             Ok(()) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub fn run_balance_cmd<A: Anchor, O: ChainOracle>( | ||||
|     graph: &KeychainTxGraph<A>, | ||||
|     chain: &O, | ||||
| ) -> Result<(), O::Error> { | ||||
|     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; | ||||
| 
 | ||||
|     println!("[confirmed]"); | ||||
|     println!("  total     = {}sats", confirmed_total); | ||||
|     println!("  spendable = {}sats", balance.confirmed); | ||||
|     println!("  immature  = {}sats", balance.immature); | ||||
| 
 | ||||
|     println!("[unconfirmed]"); | ||||
|     println!("  total     = {}sats", unconfirmed_total,); | ||||
|     println!("  trusted   = {}sats", balance.trusted_pending); | ||||
|     println!("  untrusted = {}sats", balance.untrusted_pending); | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| pub fn run_txo_cmd<A: Anchor, O: ChainOracle>( | ||||
|     graph: &KeychainTxGraph<A>, | ||||
|     chain: &O, | ||||
|     network: Network, | ||||
|     cmd: TxOutCmd, | ||||
| ) -> anyhow::Result<()> | ||||
| where | ||||
|     O::Error: std::error::Error + Send + Sync + 'static, | ||||
| { | ||||
|     let chain_tip = chain.get_chain_tip()?.unwrap_or_default(); | ||||
|     let outpoints = graph.index.outpoints().iter().cloned(); | ||||
| 
 | ||||
|     match cmd { | ||||
|         TxOutCmd::List { | ||||
|             spent, | ||||
|             unspent, | ||||
|             confirmed, | ||||
|             unconfirmed, | ||||
|         } => { | ||||
|             let txouts = graph | ||||
|                 .graph() | ||||
|                 .try_filter_chain_txouts(chain, chain_tip, outpoints) | ||||
|                 .filter(|r| match r { | ||||
|                     Ok((_, full_txo)) => match (spent, unspent) { | ||||
|                         (true, false) => full_txo.spent_by.is_some(), | ||||
|                         (false, true) => full_txo.spent_by.is_none(), | ||||
|                         _ => true, | ||||
|                     }, | ||||
|                     // always keep errored items
 | ||||
|                     Err(_) => true, | ||||
|                 }) | ||||
|                 .filter(|r| match r { | ||||
|                     Ok((_, full_txo)) => match (confirmed, unconfirmed) { | ||||
|                         (true, false) => full_txo.chain_position.is_confirmed(), | ||||
|                         (false, true) => !full_txo.chain_position.is_confirmed(), | ||||
|                         _ => true, | ||||
|                     }, | ||||
|                     // always keep errored items
 | ||||
|                     Err(_) => true, | ||||
|                 }) | ||||
|                 .collect::<Result<Vec<_>, _>>()?; | ||||
| 
 | ||||
|             for (spk_i, full_txo) in txouts { | ||||
|                 let addr = Address::from_script(&full_txo.txout.script_pubkey, network)?; | ||||
|                 println!( | ||||
|                     "{:?} {} {} {} spent:{:?}", | ||||
|                     spk_i, full_txo.txout.value, full_txo.outpoint, addr, full_txo.spent_by | ||||
|                 ) | ||||
|             } | ||||
|             Ok(()) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[allow(clippy::too_many_arguments)] | ||||
| pub fn run_send_cmd<A: Anchor, O: ChainOracle, X>( | ||||
|     graph: &Mutex<KeychainTxGraph<A>>, | ||||
|     db: &Mutex<Database<'_, A, X>>, | ||||
|     chain: &O, | ||||
|     keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>, | ||||
|     cs_algorithm: CoinSelectionAlgo, | ||||
|     address: Address, | ||||
|     value: u64, | ||||
|     broadcast: impl FnOnce(&Transaction) -> anyhow::Result<()>, | ||||
| ) -> anyhow::Result<()> | ||||
| where | ||||
|     O::Error: std::error::Error + Send + Sync + 'static, | ||||
|     ChangeSet<A, X>: Default + Append + DeserializeOwned + Serialize, | ||||
| { | ||||
|     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_additions, (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.
 | ||||
|             db.lock().unwrap().stage(ChangeSet { | ||||
|                 indexed_additions: IndexedAdditions { | ||||
|                     index_additions, | ||||
|                     ..Default::default() | ||||
|                 }, | ||||
|                 ..Default::default() | ||||
|             }); | ||||
| 
 | ||||
|             // 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 indexed_additions = 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(ChangeSet { | ||||
|                 indexed_additions, | ||||
|                 ..Default::default() | ||||
|             }); | ||||
|             Ok(()) | ||||
|         } | ||||
|         Err(e) => { | ||||
|             if let Some((keychain, index)) = change_index { | ||||
|                 // We failed to broadcast, so allow our change address to be used in the future
 | ||||
|                 graph.lock().unwrap().index.unmark_used(&keychain, index); | ||||
|             } | ||||
|             Err(e) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[allow(clippy::type_complexity)] | ||||
| pub fn create_tx<A: Anchor, O: ChainOracle>( | ||||
|     graph: &mut KeychainTxGraph<A>, | ||||
|     chain: &O, | ||||
|     keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>, | ||||
|     cs_algorithm: CoinSelectionAlgo, | ||||
|     address: Address, | ||||
|     value: u64, | ||||
| ) -> anyhow::Result<( | ||||
|     Transaction, | ||||
|     Option<(DerivationAdditions<Keychain>, (Keychain, u32))>, | ||||
| )> | ||||
| where | ||||
|     O::Error: std::error::Error + Send + Sync + 'static, | ||||
| { | ||||
|     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(graph, chain, &assets)?; | ||||
| 
 | ||||
|     // apply coin selection algorithm
 | ||||
|     match cs_algorithm { | ||||
|         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 graph.index.keychains().get(&Keychain::Internal).is_some() { | ||||
|         Keychain::Internal | ||||
|     } else { | ||||
|         Keychain::External | ||||
|     }; | ||||
| 
 | ||||
|     let ((change_index, change_script), change_additions) = | ||||
|         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( | ||||
|         &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: 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 cs_algorithm { | ||||
|         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::<Vec<_>>(); | ||||
| 
 | ||||
|     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, | ||||
|         lock_time: chain | ||||
|             .get_chain_tip()? | ||||
|             .and_then(|block_id| LockTime::from_height(block_id.height).ok()) | ||||
|             .unwrap_or(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::<Vec<_>>(); | ||||
|     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::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)) | ||||
| } | ||||
| 
 | ||||
| #[allow(clippy::type_complexity)] | ||||
| pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDerive>( | ||||
|     graph: &KeychainTxGraph<A>, | ||||
|     chain: &O, | ||||
|     assets: &bdk_tmp_plan::Assets<K>, | ||||
| ) -> Result<Vec<(bdk_tmp_plan::Plan<K>, FullTxOut<ObservedAs<A>>)>, O::Error> { | ||||
|     let chain_tip = chain.get_chain_tip()?.unwrap_or_default(); | ||||
|     let outpoints = graph.index.outpoints().iter().cloned(); | ||||
|     graph | ||||
|         .graph() | ||||
|         .try_filter_chain_unspents(chain, chain_tip, outpoints) | ||||
|         .filter_map( | ||||
|             #[allow(clippy::type_complexity)] | ||||
|             |r| -> Option<Result<(bdk_tmp_plan::Plan<K>, FullTxOut<ObservedAs<A>>), _>> { | ||||
|                 let (k, i, full_txo) = match r { | ||||
|                     Err(err) => return Some(Err(err)), | ||||
|                     Ok(((k, i), full_txo)) => (k, i, full_txo), | ||||
|                 }; | ||||
|                 let desc = graph | ||||
|                     .index | ||||
|                     .keychains() | ||||
|                     .get(&k) | ||||
|                     .expect("keychain must exist") | ||||
|                     .at_derivation_index(i); | ||||
|                 let plan = bdk_tmp_plan::plan_satisfaction(&desc, assets)?; | ||||
|                 Some(Ok((plan, full_txo))) | ||||
|             }, | ||||
|         ) | ||||
|         .collect() | ||||
| } | ||||
| 
 | ||||
| pub fn handle_commands<C: clap::Subcommand, A: Anchor, O: ChainOracle, X>( | ||||
|     graph: &Mutex<KeychainTxGraph<A>>, | ||||
|     db: &Mutex<Database<A, X>>, | ||||
|     chain: &O, | ||||
|     keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>, | ||||
|     network: Network, | ||||
|     broadcast: impl FnOnce(&Transaction) -> anyhow::Result<()>, | ||||
|     cmd: Commands<C>, | ||||
| ) -> anyhow::Result<()> | ||||
| where | ||||
|     O::Error: std::error::Error + Send + Sync + 'static, | ||||
|     ChangeSet<A, X>: Default + Append + DeserializeOwned + Serialize, | ||||
| { | ||||
|     match cmd { | ||||
|         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) | ||||
|         } | ||||
|         Commands::Balance => { | ||||
|             let graph = &*graph.lock().unwrap(); | ||||
|             run_balance_cmd(graph, chain).map_err(anyhow::Error::from) | ||||
|         } | ||||
|         Commands::TxOut { txout_cmd } => { | ||||
|             let graph = &*graph.lock().unwrap(); | ||||
|             run_txo_cmd(graph, chain, network, txout_cmd) | ||||
|         } | ||||
|         Commands::Send { | ||||
|             value, | ||||
|             address, | ||||
|             coin_select, | ||||
|         } => run_send_cmd( | ||||
|             graph, | ||||
|             db, | ||||
|             chain, | ||||
|             keymap, | ||||
|             coin_select, | ||||
|             address, | ||||
|             value, | ||||
|             broadcast, | ||||
|         ), | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub fn prepare_index<C: clap::Subcommand, SC: secp256k1::Signing>( | ||||
|     args: &Args<C>, | ||||
|     secp: &Secp256k1<SC>, | ||||
| ) -> anyhow::Result<(KeychainTxOutIndex<Keychain>, KeyMap)> { | ||||
|     let mut index = KeychainTxOutIndex::<Keychain>::default(); | ||||
| 
 | ||||
|     let (descriptor, mut keymap) = | ||||
|         Descriptor::<DescriptorPublicKey>::parse_descriptor(secp, &args.descriptor)?; | ||||
|     index.add_keychain(Keychain::External, descriptor); | ||||
| 
 | ||||
|     if let Some((internal_descriptor, internal_keymap)) = args | ||||
|         .change_descriptor | ||||
|         .as_ref() | ||||
|         .map(|desc_str| Descriptor::<DescriptorPublicKey>::parse_descriptor(secp, desc_str)) | ||||
|         .transpose()? | ||||
|     { | ||||
|         keymap.extend(internal_keymap); | ||||
|         index.add_keychain(Keychain::Internal, internal_descriptor); | ||||
|     } | ||||
| 
 | ||||
|     Ok((index, keymap)) | ||||
| } | ||||
| 
 | ||||
| #[allow(clippy::type_complexity)] | ||||
| pub fn init<'m, S: clap::Subcommand, A: Anchor, X>( | ||||
|     db_magic: &'m [u8], | ||||
|     db_default_path: &str, | ||||
| ) -> anyhow::Result<( | ||||
|     Args<S>, | ||||
|     KeyMap, | ||||
|     Mutex<KeychainTxGraph<A>>, | ||||
|     Mutex<Database<'m, A, X>>, | ||||
|     X, | ||||
| )> | ||||
| where | ||||
|     ChangeSet<A, X>: Default + Append + Serialize + DeserializeOwned, | ||||
| { | ||||
|     if std::env::var("BDK_DB_PATH").is_err() { | ||||
|         std::env::set_var("BDK_DB_PATH", db_default_path); | ||||
|     } | ||||
|     let args = Args::<S>::parse(); | ||||
|     let secp = Secp256k1::default(); | ||||
|     let (index, keymap) = prepare_index(&args, &secp)?; | ||||
| 
 | ||||
|     let mut indexed_graph = IndexedTxGraph::<A, KeychainTxOutIndex<Keychain>>::new(index); | ||||
| 
 | ||||
|     let mut db_backend = | ||||
|         match Store::<'m, ChangeSet<A, X>>::new_from_path(db_magic, args.db_path.as_path()) { | ||||
|             Ok(db_backend) => db_backend, | ||||
|             Err(err) => return Err(anyhow::anyhow!("failed to init db backend: {:?}", err)), | ||||
|         }; | ||||
| 
 | ||||
|     let ChangeSet { | ||||
|         indexed_additions, | ||||
|         extension, | ||||
|     } = db_backend.load_from_persistence()?; | ||||
|     indexed_graph.apply_additions(indexed_additions); | ||||
| 
 | ||||
|     Ok(( | ||||
|         args, | ||||
|         keymap, | ||||
|         Mutex::new(indexed_graph), | ||||
|         Mutex::new(Database::new(db_backend)), | ||||
|         extension, | ||||
|     )) | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user