From 58a69dc4bb6ae2d09bdec97e56a52b0079c34759 Mon Sep 17 00:00:00 2001 From: thunderbiscuit Date: Mon, 7 Apr 2025 11:31:02 -0400 Subject: [PATCH] exploring bdk-tx for Wallet type --- Cargo.toml | 1 + examples/example_wallet_bdk_tx/Cargo.toml | 11 ++ examples/example_wallet_bdk_tx/src/main.rs | 114 +++++++++++++++++ wallet/Cargo.toml | 1 + wallet/src/wallet/mod.rs | 135 ++++++++++++++++++++- 5 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 examples/example_wallet_bdk_tx/Cargo.toml create mode 100644 examples/example_wallet_bdk_tx/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index fbde1ace..df8eba33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "examples/example_wallet_esplora_blocking", "examples/example_wallet_esplora_async", "examples/example_wallet_rpc", + "examples/example_wallet_bdk_tx", ] [workspace.package] diff --git a/examples/example_wallet_bdk_tx/Cargo.toml b/examples/example_wallet_bdk_tx/Cargo.toml new file mode 100644 index 00000000..6df46e70 --- /dev/null +++ b/examples/example_wallet_bdk_tx/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "example_wallet_bdk_tx" +version = "0.2.0" +edition = "2021" +publish = false + +[dependencies] +bdk_wallet = { path = "../../wallet", features = ["file_store"] } +bdk_esplora = { version = "0.20", features = ["blocking"] } + +anyhow = "1" diff --git a/examples/example_wallet_bdk_tx/src/main.rs b/examples/example_wallet_bdk_tx/src/main.rs new file mode 100644 index 00000000..6705e36a --- /dev/null +++ b/examples/example_wallet_bdk_tx/src/main.rs @@ -0,0 +1,114 @@ +use std::{collections::BTreeSet, io::Write}; + +use bdk_esplora::{esplora_client, EsploraExt}; +use bdk_wallet::bitcoin::FeeRate; +use bdk_wallet::{ + bitcoin::{Amount, Network}, + file_store::Store, + KeychainKind, SignOptions, TransactionParams, Wallet, +}; + +const DB_MAGIC: &str = "bdk_wallet_esplora_example"; +const DB_PATH: &str = "bdk-example-bdk-tx.db"; +const SEND_AMOUNT: Amount = Amount::from_sat(5000); +const STOP_GAP: usize = 5; +const PARALLEL_REQUESTS: usize = 5; + +const NETWORK: Network = Network::Signet; +const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/1'/0/*)"; +const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/1'/1/*)"; +const ESPLORA_URL: &str = "http://signet.bitcoindevkit.net"; + +fn main() -> Result<(), anyhow::Error> { + let mut db = Store::::open_or_create_new(DB_MAGIC.as_bytes(), DB_PATH)?; + + let wallet_opt = Wallet::load() + .descriptor(KeychainKind::External, Some(EXTERNAL_DESC)) + .descriptor(KeychainKind::Internal, Some(INTERNAL_DESC)) + .extract_keys() + .check_network(NETWORK) + .load_wallet(&mut db)?; + let mut wallet = match wallet_opt { + Some(wallet) => wallet, + None => Wallet::create(EXTERNAL_DESC, INTERNAL_DESC) + .network(NETWORK) + .create_wallet(&mut db)?, + }; + + let address = wallet.next_unused_address(KeychainKind::External); + wallet.persist(&mut db)?; + println!( + "Next unused address: ({}) {}", + address.index, address.address + ); + + let balance = wallet.balance(); + println!("Wallet balance before syncing: {}", balance.total()); + + print!("Syncing..."); + let client = esplora_client::Builder::new(ESPLORA_URL).build_blocking(); + + let request = wallet.start_full_scan().inspect({ + let mut stdout = std::io::stdout(); + let mut once = BTreeSet::::new(); + move |keychain, spk_i, _| { + if once.insert(keychain) { + print!("\nScanning keychain [{:?}] ", keychain); + } + print!(" {:<3}", spk_i); + stdout.flush().expect("must flush") + } + }); + + let update = client.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)?; + + wallet.apply_update(update)?; + wallet.persist(&mut db)?; + println!(); + + let balance = wallet.balance(); + println!("Wallet balance after syncing: {}", balance.total()); + + if balance.total() < SEND_AMOUNT { + println!( + "Please send at least {} to the receiving address", + SEND_AMOUNT + ); + std::process::exit(0); + } + + // ---------------------------- + // TRANSACTION BUILDER WORKFLOW + // ---------------------------- + // let mut tx_builder = wallet.build_tx(); + // tx_builder + // .add_recipient(address.script_pubkey(), SEND_AMOUNT) + // .fee_rate(FeeRate::from_sat_per_vb(4).unwrap()); + // let mut psbt = tx_builder.finish()?; + + // ---------------------------- + // USING BDK-TX THROUGH A NEW WALLET METHOD INSTEAD + // ---------------------------- + // let transaction_params = TransactionParams { + // outputs: vec![(address.script_pubkey(), SEND_AMOUNT)], + // target_feerate: FeeRate::from_sat_per_vb(4).unwrap(), + // must_spend: Vec::new(), + // }; + // let mut psbt = wallet.create_complex_transaction(transaction_params).unwrap(); + + let mut psbt = wallet + .create_transaction( + vec![(address.script_pubkey(), SEND_AMOUNT)], + FeeRate::from_sat_per_vb(4).unwrap(), + ) + .unwrap(); + + let finalized = wallet.sign(&mut psbt, SignOptions::default())?; + assert!(finalized); + + let tx = psbt.extract_tx()?; + client.broadcast(&tx)?; + println!("Tx broadcasted! Txid: {}", tx.compute_txid()); + + Ok(()) +} diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 4d917c82..1c95a5de 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -23,6 +23,7 @@ serde = { version = "^1.0", features = ["derive"] } serde_json = { version = "^1.0" } bdk_chain = { version = "0.21.1", features = [ "miniscript", "serde" ], default-features = false } bdk_file_store = { version = "0.18.1", optional = true } +bdk_tx = { git = "https://github.com/bitcoindevkit/bdk-tx.git", rev = "6e2414ed7e701c04d0af46ff477c2dbf9a9f75b2" } # Optional dependencies bip39 = { version = "2.0", optional = true } diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index e4dc6d05..c5cb84f4 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -19,8 +19,6 @@ use alloc::{ sync::Arc, vec::Vec, }; -use core::{cmp::Ordering, fmt, mem, ops::Deref}; - use bdk_chain::{ indexed_tx_graph, indexer::keychain_txout::KeychainTxOutIndex, @@ -43,11 +41,13 @@ use bitcoin::{ transaction, Address, Amount, Block, BlockHash, FeeRate, Network, OutPoint, Psbt, ScriptBuf, Sequence, Transaction, TxOut, Txid, Weight, Witness, }; +use core::{cmp::Ordering, fmt, mem, ops::Deref}; use miniscript::{ descriptor::KeyMap, psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier}, }; use rand_core::RngCore; +use std::collections::BTreeSet; mod changeset; pub mod coin_selection; @@ -77,7 +77,13 @@ use crate::wallet::{ // re-exports pub use bdk_chain::Balance; +use bdk_tx::{ + create_psbt, create_selection, CreatePsbtParams, CreateSelectionParams, InputCandidates, + InputGroup, Output, +}; +use chain::KeychainIndexed; pub use changeset::ChangeSet; +use miniscript::plan::Assets; pub use params::*; pub use persisted::*; pub use utils::IsDust; @@ -2425,6 +2431,131 @@ impl Wallet { } } +pub struct TransactionParams { + pub outputs: Vec<(ScriptBuf, Amount)>, + pub target_feerate: FeeRate, + pub must_spend: Vec, + // cannot_spend: Vec, +} + +/// Methods that use the bdk_tx crate to build transactions +impl Wallet { + pub fn create_transaction( + &mut self, + outputs: Vec<(ScriptBuf, Amount)>, + target_feerate: FeeRate, + ) -> Result { + let local_outputs: Vec = self.list_unspent().collect(); + let outpoints: Vec> = local_outputs + .into_iter() + .map(|o| ((o.keychain, o.derivation_index), o.outpoint.clone())) + .collect(); + // let descriptors = self.keychains(); + let descriptors: Vec<(KeychainKind, &ExtendedDescriptor)> = self.keychains().collect(); + + let mut descriptors_map = BTreeMap::new(); + let _ = descriptors.into_iter().for_each(|(kind, desc)| { + descriptors_map.insert(kind, desc.clone()); + }); + dbg!(&descriptors_map); + + let input_candidates: Vec = InputCandidates::new( + &self.tx_graph(), // tx_graph + &self.local_chain(), // chain + self.local_chain().tip().block_id().clone(), // chain_tip + outpoints, // outpoints + descriptors_map, // descriptors + BTreeSet::default(), // allow_malleable + Assets::new(), // additional_assets + ) + .unwrap() + .into_single_groups(|_| true); + + let next_change_index: u32 = self.reveal_next_address(KeychainKind::Internal).index; + let public_change_descriptor = self.public_descriptor(KeychainKind::Internal); + + let outputs_vector = outputs + .into_iter() + .map(|o| Output::with_script(o.0, o.1)) + .collect::>(); + + let (selection, metrics) = create_selection(CreateSelectionParams::new( + input_candidates, + public_change_descriptor + .at_derivation_index(next_change_index) + .map_err(|_| CreateBdkTxError::CannotCreateTx)?, + outputs_vector, + target_feerate, + )) + .map_err(|_| CreateBdkTxError::CannotCreateTx)?; + + let (psbt, _) = create_psbt(CreatePsbtParams::new(selection)) + .map_err(|_| CreateBdkTxError::CannotCreateTx)?; + + Ok(psbt) + } + + pub fn create_complex_transaction( + &mut self, + transaction_params: TransactionParams, + ) -> Result { + let local_outputs: Vec = self.list_unspent().collect(); + let outpoints: Vec> = local_outputs + .into_iter() + .map(|o| ((o.keychain, o.derivation_index), o.outpoint.clone())) + .collect(); + // let descriptors = self.keychains(); + let descriptors: Vec<(KeychainKind, &ExtendedDescriptor)> = self.keychains().collect(); + + let mut descriptors_map = BTreeMap::new(); + let _ = descriptors.into_iter().for_each(|(kind, desc)| { + descriptors_map.insert(kind, desc.clone()); + }); + dbg!(&descriptors_map); + + let input_candidates: Vec = InputCandidates::new( + &self.tx_graph(), // tx_graph + &self.local_chain(), // chain + self.local_chain().tip().block_id().clone(), // chain_tip + outpoints, // outpoints + descriptors_map, // descriptors + BTreeSet::default(), // allow_malleable + Assets::new(), // additional_assets + ) + .unwrap() + .into_single_groups(|_| true); + + let next_change_index: u32 = self.reveal_next_address(KeychainKind::Internal).index; + let public_change_descriptor = self.public_descriptor(KeychainKind::Internal); + + let outputs_vector = transaction_params + .outputs + .into_iter() + .map(|o| Output::with_script(o.0, o.1)) + .collect::>(); + + let (selection, metrics) = create_selection(CreateSelectionParams::new( + input_candidates, + public_change_descriptor + .at_derivation_index(next_change_index) + .map_err(|_| CreateBdkTxError::CannotCreateTx)?, + outputs_vector, + transaction_params.target_feerate, + )) + .map_err(|_| CreateBdkTxError::CannotCreateTx)?; + + let (psbt, _) = create_psbt(CreatePsbtParams::new(selection)) + .map_err(|_| CreateBdkTxError::CannotCreateTx)?; + + Ok(psbt) + } +} + +#[derive(Debug)] +pub enum CreateBdkTxError { + CannotCreateTx, +} + impl AsRef> for Wallet { fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph { self.indexed_graph.graph()