diff --git a/.gitignore b/.gitignore index 4dc9a2ff9..5df2148e8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ Cargo.lock *.swp .idea + +# IDE +/.vscode diff --git a/src/database/memory.rs b/src/database/memory.rs index 7d806eb4a..7c6cd76b8 100644 --- a/src/database/memory.rs +++ b/src/database/memory.rs @@ -555,6 +555,7 @@ macro_rules! doctest_wallet { use $crate::bitcoin::Network; use $crate::database::MemoryDatabase; use $crate::testutils; + let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"; let descriptors = testutils!(@descriptors (descriptor) (descriptor)); @@ -567,13 +568,17 @@ macro_rules! doctest_wallet { Some(100), ); - $crate::Wallet::new( + let wallet = $crate::Wallet::new( &descriptors.0, descriptors.1.as_ref(), Network::Regtest, db ) - .unwrap() + .unwrap(); + + wallet.ensure_addresses_cached(10).unwrap(); + + wallet }} } diff --git a/src/error.rs b/src/error.rs index 15ec5713d..f1976474a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -73,8 +73,8 @@ pub enum Error { Key(crate::keys::KeyError), /// Descriptor checksum mismatch ChecksumMismatch, - /// Spending policy is not compatible with this [`KeychainKind`](crate::types::KeychainKind) - SpendingPolicyRequired(crate::types::KeychainKind), + /// Spending policy is not compatible with this [`ExtendedDescriptor`](crate::descriptor::ExtendedDescriptor) + SpendingPolicyRequired(Box), /// Error while extracting and manipulating policies InvalidPolicyPathError(crate::descriptor::policy::PolicyError), /// Signing error diff --git a/src/testutils/blockchain_tests.rs b/src/testutils/blockchain_tests.rs index 7c08699c6..3084a7e06 100644 --- a/src/testutils/blockchain_tests.rs +++ b/src/testutils/blockchain_tests.rs @@ -1328,8 +1328,8 @@ macro_rules! bdk_blockchain_tests { let tx = { let mut builder = wallet.build_tx(); builder.add_recipient(test_client.get_node_address(None).script_pubkey(), 25_000) - .policy_path(ext_path, KeychainKind::External) - .policy_path(int_path, KeychainKind::Internal); + .policy_path(ext_path, wallet.get_descriptor_for_keychain(KeychainKind::External)) + .policy_path(int_path, wallet.get_descriptor_for_keychain(KeychainKind::Internal)); let (mut psbt, _details) = builder.finish().unwrap(); wallet.sign(&mut psbt, Default::default()).unwrap(); psbt.extract_tx() diff --git a/src/wallet/coin_selection.rs b/src/wallet/coin_selection.rs index 7b4a48334..8d025a881 100644 --- a/src/wallet/coin_selection.rs +++ b/src/wallet/coin_selection.rs @@ -26,15 +26,14 @@ //! ``` //! # use std::str::FromStr; //! # use bitcoin::*; -//! # use bdk::wallet::{self, coin_selection::*}; -//! # use bdk::database::Database; +//! # use bdk::wallet::{self, coin_selection::*, multi_tracker::*}; //! # use bdk::*; //! # const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4 + 1) * 4; //! #[derive(Debug)] //! struct AlwaysSpendEverything; //! -//! impl CoinSelectionAlgorithm for AlwaysSpendEverything { -//! fn coin_select( +//! impl CoinSelectionAlgorithm for AlwaysSpendEverything { +//! fn coin_select( //! &self, //! database: &D, //! required_utxos: Vec, @@ -89,7 +88,7 @@ //! ``` use crate::types::FeeRate; -use crate::{database::Database, WeightedUtxo}; +use crate::WeightedUtxo; use crate::{error::Error, Utxo}; use rand::seq::SliceRandom; @@ -100,6 +99,8 @@ use rand::{rngs::StdRng, SeedableRng}; use std::collections::HashMap; use std::convert::TryInto; +use super::multi_tracker::MultiTracker; + /// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not /// overridden #[cfg(not(test))] @@ -140,15 +141,15 @@ impl CoinSelectionResult { /// Trait for generalized coin selection algorithms /// -/// This trait can be implemented to make the [`Wallet`](super::Wallet) use a customized coin -/// selection algorithm when it creates transactions. +/// This trait can be implemented to make the [`TxBuilder`](super::tx_builder::TxBuilder) use a +/// customized coin selection algorithm when it creates transactions. /// /// For an example see [this module](crate::wallet::coin_selection)'s documentation. -pub trait CoinSelectionAlgorithm: std::fmt::Debug { +pub trait CoinSelectionAlgorithm: std::fmt::Debug { /// Perform the coin selection /// - /// - `database`: a reference to the wallet's database that can be used to lookup additional - /// details for a specific UTXO + /// - `multi_tracker`: a reference to the multi-descriptor tracker that can be used to lookup + /// additional details for a specific UTXO /// - `required_utxos`: the utxos that must be spent regardless of `amount_needed` with their /// weight cost /// - `optional_utxos`: the remaining available utxos to satisfy `amount_needed` with their @@ -157,9 +158,9 @@ pub trait CoinSelectionAlgorithm: std::fmt::Debug { /// - `amount_needed`: the amount in satoshi to select /// - `fee_amount`: the amount of fees in satoshi already accumulated from adding outputs and /// the transaction's header - fn coin_select( + fn coin_select( &self, - database: &D, + multi_tracker: &Mt, required_utxos: Vec, optional_utxos: Vec, fee_rate: FeeRate, @@ -175,10 +176,10 @@ pub trait CoinSelectionAlgorithm: std::fmt::Debug { #[derive(Debug, Default, Clone, Copy)] pub struct LargestFirstCoinSelection; -impl CoinSelectionAlgorithm for LargestFirstCoinSelection { - fn coin_select( +impl CoinSelectionAlgorithm for LargestFirstCoinSelection { + fn coin_select( &self, - _database: &D, + _multi_tracker: &Mt, required_utxos: Vec, mut optional_utxos: Vec, fee_rate: FeeRate, @@ -213,10 +214,10 @@ impl CoinSelectionAlgorithm for LargestFirstCoinSelection { #[derive(Debug, Default, Clone, Copy)] pub struct OldestFirstCoinSelection; -impl CoinSelectionAlgorithm for OldestFirstCoinSelection { - fn coin_select( +impl CoinSelectionAlgorithm for OldestFirstCoinSelection { + fn coin_select( &self, - database: &D, + multi_tracker: &Mt, required_utxos: Vec, mut optional_utxos: Vec, fee_rate: FeeRate, @@ -233,10 +234,13 @@ impl CoinSelectionAlgorithm for OldestFirstCoinSelection { if bh_acc.contains_key(&txid) { Ok(bh_acc) } else { - database.get_tx(&txid, false).map(|details| { - bh_acc.insert( - txid, - details.and_then(|d| d.confirmation_time.map(|ct| ct.height)), + multi_tracker.get_tx(&txid).map(|details| { + let conf_time = + details.and_then(|tx| tx.confirmed.map(|conf| conf.height)); + bh_acc.insert(txid, conf_time); + println!( + "OldestFirstCoinSelection: insert({}, {:?})", + txid, conf_time ); bh_acc }) @@ -357,10 +361,10 @@ impl BranchAndBoundCoinSelection { const BNB_TOTAL_TRIES: usize = 100_000; -impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { - fn coin_select( +impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { + fn coin_select( &self, - _database: &D, + _multi_tracker: &Mt, required_utxos: Vec, optional_utxos: Vec, fee_rate: FeeRate, @@ -603,13 +607,17 @@ impl BranchAndBoundCoinSelection { #[cfg(test)] mod test { + use std::cell::RefCell; use std::str::FromStr; - use bitcoin::{OutPoint, Script, TxOut}; + use bitcoin::{Network, OutPoint, Script, Transaction, TxOut, Txid}; use super::*; - use crate::database::{BatchOperations, MemoryDatabase}; + use crate::database::{BatchOperations, Database, MemoryDatabase}; + use crate::descriptor::{ExtendedDescriptor, IntoWalletDescriptor}; use crate::types::*; + use crate::wallet::multi_tracker::LegacyTracker; + use crate::wallet::utils::SecpCtx; use crate::wallet::Vbytes; use rand::rngs::StdRng; @@ -620,6 +628,24 @@ mod test { const FEE_AMOUNT: u64 = 50; + fn new_desc() -> ExtendedDescriptor { + "wsh(pk(cRYhyJQzA92kLQUtJZbVVQrBoQwSm4WT23sanTXYrKoUMjX8ph1Z))" + .into_wallet_descriptor(&SecpCtx::new(), Network::Regtest) + .unwrap() + .0 + } + + fn new_db() -> RefCell { + RefCell::new(MemoryDatabase::new()) + } + + fn new_collection<'a>( + desc: &'a ExtendedDescriptor, + db: &'a RefCell, + ) -> LegacyTracker<'a, MemoryDatabase> { + LegacyTracker::new(desc, None, Network::Regtest, db, Vec::new(), Vec::new()) + } + fn utxo(value: u64, index: u32) -> WeightedUtxo { assert!(index < 10); let outpoint = OutPoint::from_str(&format!( @@ -649,21 +675,52 @@ mod test { ] } + // creates a tx with given utxo as single output. + // updates utxo to contain correct txid in outpoint. + fn tx(utxo: &mut WeightedUtxo, lock_time: u32) -> (Transaction, Txid) { + let local = match &mut utxo.utxo { + Utxo::Local(local) => local, + _ => panic!("utxo should be a `LocalUtxo`"), + }; + let tx = Transaction { + version: 2, + lock_time, + input: Vec::new(), + output: vec![local.txout.clone()], + }; + let txid = tx.txid(); + local.outpoint.txid = txid; + (tx, txid) + } + fn setup_database_and_get_oldest_first_test_utxos( database: &mut D, ) -> Vec { // ensure utxos are from different tx - let utxo1 = utxo(120_000, 1); - let utxo2 = utxo(80_000, 2); - let utxo3 = utxo(300_000, 3); + let mut utxo1 = utxo(120_000, 1); + let mut utxo2 = utxo(80_000, 2); + let mut utxo3 = utxo(300_000, 3); + + // let tx = |utxo: &WeightedUtxo, index: u32| { + // let local = match &utxo.utxo { + // Utxo::Local(local) => local, + // _ => panic!(), + // }; + // Transaction{ version: 2, lock_time: index, input: Vec::new(), output: vec![local.txout.clone()] } + // }; + + let (tx1, txid1) = tx(&mut utxo1, 1); + let (tx2, txid2) = tx(&mut utxo2, 2); + let (tx3, txid3) = tx(&mut utxo3, 3); // add tx to DB so utxos are sorted by blocktime asc // utxos will be selected by the following order // utxo1(blockheight 1) -> utxo2(blockheight 2), utxo3 (blockheight 3) // timestamp are all set as the same to ensure that only block height is used in sorting let utxo1_tx_details = TransactionDetails { - transaction: None, - txid: utxo1.utxo.outpoint().txid, + transaction: Some(tx1), + txid: txid1, + // txid: utxo1.utxo.outpoint().txid, received: 1, sent: 0, fee: None, @@ -674,8 +731,9 @@ mod test { }; let utxo2_tx_details = TransactionDetails { - transaction: None, - txid: utxo2.utxo.outpoint().txid, + transaction: Some(tx2), + txid: txid2, + // txid: utxo2.utxo.outpoint().txid, received: 1, sent: 0, fee: None, @@ -686,8 +744,9 @@ mod test { }; let utxo3_tx_details = TransactionDetails { - transaction: None, - txid: utxo3.utxo.outpoint().txid, + transaction: Some(tx3), + txid: txid3, + // txid: utxo3.utxo.outpoint().txid, received: 1, sent: 0, fee: None, @@ -757,11 +816,13 @@ mod test { #[test] fn test_largest_first_coin_selection_success() { let utxos = get_test_utxos(); - let database = MemoryDatabase::default(); + let desc = new_desc(); + let db = new_db(); + let cache = new_collection(&desc, &db); let result = LargestFirstCoinSelection::default() .coin_select( - &database, + &cache, utxos, vec![], FeeRate::from_sat_per_vb(1.0), @@ -778,11 +839,13 @@ mod test { #[test] fn test_largest_first_coin_selection_use_all() { let utxos = get_test_utxos(); - let database = MemoryDatabase::default(); + let desc = new_desc(); + let db = new_db(); + let cache = new_collection(&desc, &db); let result = LargestFirstCoinSelection::default() .coin_select( - &database, + &cache, utxos, vec![], FeeRate::from_sat_per_vb(1.0), @@ -799,11 +862,13 @@ mod test { #[test] fn test_largest_first_coin_selection_use_only_necessary() { let utxos = get_test_utxos(); - let database = MemoryDatabase::default(); + let desc = new_desc(); + let db = new_db(); + let cache = new_collection(&desc, &db); let result = LargestFirstCoinSelection::default() .coin_select( - &database, + &cache, vec![], utxos, FeeRate::from_sat_per_vb(1.0), @@ -821,11 +886,13 @@ mod test { #[should_panic(expected = "InsufficientFunds")] fn test_largest_first_coin_selection_insufficient_funds() { let utxos = get_test_utxos(); - let database = MemoryDatabase::default(); + let desc = new_desc(); + let db = new_db(); + let cache = new_collection(&desc, &db); LargestFirstCoinSelection::default() .coin_select( - &database, + &cache, vec![], utxos, FeeRate::from_sat_per_vb(1.0), @@ -839,11 +906,13 @@ mod test { #[should_panic(expected = "InsufficientFunds")] fn test_largest_first_coin_selection_insufficient_funds_high_fees() { let utxos = get_test_utxos(); - let database = MemoryDatabase::default(); + let desc = new_desc(); + let db = new_db(); + let cache = new_collection(&desc, &db); LargestFirstCoinSelection::default() .coin_select( - &database, + &cache, vec![], utxos, FeeRate::from_sat_per_vb(1000.0), @@ -855,12 +924,14 @@ mod test { #[test] fn test_oldest_first_coin_selection_success() { - let mut database = MemoryDatabase::default(); - let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database); + let desc = new_desc(); + let db = new_db(); + let cache = new_collection(&desc, &db); + let utxos = { setup_database_and_get_oldest_first_test_utxos(&mut *db.borrow_mut()) }; let result = OldestFirstCoinSelection::default() .coin_select( - &database, + &cache, vec![], utxos, FeeRate::from_sat_per_vb(1.0), @@ -877,19 +948,29 @@ mod test { #[test] fn test_oldest_first_coin_selection_utxo_not_in_db_will_be_selected_last() { // ensure utxos are from different tx - let utxo1 = utxo(120_000, 1); - let utxo2 = utxo(80_000, 2); - let utxo3 = utxo(300_000, 3); + let mut utxo1 = utxo(120_000, 1); + let mut utxo2 = utxo(80_000, 2); + let mut utxo3 = utxo(300_000, 3); + + let (tx1, txid1) = tx(&mut utxo1, 1); + let (tx2, txid2) = tx(&mut utxo2, 2); + let _ = tx(&mut utxo3, 3); - let mut database = MemoryDatabase::default(); + // let txid1 = tx1.txid(); + // let txid2 = tx2.txid(); + + let desc = new_desc(); + let db = new_db(); + let cache = new_collection(&desc, &db); // add tx to DB so utxos are sorted by blocktime asc // utxos will be selected by the following order // utxo1(blockheight 1) -> utxo2(blockheight 2), utxo3 (not exist in DB) // timestamp are all set as the same to ensure that only block height is used in sorting let utxo1_tx_details = TransactionDetails { - transaction: None, - txid: utxo1.utxo.outpoint().txid, + transaction: Some(tx1), + txid: txid1, + // txid: utxo1.utxo.outpoint().txid, received: 1, sent: 0, fee: None, @@ -900,8 +981,9 @@ mod test { }; let utxo2_tx_details = TransactionDetails { - transaction: None, - txid: utxo2.utxo.outpoint().txid, + transaction: Some(tx2), + txid: txid2, + // txid: utxo2.utxo.outpoint().txid, received: 1, sent: 0, fee: None, @@ -911,12 +993,15 @@ mod test { }), }; - database.set_tx(&utxo1_tx_details).unwrap(); - database.set_tx(&utxo2_tx_details).unwrap(); + { + let db = &mut *db.borrow_mut(); + db.set_tx(&utxo1_tx_details).unwrap(); + db.set_tx(&utxo2_tx_details).unwrap(); + } let result = OldestFirstCoinSelection::default() .coin_select( - &database, + &cache, vec![], vec![utxo3, utxo1, utxo2], FeeRate::from_sat_per_vb(1.0), @@ -932,12 +1017,14 @@ mod test { #[test] fn test_oldest_first_coin_selection_use_all() { - let mut database = MemoryDatabase::default(); - let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database); + let desc = new_desc(); + let db = new_db(); + let cache = new_collection(&desc, &db); + let utxos = { setup_database_and_get_oldest_first_test_utxos(&mut *db.borrow_mut()) }; let result = OldestFirstCoinSelection::default() .coin_select( - &database, + &cache, utxos, vec![], FeeRate::from_sat_per_vb(1.0), @@ -953,12 +1040,14 @@ mod test { #[test] fn test_oldest_first_coin_selection_use_only_necessary() { - let mut database = MemoryDatabase::default(); - let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database); + let desc = new_desc(); + let db = new_db(); + let cache = new_collection(&desc, &db); + let utxos = { setup_database_and_get_oldest_first_test_utxos(&mut *db.borrow_mut()) }; let result = OldestFirstCoinSelection::default() .coin_select( - &database, + &cache, vec![], utxos, FeeRate::from_sat_per_vb(1.0), @@ -975,12 +1064,14 @@ mod test { #[test] #[should_panic(expected = "InsufficientFunds")] fn test_oldest_first_coin_selection_insufficient_funds() { - let mut database = MemoryDatabase::default(); - let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database); + let desc = new_desc(); + let db = new_db(); + let cache = new_collection(&desc, &db); + let utxos = { setup_database_and_get_oldest_first_test_utxos(&mut *db.borrow_mut()) }; OldestFirstCoinSelection::default() .coin_select( - &database, + &cache, vec![], utxos, FeeRate::from_sat_per_vb(1.0), @@ -993,15 +1084,17 @@ mod test { #[test] #[should_panic(expected = "InsufficientFunds")] fn test_oldest_first_coin_selection_insufficient_funds_high_fees() { - let mut database = MemoryDatabase::default(); - let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database); + let desc = new_desc(); + let db = new_db(); + let cache = new_collection(&desc, &db); + let utxos = { setup_database_and_get_oldest_first_test_utxos(&mut *db.borrow_mut()) }; let amount_needed: u64 = utxos.iter().map(|wu| wu.utxo.txout().value).sum::() - (FEE_AMOUNT + 50); OldestFirstCoinSelection::default() .coin_select( - &database, + &cache, vec![], utxos, FeeRate::from_sat_per_vb(1000.0), @@ -1017,11 +1110,13 @@ mod test { // select three outputs let utxos = generate_same_value_utxos(100_000, 20); - let database = MemoryDatabase::default(); + let desc = new_desc(); + let db = new_db(); + let cache = new_collection(&desc, &db); let result = BranchAndBoundCoinSelection::default() .coin_select( - &database, + &cache, vec![], utxos, FeeRate::from_sat_per_vb(1.0), @@ -1038,11 +1133,13 @@ mod test { #[test] fn test_bnb_coin_selection_required_are_enough() { let utxos = get_test_utxos(); - let database = MemoryDatabase::default(); + let desc = new_desc(); + let db = new_db(); + let cache = new_collection(&desc, &db); let result = BranchAndBoundCoinSelection::default() .coin_select( - &database, + &cache, utxos.clone(), utxos, FeeRate::from_sat_per_vb(1.0), @@ -1059,11 +1156,13 @@ mod test { #[test] fn test_bnb_coin_selection_optional_are_enough() { let utxos = get_test_utxos(); - let database = MemoryDatabase::default(); + let desc = new_desc(); + let db = new_db(); + let cache = new_collection(&desc, &db); let result = BranchAndBoundCoinSelection::default() .coin_select( - &database, + &cache, vec![], utxos, FeeRate::from_sat_per_vb(1.0), @@ -1080,7 +1179,9 @@ mod test { #[test] fn test_bnb_coin_selection_required_not_enough() { let utxos = get_test_utxos(); - let database = MemoryDatabase::default(); + let desc = new_desc(); + let db = new_db(); + let cache = new_collection(&desc, &db); let required = vec![utxos[0].clone()]; let mut optional = utxos[1..].to_vec(); @@ -1094,7 +1195,7 @@ mod test { let result = BranchAndBoundCoinSelection::default() .coin_select( - &database, + &cache, required, optional, FeeRate::from_sat_per_vb(1.0), @@ -1112,11 +1213,13 @@ mod test { #[should_panic(expected = "InsufficientFunds")] fn test_bnb_coin_selection_insufficient_funds() { let utxos = get_test_utxos(); - let database = MemoryDatabase::default(); + let desc = new_desc(); + let db = new_db(); + let cache = new_collection(&desc, &db); BranchAndBoundCoinSelection::default() .coin_select( - &database, + &cache, vec![], utxos, FeeRate::from_sat_per_vb(1.0), @@ -1130,11 +1233,13 @@ mod test { #[should_panic(expected = "InsufficientFunds")] fn test_bnb_coin_selection_insufficient_funds_high_fees() { let utxos = get_test_utxos(); - let database = MemoryDatabase::default(); + let desc = new_desc(); + let db = new_db(); + let cache = new_collection(&desc, &db); BranchAndBoundCoinSelection::default() .coin_select( - &database, + &cache, vec![], utxos, FeeRate::from_sat_per_vb(1000.0), @@ -1147,11 +1252,12 @@ mod test { #[test] fn test_bnb_coin_selection_check_fee_rate() { let utxos = get_test_utxos(); - let database = MemoryDatabase::default(); - + let desc = new_desc(); + let db = new_db(); + let cache = new_collection(&desc, &db); let result = BranchAndBoundCoinSelection::new(0) .coin_select( - &database, + &cache, vec![], utxos, FeeRate::from_sat_per_vb(1.0), @@ -1171,14 +1277,16 @@ mod test { fn test_bnb_coin_selection_exact_match() { let seed = [0; 32]; let mut rng: StdRng = SeedableRng::from_seed(seed); - let database = MemoryDatabase::default(); + let desc = new_desc(); + let db = new_db(); + let cache = new_collection(&desc, &db); for _i in 0..200 { let mut optional_utxos = generate_random_utxos(&mut rng, 16); let target_amount = sum_random_utxos(&mut rng, &mut optional_utxos); let result = BranchAndBoundCoinSelection::new(0) .coin_select( - &database, + &cache, vec![], optional_utxos, FeeRate::from_sat_per_vb(0.0), diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 9231c3b74..76de4e3ad 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -41,6 +41,9 @@ pub mod address_validator; pub mod coin_selection; pub mod export; pub mod signer; + +/// Spendable collection +pub mod multi_tracker; pub mod time; pub mod tx_builder; pub(crate) mod utils; @@ -63,9 +66,8 @@ use crate::database::{AnyDatabase, BatchDatabase, BatchOperations, DatabaseUtils use crate::descriptor::derived::AsDerived; use crate::descriptor::policy::BuildSatisfaction; use crate::descriptor::{ - get_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DerivedDescriptorMeta, - DescriptorMeta, DescriptorScripts, ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, - Policy, XKeyUtils, + get_checksum, into_wallet_descriptor_checked, DerivedDescriptorMeta, DescriptorMeta, + DescriptorScripts, ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, Policy, XKeyUtils, }; use crate::error::Error; use crate::psbt::PsbtUtils; @@ -73,6 +75,9 @@ use crate::signer::SignerError; use crate::testutils; use crate::types::*; +use self::multi_tracker::TransactionItem; +pub use self::multi_tracker::{LegacyTracker, MultiTracker}; + const CACHE_ADDR_BATCH_SIZE: u32 = 100; const COINBASE_MATURITY: u32 = 100; @@ -245,12 +250,39 @@ where // Return a newly derived address for the specified `keychain`. fn get_new_address(&self, keychain: KeychainKind) -> Result { - let incremented_index = self.fetch_and_increment_index(keychain)?; + let (descriptor, _) = self._get_descriptor_for_keychain(keychain); - let address_result = self - .get_descriptor_for_keychain(keychain) - .as_derived(incremented_index, &self.secp) - .address(self.network); + Self::_get_new_address( + &self.database, + &self.secp, + self.address_validators.clone(), + descriptor, + keychain, + self.network, + ) + } + + /// TODO @evanlinjin: Move out of [`Wallet`]. + #[allow(deprecated)] + pub(crate) fn _get_new_address( + database: &RefCell, + secp: &SecpCtx, + address_validators: Vec>, + descriptor: &ExtendedDescriptor, + keychain: KeychainKind, + network: Network, + ) -> Result { + let incremented_index = Self::_fetch_and_increment_index( + database, + secp, + address_validators, + descriptor, + keychain, + )?; + + let address_result = descriptor + .as_derived(incremented_index, secp) + .address(network); address_result .map(|address| AddressInfo { @@ -517,6 +549,19 @@ where &self.address_validators } + fn as_multi_tracker(&self) -> LegacyTracker { + let signers = vec![Arc::clone(&self.signers), Arc::clone(&self.change_signers)]; + + LegacyTracker::new( + &self.descriptor, + self.change_descriptor.as_ref(), + self.network, + &self.database, + signers, + self.address_validators.clone(), + ) + } + /// Start building a transaction. /// /// This returns a blank [`TxBuilder`] from which you can specify the parameters for the transaction. @@ -543,74 +588,70 @@ where /// ``` /// /// [`TxBuilder`]: crate::TxBuilder - pub fn build_tx(&self) -> TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, CreateTx> { + pub fn build_tx( + &self, + ) -> TxBuilder<'_, LegacyTracker, DefaultCoinSelectionAlgorithm, CreateTx> { TxBuilder { - wallet: self, + multi_tracker: self.as_multi_tracker(), params: TxParams::default(), coin_selection: DefaultCoinSelectionAlgorithm::default(), + secp: self.secp.clone(), phantom: core::marker::PhantomData, } } - pub(crate) fn create_tx>( - &self, + /// TODO @evanlinjin: Move this out of [`Wallet`]. + /// Is `fallback_block_height` in the right place? + pub(crate) fn _create_tx<'a, Sc, Cs>( + spendable_collection: &'a Sc, coin_selection: Cs, params: TxParams, - ) -> Result<(psbt::PartiallySignedTransaction, TransactionDetails), Error> { - let external_policy = self - .descriptor - .extract_policy(&self.signers, BuildSatisfaction::None, &self.secp)? - .unwrap(); - let internal_policy = self - .change_descriptor - .as_ref() - .map(|desc| { - Ok::<_, Error>( - desc.extract_policy(&self.change_signers, BuildSatisfaction::None, &self.secp)? - .unwrap(), - ) - }) - .transpose()?; + secp: &SecpCtx, + ) -> Result<(psbt::PartiallySignedTransaction, TransactionDetails), Error> + where + Sc: MultiTracker, + Cs: coin_selection::CoinSelectionAlgorithm, + { + let conditions = spendable_collection.iter_descriptors()?.map(|item| { + let policy = item + .descriptor + .extract_policy(&item.signers, BuildSatisfaction::None, secp)? + .unwrap(); + let policy_path = params.descriptor_policy_paths.get(&item.descriptor); - // The policy allows spending external outputs, but it requires a policy path that hasn't been - // provided - if params.change_policy != tx_builder::ChangeSpendPolicy::OnlyChange - && external_policy.requires_path() - && params.external_policy_path.is_none() - { - return Err(Error::SpendingPolicyRequired(KeychainKind::External)); - }; - // Same for the internal_policy path, if present - if let Some(internal_policy) = &internal_policy { - if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeForbidden - && internal_policy.requires_path() - && params.internal_policy_path.is_none() - { - return Err(Error::SpendingPolicyRequired(KeychainKind::Internal)); + // are we permitted to spend inputs? + let is_permitted = match item.keychain { + KeychainKind::External => { + params.change_policy != tx_builder::ChangeSpendPolicy::OnlyChange + } + KeychainKind::Internal => { + params.change_policy != tx_builder::ChangeSpendPolicy::ChangeForbidden + } }; - } - let external_requirements = external_policy.get_condition( - params - .external_policy_path - .as_ref() - .unwrap_or(&BTreeMap::new()), - )?; - let internal_requirements = internal_policy - .map(|policy| { - Ok::<_, Error>( - policy.get_condition( - params - .internal_policy_path - .as_ref() - .unwrap_or(&BTreeMap::new()), - )?, - ) + // if permitted, ensure policy path requirements are satisfied + if is_permitted && policy.requires_path() && policy_path.is_none() { + Err(Error::SpendingPolicyRequired(Box::new( + item.descriptor.clone(), + ))) + } else { + policy + .get_condition(policy_path.unwrap_or(&BTreeMap::new())) + .map_err(Error::InvalidPolicyPathError) + } + }); + + let requirements = conditions + .reduce(|acc_res, cond_res| match (acc_res, cond_res) { + (Ok(acc), Ok(cond)) => acc.merge(&cond).map_err(Error::InvalidPolicyPathError), + (Err(err), _) => Err(err), + (_, Err(err)) => Err(err), }) - .transpose()?; + .transpose()? + // TODO @evanlinjin: Figure out an appropriate error when there are no descriptors in + // the spendable collection. + .ok_or_else(|| Error::Generic("No descriptors in spendable collection".to_string()))?; - let requirements = - external_requirements.merge(&internal_requirements.unwrap_or_default())?; debug!("Policy requirements: {:?}", requirements); let version = match params.version { @@ -631,10 +672,9 @@ where // We use a match here instead of a map_or_else as it's way more readable :) let current_height = match params.current_height { // If they didn't tell us the current height, we assume it's the latest sync height. - None => self - .database() - .get_sync_time()? - .map(|sync_time| sync_time.block_time.height), + None => spendable_collection + .latest_blockheight()? + .map(|bt| bt.height), h => h, }; @@ -744,7 +784,7 @@ where return Err(Error::OutputBelowDustLimit(index)); } - if self.is_mine(script_pubkey)? { + if spendable_collection.is_mine(script_pubkey)? { received += value; } @@ -752,6 +792,7 @@ where script_pubkey: script_pubkey.clone(), value, }; + fee_amount += fee_rate.fee_vb(serialize(&new_out).len()); tx.output.push(new_out); @@ -759,26 +800,35 @@ where outgoing += value; } - if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed - && self.change_descriptor.is_none() - { - return Err(Error::Generic( - "The `change_policy` can be set only if the wallet has a change_descriptor".into(), - )); + // If policy is to only use internal inputs, we need to ensure we have internal descriptors + // @evanlinjin: This logic has been changed, see Discord: + // https://discord.com/channels/753336465005608961/753367451319926827/995475330531868782 + if params.change_policy == tx_builder::ChangeSpendPolicy::OnlyChange { + let has_internal = spendable_collection + .iter_descriptors()? + .any(|item| item.keychain == KeychainKind::Internal); + + if !has_internal { + return Err(Error::Generic( + "The `change_policy` can be set only if the wallet has a change_descriptor" + .into(), + )); + } } - let (required_utxos, optional_utxos) = self.preselect_utxos( + let (required_utxos, optional_utxos) = Self::_preselect_utxos( + spendable_collection, params.change_policy, ¶ms.unspendable, params.utxos.clone(), params.drain_wallet, params.manually_selected_only, - params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee + params.bumping_fee.is_some(), /* we mandate confirmed transactions if we're bumping the fee */ current_height, )?; let coin_selection = coin_selection.coin_select( - self.database.borrow().deref(), + spendable_collection, required_utxos, optional_utxos, fee_rate, @@ -802,8 +852,9 @@ where let mut drain_output = { let script_pubkey = match params.drain_to { Some(ref drain_recipient) => drain_recipient.clone(), - None => self - .get_internal_address(AddressIndex::New)? + #[allow(deprecated)] + None => spendable_collection + .new_auto_address()? .address .script_pubkey(), }; @@ -842,7 +893,7 @@ where fee_amount += drain_val; } else { drain_output.value = drain_val; - if self.is_mine(&drain_output.script_pubkey)? { + if spendable_collection.is_mine(&drain_output.script_pubkey)? { received += drain_val; } tx.output.push(drain_output); @@ -853,7 +904,13 @@ where let txid = tx.txid(); let sent = coin_selection.local_selected_amount(); - let psbt = self.complete_transaction(tx, coin_selection.selected, params)?; + let psbt = Self::_complete_transaction( + spendable_collection, + tx, + coin_selection.selected, + params, + secp, + )?; let transaction_details = TransactionDetails { transaction: None, @@ -910,43 +967,49 @@ where pub fn build_fee_bump( &self, txid: Txid, - ) -> Result, Error> { - let mut details = match self.database.borrow().get_tx(&txid, true)? { + ) -> Result, DefaultCoinSelectionAlgorithm, BumpFee>, Error> + { + let multi_tracker = self.as_multi_tracker(); + + Self::_build_fee_bump(multi_tracker, self.secp.clone(), txid) + } + + pub(crate) fn _build_fee_bump<'a, Mt: MultiTracker>( + multi_tracker: Mt, + secp: SecpCtx, + txid: Txid, + ) -> Result, Error> { + let (mut tx, tx_fee) = match multi_tracker.get_tx(&txid)? { None => return Err(Error::TransactionNotFound), - Some(tx) if tx.transaction.is_none() => return Err(Error::TransactionNotFound), - Some(tx) if tx.confirmation_time.is_some() => return Err(Error::TransactionConfirmed), - Some(tx) => tx, + Some(TransactionItem { + confirmed: Some(_), .. + }) => return Err(Error::TransactionConfirmed), + Some(TransactionItem { fees: None, .. }) => return Err(Error::FeeRateUnavailable), + Some(tx) => (tx.raw, tx.fees.unwrap()), }; - let mut tx = details.transaction.take().unwrap(); + + // let mut tx = details.transaction.take().unwrap(); if !tx.input.iter().any(|txin| txin.sequence <= 0xFFFFFFFD) { return Err(Error::IrreplaceableTransaction); } - let feerate = FeeRate::from_wu(details.fee.ok_or(Error::FeeRateUnavailable)?, tx.weight()); + let feerate = FeeRate::from_wu(tx_fee, tx.weight()); // remove the inputs from the tx and process them let original_txin = tx.input.drain(..).collect::>(); let original_utxos = original_txin .iter() .map(|txin| -> Result<_, Error> { - let txout = self - .database - .borrow() - .get_previous_output(&txin.previous_output)? + let txout = multi_tracker + .get_output(&txin.previous_output)? .ok_or(Error::UnknownUtxo)?; - let (weight, keychain) = match self - .database - .borrow() - .get_path_from_script_pubkey(&txout.script_pubkey)? - { - Some((keychain, _)) => ( - self._get_descriptor_for_keychain(keychain) - .0 - .max_satisfaction_weight() - .unwrap(), - keychain, - ), + let path = multi_tracker.get_path_of_script_pubkey(&txout.script_pubkey)?; + let (weight, keychain) = match path { + Some((item, _)) => { + let weight = item.descriptor.max_satisfaction_weight().unwrap(); + (weight, item.keychain) + } None => { // estimate the weight based on the scriptsig/witness size present in the // original transaction @@ -970,17 +1033,19 @@ where }) .collect::, _>>()?; + // Find change index + // TODO @evanlinjin: Should we prioritize larger inputs so we have more wriggle room? if tx.output.len() > 1 { let mut change_index = None; + for (index, txout) in tx.output.iter().enumerate() { - let (_, change_type) = self._get_descriptor_for_keychain(KeychainKind::Internal); - match self - .database - .borrow() - .get_path_from_script_pubkey(&txout.script_pubkey)? - { - Some((keychain, _)) if keychain == change_type => change_index = Some(index), - _ => {} + let path = multi_tracker.get_path_of_script_pubkey(&txout.script_pubkey)?; + + if let Some((item, _)) = path { + // Prioritize internal inputs + if change_index.is_none() || item.keychain == KeychainKind::Internal { + change_index = Some(index); + } } } @@ -999,17 +1064,18 @@ where .collect(), utxos: original_utxos, bumping_fee: Some(tx_builder::PreviousFee { - absolute: details.fee.ok_or(Error::FeeRateUnavailable)?, + absolute: tx_fee, rate: feerate.as_sat_vb(), }), ..Default::default() }; Ok(TxBuilder { - wallet: self, + multi_tracker, params, coin_selection: DefaultCoinSelectionAlgorithm::default(), phantom: core::marker::PhantomData, + secp, }) } @@ -1043,9 +1109,21 @@ where &self, psbt: &mut psbt::PartiallySignedTransaction, sign_options: SignOptions, + ) -> Result { + let spendable_collection = self.as_multi_tracker(); + + Self::_sign(&spendable_collection, &self.secp, psbt, sign_options) + } + + pub(crate) fn _sign( + spendable_collection: &Sc, + secp: &SecpCtx, + psbt: &mut psbt::PartiallySignedTransaction, + sign_options: SignOptions, ) -> Result { // this helps us doing our job later - self.add_input_hd_keypaths(psbt)?; + Self::_add_input_hd_keypaths(spendable_collection, secp, psbt)?; + // self.add_input_hd_keypaths(psbt)?; // If we aren't allowed to use `witness_utxo`, ensure that every input (except p2tr and finalized ones) // has the `non_witness_utxo` @@ -1073,18 +1151,13 @@ where return Err(Error::Signer(signer::SignerError::NonStandardSighash)); } - for signer in self - .signers - .signers() - .iter() - .chain(self.change_signers.signers().iter()) - { - signer.sign_transaction(psbt, &self.secp)?; + for signer in spendable_collection.iter_signers()? { + signer.sign_transaction(psbt, secp)?; } // attempt to finalize if sign_options.try_finalize { - self.finalize_psbt(psbt, sign_options) + Self::_finalize_psbt(spendable_collection, secp, psbt, sign_options) } else { Ok(false) } @@ -1129,6 +1202,17 @@ where &self, psbt: &mut psbt::PartiallySignedTransaction, sign_options: SignOptions, + ) -> Result { + let spendable_collection = self.as_multi_tracker(); + + Self::_finalize_psbt(&spendable_collection, &self.secp, psbt, sign_options) + } + + pub(crate) fn _finalize_psbt( + spendable_collection: &Sc, + secp: &SecpCtx, + psbt: &mut psbt::PartiallySignedTransaction, + sign_options: SignOptions, ) -> Result { let tx = &psbt.unsigned_tx; let mut finished = true; @@ -1143,15 +1227,12 @@ where } // if the height is None in the database it means it's still unconfirmed, so consider // that as a very high value - let create_height = self - .database - .borrow() - .get_tx(&input.previous_output.txid, false)? - .map(|tx| tx.confirmation_time.map(|c| c.height).unwrap_or(u32::MAX)); - let last_sync_height = self - .database() - .get_sync_time()? - .map(|sync_time| sync_time.block_time.height); + let create_height = spendable_collection + .get_tx(&input.previous_output.txid)? + .map(|tx| tx.confirmed.map_or(u32::MAX, |c| c.height)); + let last_sync_height = spendable_collection + .latest_blockheight()? + .map(|bt| bt.height); let current_height = sign_options.assume_height.or(last_sync_height); debug!( @@ -1165,25 +1246,28 @@ where // is in `src/descriptor/mod.rs`, but it will basically look at `bip32_derivation`, // `redeem_script` and `witness_script` to determine the right derivation // - If that also fails, it will try it on the internal descriptor, if present - let desc = psbt + let derived_desc = psbt .get_utxo_for(n) - .map(|txout| self.get_descriptor_for_txout(&txout)) - .transpose()? - .flatten() - .or_else(|| { - self.descriptor.derive_from_psbt_input( - psbt_input, - psbt.get_utxo_for(n), - &self.secp, - ) + .and_then(|txout| { + spendable_collection + .get_path_of_script_pubkey(&txout.script_pubkey) + .expect("spendable collection should not fail") + .map(|(parent_item, child_ind)| { + parent_item.descriptor.as_derived(child_ind, secp) + }) }) .or_else(|| { - self.change_descriptor.as_ref().and_then(|desc| { - desc.derive_from_psbt_input(psbt_input, psbt.get_utxo_for(n), &self.secp) - }) + spendable_collection + .iter_descriptors() + .expect("spendable collection should not fail") + .find_map(|item| { + let prev_utxo = psbt.get_utxo_for(n); + item.descriptor + .derive_from_psbt_input(psbt_input, prev_utxo, secp) + }) }); - match desc { + match derived_desc { Some(desc) => { let mut tmp_input = bitcoin::TxIn::default(); match desc.satisfy( @@ -1241,41 +1325,52 @@ where } } - fn get_descriptor_for_txout( - &self, - txout: &TxOut, - ) -> Result>, Error> { - Ok(self - .database - .borrow() - .get_path_from_script_pubkey(&txout.script_pubkey)? - .map(|(keychain, child)| (self.get_descriptor_for_keychain(keychain), child)) - .map(|(desc, child)| desc.as_derived(child, &self.secp))) - } - fn fetch_and_increment_index(&self, keychain: KeychainKind) -> Result { let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain); + Self::_fetch_and_increment_index( + &self.database, + &self.secp, + self.address_validators.clone(), + descriptor, + keychain, + ) + } + + // TODO @evanlinjin: Move out of [`Wallet`]. + #[allow(deprecated)] + fn _fetch_and_increment_index( + database: &RefCell, + secp: &SecpCtx, + address_validators: Vec>, + descriptor: &ExtendedDescriptor, + keychain: KeychainKind, + ) -> Result { let index = match descriptor.is_deriveable() { false => 0, - true => self.database.borrow_mut().increment_last_index(keychain)?, + true => database.borrow_mut().increment_last_index(keychain)?, }; - if self - .database + if database .borrow() .get_script_pubkey_from_path(keychain, index)? .is_none() { - self.cache_addresses(keychain, index, CACHE_ADDR_BATCH_SIZE)?; + Self::_cache_addresses( + database, + secp, + descriptor, + keychain, + index, + CACHE_ADDR_BATCH_SIZE, + )?; } - let derived_descriptor = descriptor.as_derived(index, &self.secp); + let derived_descriptor = descriptor.as_derived(index, secp); - let hd_keypaths = derived_descriptor.get_hd_keypaths(&self.secp); + let hd_keypaths = derived_descriptor.get_hd_keypaths(secp); let script = derived_descriptor.script_pubkey(); - for validator in &self.address_validators { - #[allow(deprecated)] + for validator in address_validators { validator.validate(keychain, &hd_keypaths, &script)?; } @@ -1301,13 +1396,27 @@ where Ok(()) } - fn cache_addresses( - &self, + fn cache_addresses(&self, keychain: KeychainKind, from: u32, count: u32) -> Result<(), Error> { + let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain); + Self::_cache_addresses( + &self.database, + &self.secp, + descriptor, + keychain, + from, + count, + ) + } + + /// TODO @evanlinjin: Move out of [`Wallet`]. + fn _cache_addresses( + database: &RefCell, + secp: &SecpCtx, + descriptor: &ExtendedDescriptor, keychain: KeychainKind, from: u32, mut count: u32, ) -> Result<(), Error> { - let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain); if !descriptor.is_deriveable() { if from > 0 { return Ok(()); @@ -1316,12 +1425,12 @@ where count = 1; } - let mut address_batch = self.database.borrow().begin_batch(); + let mut address_batch = database.borrow().begin_batch(); let start_time = time::Instant::new(); for i in from..(from + count) { address_batch.set_script_pubkey( - &descriptor.as_derived(i, &self.secp).script_pubkey(), + &descriptor.as_derived(i, secp).script_pubkey(), keychain, i, )?; @@ -1334,33 +1443,18 @@ where start_time.elapsed().as_millis() ); - self.database.borrow_mut().commit_batch(address_batch)?; + database.borrow_mut().commit_batch(address_batch)?; Ok(()) } - fn get_available_utxos(&self) -> Result, Error> { - Ok(self - .list_unspent()? - .into_iter() - .map(|utxo| { - let keychain = utxo.keychain; - ( - utxo, - self.get_descriptor_for_keychain(keychain) - .max_satisfaction_weight() - .unwrap(), - ) - }) - .collect()) - } - /// Given the options returns the list of utxos that must be used to form the /// transaction and any further that may be used if needed. + /// TODO @evanlinjin: Move out of [`Wallet`]. #[allow(clippy::type_complexity)] #[allow(clippy::too_many_arguments)] - fn preselect_utxos( - &self, + fn _preselect_utxos( + spendable_collection: &Sc, change_policy: tx_builder::ChangeSpendPolicy, unspendable: &HashSet, manually_selected: Vec, @@ -1368,30 +1462,36 @@ where manual_only: bool, must_only_use_confirmed_tx: bool, current_height: Option, - ) -> Result<(Vec, Vec), Error> { - // must_spend <- manually selected utxos - // may_spend <- all other available utxos - let mut may_spend = self.get_available_utxos()?; - - may_spend.retain(|may_spend| { - !manually_selected - .iter() - .any(|manually_selected| manually_selected.utxo.outpoint() == may_spend.0.outpoint) - }); - let mut must_spend = manually_selected; - + ) -> Result<(Vec, Vec), Error> + where + Sc: MultiTracker, + { // NOTE: we are intentionally ignoring `unspendable` here. i.e manual // selection overrides unspendable. if manual_only { - return Ok((must_spend, vec![])); + return Ok((manually_selected, vec![])); } - let database = self.database.borrow(); + // must_spend <- manually selected utxos + // may_spend <- all other available utxos + let mut may_spend = spendable_collection + .iter_unspent()? + .filter(|(utxo, _)| { + let is_manually_selected = manually_selected + .iter() + .any(|selected_utxo| selected_utxo.utxo.outpoint() == utxo.outpoint); + + !is_manually_selected + }) + .collect::>(); + + let mut must_spend = manually_selected; + let satisfies_confirmed = may_spend .iter() .map(|u| { - database - .get_tx(&u.0.outpoint.txid, true) + spendable_collection + .get_tx(&u.0.outpoint.txid) .map(|tx| match tx { // We don't have the tx in the db for some reason, // so we can't know for sure if it's mature or not. @@ -1400,16 +1500,12 @@ where Some(tx) => { // Whether the UTXO is mature and, if needed, confirmed let mut spendable = true; - if must_only_use_confirmed_tx && tx.confirmation_time.is_none() { + if must_only_use_confirmed_tx && tx.confirmed.is_none() { return false; } - if tx - .transaction - .expect("We specifically ask for the transaction above") - .is_coin_base() - { + if tx.raw.is_coin_base() { if let Some(current_height) = current_height { - match &tx.confirmation_time { + match &tx.confirmed { Some(t) => { // https://github.com/bitcoin/bitcoin/blob/c5e67be03bb06a5d7885c55db1f016fbf2333fe3/src/validation.cpp#L373-L375 spendable &= (current_height.saturating_sub(t.height)) @@ -1449,26 +1545,32 @@ where Ok((must_spend, may_spend)) } - fn complete_transaction( - &self, + /// TODO @evanlinjin: Move out of [`Wallet`]. + fn _complete_transaction<'a, Sc>( + spendable_collection: &'a Sc, tx: Transaction, selected: Vec, params: TxParams, - ) -> Result { + secp: &SecpCtx, + ) -> Result + where + Sc: MultiTracker, + { let mut psbt = psbt::PartiallySignedTransaction::from_unsigned_tx(tx)?; if params.add_global_xpubs { - let mut all_xpubs = self.descriptor.get_extended_keys()?; - if let Some(change_descriptor) = &self.change_descriptor { - all_xpubs.extend(change_descriptor.get_extended_keys()?); - } + let all_xpubs = spendable_collection + .iter_descriptors()? + .try_fold(Vec::new(), |mut xpubs, item| { + xpubs.append(&mut item.descriptor.get_extended_keys()?); + Ok(xpubs) + }) + .map_err(Error::Descriptor)?; for xpub in all_xpubs { let origin = match xpub.origin { Some(origin) => origin, - None if xpub.xkey.depth == 0 => { - (xpub.root_fingerprint(&self.secp), vec![].into()) - } + None if xpub.xkey.depth == 0 => (xpub.root_fingerprint(secp), vec![].into()), _ => return Err(Error::MissingKeyOrigin(xpub.xkey.to_string())), }; @@ -1490,17 +1592,23 @@ where match utxo { Utxo::Local(utxo) => { - *psbt_input = - match self.get_psbt_input(utxo, params.sighash, params.only_witness_utxo) { - Ok(psbt_input) => psbt_input, - Err(e) => match e { - Error::UnknownUtxo => psbt::Input { - sighash_type: params.sighash, - ..psbt::Input::default() - }, - _ => return Err(e), + let psbt_input_res = Self::_get_psbt_input( + spendable_collection, + secp, + utxo, + params.sighash, + params.only_witness_utxo, + ); + *psbt_input = match psbt_input_res { + Ok(psbt_input) => psbt_input, + Err(e) => match e { + Error::UnknownUtxo => psbt::Input { + sighash_type: params.sighash, + ..psbt::Input::default() }, - } + _ => return Err(e), + }, + } } Utxo::Foreign { psbt_input: foreign_psbt_input, @@ -1526,18 +1634,18 @@ where } // probably redundant but it doesn't hurt... - self.add_input_hd_keypaths(&mut psbt)?; + // self.add_input_hd_keypaths(&mut psbt)?; + Self::_add_input_hd_keypaths(spendable_collection, secp, &mut psbt)?; // add metadata for the outputs for (psbt_output, tx_output) in psbt.outputs.iter_mut().zip(psbt.unsigned_tx.output.iter()) { - if let Some((keychain, child)) = self - .database - .borrow() - .get_path_from_script_pubkey(&tx_output.script_pubkey)? - { - let (desc, _) = self._get_descriptor_for_keychain(keychain); - let derived_descriptor = desc.as_derived(child, &self.secp); + let path_opt = + spendable_collection.get_path_of_script_pubkey(&tx_output.script_pubkey)?; + + if let Some((item, child)) = path_opt { + // let (desc, _) = self._get_descriptor_for_keychain(keychain); + let derived_descriptor = item.descriptor.as_derived(child, secp); if let miniscript::Descriptor::Tr(tr) = &derived_descriptor { let tap_tree = if tr.taptree().is_some() { @@ -1558,12 +1666,12 @@ where psbt_output.tap_tree = tap_tree; psbt_output .tap_key_origins - .append(&mut derived_descriptor.get_tap_key_origins(&self.secp)); + .append(&mut derived_descriptor.get_tap_key_origins(secp)); psbt_output.tap_internal_key = Some(tr.internal_key().to_x_only_pubkey()); } else { psbt_output .bip32_derivation - .append(&mut derived_descriptor.get_hd_keypaths(&self.secp)); + .append(&mut derived_descriptor.get_hd_keypaths(secp)); } if params.include_output_redeem_witness_script { psbt_output.witness_script = derived_descriptor.psbt_witness_script(); @@ -1581,13 +1689,30 @@ where utxo: LocalUtxo, sighash_type: Option, only_witness_utxo: bool, + ) -> Result { + let spendable_collection = self.as_multi_tracker(); + Self::_get_psbt_input( + &spendable_collection, + &self.secp, + utxo, + sighash_type, + only_witness_utxo, + ) + } + + /// TODO @evanlinjin: Move out of [`Wallet`]. + /// * Should this method be renamed to `make_psbt_input`? + fn _get_psbt_input( + spendable_collection: &Sc, + secp: &SecpCtx, + utxo: LocalUtxo, + sighash_type: Option, + only_witness_utxo: bool, ) -> Result { // Try to find the prev_script in our db to figure out if this is internal or external, // and the derivation index - let (keychain, child) = self - .database - .borrow() - .get_path_from_script_pubkey(&utxo.txout.script_pubkey)? + let (item, child) = spendable_collection + .get_path_of_script_pubkey(&utxo.txout.script_pubkey)? .ok_or(Error::UnknownUtxo)?; let mut psbt_input = psbt::Input { @@ -1595,11 +1720,10 @@ where ..psbt::Input::default() }; - let desc = self.get_descriptor_for_keychain(keychain); - let derived_descriptor = desc.as_derived(child, &self.secp); + let derived_descriptor = item.descriptor.as_derived(child, secp); if let miniscript::Descriptor::Tr(tr) = &derived_descriptor { - psbt_input.tap_key_origins = derived_descriptor.get_tap_key_origins(&self.secp); + psbt_input.tap_key_origins = derived_descriptor.get_tap_key_origins(secp); psbt_input.tap_internal_key = Some(tr.internal_key().to_x_only_pubkey()); let spend_info = tr.spend_info(); @@ -1614,61 +1738,78 @@ where }) .collect(); } else { - psbt_input.bip32_derivation = derived_descriptor.get_hd_keypaths(&self.secp); + psbt_input.bip32_derivation = derived_descriptor.get_hd_keypaths(secp); } psbt_input.redeem_script = derived_descriptor.psbt_redeem_script(); psbt_input.witness_script = derived_descriptor.psbt_witness_script(); let prev_output = utxo.outpoint; - if let Some(prev_tx) = self.database.borrow().get_raw_tx(&prev_output.txid)? { - if desc.is_witness() || desc.is_taproot() { - psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone()); + let prev_tx_opt = spendable_collection.get_tx(&prev_output.txid)?; + // if let Some(prev_tx) = self.database.borrow().get_raw_tx(&prev_output.txid)? { + if let Some(prev_tx) = prev_tx_opt { + if item.descriptor.is_witness() || item.descriptor.is_taproot() { + psbt_input.witness_utxo = + Some(prev_tx.raw.output[prev_output.vout as usize].clone()); } - if !desc.is_taproot() && (!desc.is_witness() || !only_witness_utxo) { - psbt_input.non_witness_utxo = Some(prev_tx); + if !item.descriptor.is_taproot() + && (!item.descriptor.is_witness() || !only_witness_utxo) + { + psbt_input.non_witness_utxo = Some(prev_tx.raw); } } Ok(psbt_input) } - fn add_input_hd_keypaths( - &self, + /// TODO @evanlinjin: Move out of [`Wallet`]. + fn _add_input_hd_keypaths<'a, Sc>( + spendable_collection: &'a Sc, + secp: &SecpCtx, psbt: &mut psbt::PartiallySignedTransaction, - ) -> Result<(), Error> { - let mut input_utxos = Vec::with_capacity(psbt.inputs.len()); - for n in 0..psbt.inputs.len() { - input_utxos.push(psbt.get_utxo_for(n).clone()); - } + ) -> Result<(), Error> + where + Sc: MultiTracker, + { + let input_utxos = (0..psbt.inputs.len()) + .map(|i| psbt.get_utxo_for(i)) + .collect::>(); + + psbt.inputs + .iter_mut() + .zip(input_utxos.iter()) + .try_for_each(|(psbt_input, input_utxo)| { + // obtain previous output's script pubkey (if any) + let prev_script_pubkey = match input_utxo { + Some(input_utxo) => &input_utxo.script_pubkey, + None => return Ok(()), + }; - // try to add hd_keypaths if we've already seen the output - for (psbt_input, out) in psbt.inputs.iter_mut().zip(input_utxos.iter()) { - if let Some(out) = out { - if let Some((keychain, child)) = self - .database - .borrow() - .get_path_from_script_pubkey(&out.script_pubkey)? - { - debug!("Found descriptor {:?}/{}", keychain, child); - - // merge hd_keypaths or tap_key_origins - let desc = self.get_descriptor_for_keychain(keychain); - if desc.is_taproot() { - let mut tap_key_origins = desc - .as_derived(child, &self.secp) - .get_tap_key_origins(&self.secp); - psbt_input.tap_key_origins.append(&mut tap_key_origins); - } else { - let mut hd_keypaths = desc - .as_derived(child, &self.secp) - .get_hd_keypaths(&self.secp); - psbt_input.bip32_derivation.append(&mut hd_keypaths); - } + // obtain parent descriptor + child index of script_pubkey (if any) + let (parent, child) = + match spendable_collection.get_path_of_script_pubkey(prev_script_pubkey)? { + Some(path) => path, + None => return Ok(()), + }; + + debug!("Found descriptor {} /{}", parent.descriptor, child); + + // merge hd_keypaths or tap_keys_origins + if parent.descriptor.is_taproot() { + let tap_key_origins = &mut parent + .descriptor + .as_derived(child, secp) + .get_tap_key_origins(secp); + psbt_input.tap_key_origins.append(tap_key_origins); + } else { + let hd_keypaths = &mut parent + .descriptor + .as_derived(child, secp) + .get_hd_keypaths(secp); + psbt_input.bip32_derivation.append(hd_keypaths); } - } - } - Ok(()) + Ok(()) + }) } /// Return an immutable reference to the internal database @@ -1804,6 +1945,7 @@ pub(crate) mod test { use crate::database::Database; use crate::types::KeychainKind; + use crate::wallet::multi_tracker::MultiTrackerInner; use super::*; use crate::signer::{SignOptions, SignerError}; @@ -2078,8 +2220,10 @@ pub(crate) mod test { fn test_create_tx_fee_sniping_locktime_last_sync() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_address(New).unwrap(); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), 25_000); + + // @evanlinjin: Setting sync time has been moved to before the `wallet.build_tx()` call. + // This is because the "fallback nLockTime" is now determined in the `wallet.build_tx()` + // step (moved from the `builder.finish()` step). let sync_time = SyncTime { block_time: BlockTime { height: 25, @@ -2091,6 +2235,10 @@ pub(crate) mod test { .borrow_mut() .set_sync_time(sync_time.clone()) .unwrap(); + + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (psbt, _) = builder.finish().unwrap(); // If there's no current_height we're left with using the last sync height @@ -2248,7 +2396,7 @@ pub(crate) mod test { let mut builder = wallet.build_tx(); builder .add_recipient(addr.script_pubkey(), 25_000) - .do_not_spend_change(); + .only_spend_change(); builder.finish().unwrap(); } @@ -2298,9 +2446,10 @@ pub(crate) mod test { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_address(New).unwrap(); let utxos: Vec<_> = wallet - .get_available_utxos() + .as_multi_tracker() + .iter_unspent() .unwrap() - .into_iter() + // .into_iter() .map(|(u, _)| u.outpoint) .collect(); let mut builder = wallet.build_tx(); @@ -2715,14 +2864,17 @@ pub(crate) mod test { } #[test] - #[should_panic(expected = "SpendingPolicyRequired(External)")] fn test_create_tx_policy_path_required() { let (wallet, _, _) = get_funded_wallet(get_test_a_or_b_plus_csv()); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 30_000); - builder.finish().unwrap(); + + match builder.finish().unwrap_err() { + Error::SpendingPolicyRequired(desc) => assert_eq!(desc.as_ref(), &wallet.descriptor), + err => panic!("unexpected error type: {}", err), + }; } #[test] @@ -2738,7 +2890,7 @@ pub(crate) mod test { let mut builder = wallet.build_tx(); builder .add_recipient(addr.script_pubkey(), 30_000) - .policy_path(path, KeychainKind::External); + .policy_path(path, &wallet.descriptor); let (psbt, _) = builder.finish().unwrap(); assert_eq!(psbt.unsigned_tx.input[0].sequence, 0xFFFFFFFF); @@ -2757,7 +2909,7 @@ pub(crate) mod test { let mut builder = wallet.build_tx(); builder .add_recipient(addr.script_pubkey(), 30_000) - .policy_path(path, KeychainKind::External); + .policy_path(path, &wallet.descriptor); let (psbt, _) = builder.finish().unwrap(); assert_eq!(psbt.unsigned_tx.input[0].sequence, 144); @@ -2937,11 +3089,15 @@ pub(crate) mod test { .max_satisfaction_weight() .unwrap(); - let mut builder = wallet1.build_tx(); - builder.add_recipient(addr.script_pubkey(), 60_000); + let make_builder = || { + let mut builder = wallet1.build_tx(); + builder.add_recipient(addr.script_pubkey(), 60_000); + builder + }; { - let mut builder = builder.clone(); + // let mut builder = builder.clone(); + let mut builder = make_builder(); let psbt_input = psbt::Input { witness_utxo: Some(utxo2.txout.clone()), ..Default::default() @@ -2956,7 +3112,7 @@ pub(crate) mod test { } { - let mut builder = builder.clone(); + let mut builder = make_builder(); let psbt_input = psbt::Input { witness_utxo: Some(utxo2.txout.clone()), ..Default::default() @@ -2972,7 +3128,8 @@ pub(crate) mod test { } { - let mut builder = builder.clone(); + // let mut builder = builder.clone(); + let mut builder = make_builder(); let tx2 = wallet2 .database .borrow() @@ -4546,7 +4703,7 @@ pub(crate) mod test { let mut builder = wallet.build_tx(); builder .add_recipient(addr.script_pubkey(), 25_000) - .policy_path(path, KeychainKind::External); + .policy_path(path, &wallet.descriptor); let (psbt, _) = builder.finish().unwrap(); assert_eq!( @@ -4891,6 +5048,10 @@ pub(crate) mod test { ) .unwrap(); + wallet + .cache_addresses(KeychainKind::External, 0, 5) + .unwrap(); + let confirmation_time = 5; crate::populate_test_db!( diff --git a/src/wallet/multi_tracker.rs b/src/wallet/multi_tracker.rs new file mode 100644 index 000000000..c330d8260 --- /dev/null +++ b/src/wallet/multi_tracker.rs @@ -0,0 +1,416 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2022 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +///! MultiTracker +/// +/// This module defines the [`MultiTracker`] trait. +use std::{cell::RefCell, collections::HashMap, sync::Arc}; + +use bitcoin::{Network, OutPoint, Script, Transaction, TxOut, Txid}; +use miniscript::DescriptorTrait; + +#[allow(deprecated)] +use crate::{ + address_validator::AddressValidator, + database::{BatchDatabase, DatabaseUtils}, + descriptor::{AsDerived, ExtendedDescriptor}, + signer::{SignersContainer, TransactionSigner}, + BlockTime, Error, KeychainKind, LocalUtxo, Wallet, +}; + +use super::{utils::SecpCtx, AddressInfo}; + +/// Contains a descriptor and associated data +#[derive(Debug, Clone)] +pub struct DescriptorItem { + /// Wallet descriptor + pub descriptor: ExtendedDescriptor, + /// Keychain kind of descriptor - external/internal + pub keychain: KeychainKind, + /// Signers of descriptor + pub signers: Arc, +} + +/// Contains a transaction and it's associated data +#[derive(Debug, Clone)] +pub struct TransactionItem { + /// Raw transaction + pub raw: Transaction, + /// Confirmed time (if any) of the transaction + pub confirmed: Option, + /// Fees of the transaction (if avaliable) + pub fees: Option, +} + +/// Contains a local tx output and it's associated data +/// TODO @evanlinjin: Figure out if this is useful +#[derive(Debug, Clone)] +pub struct LocalOutputItem { + /// References this output + pub outpoint: OutPoint, + /// The actual tx output + pub txout: TxOut, + /// Confirmation time (if any) of the tx that contains this output + pub confirmed: Option, + /// Path for `scriptPubKey` creation (descriptor_item, child_index) + pub script_path: (DescriptorItem, u32), + /// Satisfaction weight for the `scriptPubKey` (required weight of corresponding + /// `scriptSig`/`witness` data) + pub satisfaction_weight: u32, +} + +/// Contains the "required" methods of [`MultiTracker`], where all other methods could be +/// deried from (albiet probably not in an optimised manner). +/// +/// TODO @evanlinjin: Should we have an `iter_txout` method as defined in `bdk_core`? +/// +pub trait MultiTrackerInner { + /// Returns an iterator that ranges through all owned descriptors. + fn iter_descriptors(&self) -> Result + '_>, Error>; + + /// Returns an iterator that ranges through all owned signers. + fn iter_signers( + &self, + ) -> Result> + '_>, Error>; + + /// Obtains owned UTXOs alongside their satisfaction weights. + /// + /// TODO @evanlinjin: We should have our own structure for Unspents. + /// Fields that make sense => + /// * Confirmed height. + /// * Descriptor + Child index. + /// * Satisfaction weight of scriptPubKey (weight of corresponding scriptSig/witness data). + fn iter_unspent(&self) -> Result + '_>, Error>; + + /// Obtains transaction (and details) of given txid. + /// + /// Internally, we should include atleast all transactions containing owned and spendable UTXOs + /// of the given descriptors (internal and external). + fn get_tx(&self, txid: &Txid) -> Result, Error>; + + /// Obtains the latest block height of internal trackers. + fn latest_blockheight(&self) -> Result, Error>; + + /// Returns a fresh/unused address derived from given descriptor. This is currently used for + /// obtaining a change/drain address. + #[deprecated(note = "Change selection should be in it's separate step")] + fn new_address(&self, desc: &ExtendedDescriptor) -> Result; +} + +/// Represents a collection of owned and spendable `UTXO`s, `ExtendedDescriptor`s and associated +/// `Transaction`s. +pub trait MultiTracker: MultiTrackerInner { + /// Obtains parent [`ExtendedDescriptor`] and child index of the provided `ScriptPubKey`. + /// Note that if the script is not owned, None shoud be returned. + /// + /// The default implementation of this method is very inefficient and should be overloaded. + fn get_path_of_script_pubkey( + &self, + script: &Script, + ) -> Result, Error> { + let descriptors = self.iter_descriptors()?.collect::>(); + + let secp = SecpCtx::new(); + + for index in 0..200_u32 { + for item in &descriptors { + let desc = item.descriptor.clone(); + let derived_script = desc.as_derived(index, &secp).script_pubkey(); + if script == &derived_script { + return Ok(Some((item.clone(), index))); + } + } + } + + Ok(None) + } + + /// Obtain a local UTXO given the provided outpoint. + /// The default implementation is very inefficient and should be overloaded. + fn get_utxo(&self, outpoint: &OutPoint) -> Result, Error> { + Ok(self.iter_unspent()?.find_map(|(utxo, _)| { + if &utxo.outpoint == outpoint { + Some(utxo) + } else { + None + } + })) + } + + /// Obtain output of the provided outpoint. + /// Output may not be owned by us, just be part of a transaction that we are keeping track of. + fn get_output(&self, outpoint: &OutPoint) -> Result, Error> { + match self.get_tx(&outpoint.txid)? { + // return error if vout is invalid + Some(tx) => tx.raw.output.get(outpoint.vout as usize).map_or_else( + || Err(Error::InvalidOutpoint(*outpoint)), + |tx_out| Ok(Some(tx_out.clone())), + ), + None => Ok(None), + } + } + + /// Returns whether given script is owned by us, or not. + fn is_mine(&self, script: &Script) -> Result { + self.get_path_of_script_pubkey(script) + .map(|path| path.is_some()) + } + + /// Obtains a new change address without a descriptor provided explicitly + #[deprecated] + fn new_auto_address(&self) -> Result { + let (internal, external): (Vec<_>, Vec<_>) = self + .iter_descriptors()? + .partition(|item| item.keychain == KeychainKind::Internal); + + // This should obtain the internal descriptor (if exists), all obtains the external + // descriptor as fallback. + let item = internal.iter().chain(external.iter()).next().unwrap(); + + #[allow(deprecated)] + self.new_address(&item.descriptor) + } +} + +/// Implements [`MultiTracker`] with one external descriptor and an optional internal descriptor. +// #[derive(Clone)] +pub struct LegacyTracker<'a, D> { + descriptor: &'a ExtendedDescriptor, + change_descriptor: Option<&'a ExtendedDescriptor>, + network: Network, + secp: SecpCtx, + + pub(crate) db: &'a RefCell, + pub(crate) signers: Vec>, // [external, internal] + #[allow(deprecated)] + pub(crate) address_validators: Vec>, +} + +impl<'a, D: BatchDatabase> Clone for LegacyTracker<'a, D> { + fn clone(&self) -> Self { + Self { + descriptor: self.descriptor, + change_descriptor: self.change_descriptor, + network: self.network, + secp: self.secp.clone(), + db: self.db, + signers: self.signers.clone(), + address_validators: self.address_validators.clone(), + } + } +} + +/// [`MultiTracker`] descriptor iterator implementation for [`LegacyTracker`]. +pub struct LegacyDescIter<'a, D>(LegacyTracker<'a, D>, usize); + +impl<'a, D> Iterator for LegacyDescIter<'a, D> { + type Item = DescriptorItem; + + fn next(&mut self) -> Option { + self.1 += 1; + + match self.1 { + 1 => Some(DescriptorItem { + descriptor: self.0.descriptor.clone(), + keychain: KeychainKind::External, + signers: Arc::clone(&self.0.signers[0]), + }), + 2 => self.0.change_descriptor.map(|change_desc| DescriptorItem { + descriptor: change_desc.clone(), + keychain: KeychainKind::Internal, + signers: Arc::clone(&self.0.signers[1]), + }), + _ => None, + } + } +} + +impl<'a, D> ExactSizeIterator for LegacyDescIter<'a, D> { + fn len(&self) -> usize { + let start_count = if self.0.change_descriptor.is_some() { + 2 + } else { + 1 + }; + start_count - self.1 + } +} + +impl<'a, D: BatchDatabase> LegacyTracker<'a, D> { + /// Creates a new [`LegacyTracker`]. + #[allow(deprecated)] + pub fn new( + descriptor: &'a ExtendedDescriptor, + change_descriptor: Option<&'a ExtendedDescriptor>, + network: Network, + db: &'a RefCell, + signers: Vec>, + address_validators: Vec>, + ) -> Self { + let secp = SecpCtx::new(); + Self { + descriptor, + change_descriptor, + network, + secp, + db, + signers, + address_validators, + } + } +} + +impl<'a, D: BatchDatabase> MultiTrackerInner for LegacyTracker<'a, D> { + fn iter_descriptors(&self) -> Result + '_>, Error> { + Ok(Box::new(LegacyDescIter::<'_>(self.clone(), 0))) + } + + fn iter_signers( + &self, + ) -> Result> + '_>, Error> { + let signers = self + .signers + .iter() + .flat_map(|cont| cont.signers()) + .map(Arc::clone); + + Ok(Box::new(signers)) + } + + /// TODO @evanlinjin: + /// With the old implementation, we can call `get_descriptor_from_keychain` to obtain the + /// parent descriptor without relying on a "cache" of relationships between `ScriptPubKey`s + /// and "paths". + /// However, we cannot continue to use this approach since we need to generalize everything + /// to support multiple descriptors. + /// A possible solution would be to replace usage of [`KeychainKind`] with + /// `(KeychainKind, u32)`, so descriptors are referenced with an additional index. + /// + /// For now, we need to ensure the aforementioned relationship is sufficiently cached to + /// avoid "missing" available UTXOs. + fn iter_unspent(&self) -> Result + '_>, Error> { + let utxos = self + .db + .borrow() + .iter_utxos()? + .into_iter() + .filter(|utxo| !utxo.is_spent) + .filter_map(move |utxo| { + let (item, _) = self + // @evanlinjin: Will panic with default implementation on timeout. + .get_path_of_script_pubkey(&utxo.txout.script_pubkey) + .unwrap()?; + let weight = item.descriptor.max_satisfaction_weight().unwrap(); + Some((utxo, weight)) + }); + + Ok(Box::new(utxos)) + } + + fn get_tx(&self, txid: &Txid) -> Result, Error> { + Ok(self + .db + .borrow() + .get_tx(txid, true)? + .map(|details| TransactionItem { + raw: details.transaction.unwrap(), + confirmed: details.confirmation_time, + fees: details.fee, + })) + } + + fn latest_blockheight(&self) -> Result, Error> { + self.db + .borrow() + .get_sync_time() + .map(|opt| opt.map(|sync_time| sync_time.block_time)) + } + + fn new_address(&self, desc: &ExtendedDescriptor) -> Result { + let keychain = if desc == self.descriptor { + KeychainKind::External + } else if self.change_descriptor == Some(desc) { + KeychainKind::Internal + } else { + return Err(Error::Generic("descriptor does not exist".to_string())); + }; + + Wallet::_get_new_address( + self.db, + &self.secp, + self.address_validators.clone(), + desc, + keychain, + self.network, + ) + } +} + +impl<'a, D: BatchDatabase> MultiTracker for LegacyTracker<'a, D> { + fn get_utxo(&self, outpoint: &OutPoint) -> Result, Error> { + self.db.borrow().get_utxo(outpoint) + } + + fn get_path_of_script_pubkey( + &self, + script: &Script, + ) -> Result, Error> { + // TODO: Add as struct field + let desc_map = self + .iter_descriptors()? + .map(|desc| (desc.descriptor.to_string(), desc)) + .collect::>(); + + // check internal cache for relation + let cached_res = + self.db + .borrow() + .get_path_from_script_pubkey(script)? + .map(|(keychain, child_ind)| { + let desc = match keychain { + KeychainKind::External => self.descriptor, + KeychainKind::Internal => self.change_descriptor.unwrap(), + }; + let item = desc_map.get(&desc.to_string()).unwrap(); + (item.clone(), child_ind) + }); + + if cached_res.is_some() { + return Ok(cached_res); + } + + // try brute-force method + let db = self.db.borrow(); + let last_ext = db.get_last_index(KeychainKind::External)?.unwrap_or(0_u32); + let last_int = db.get_last_index(KeychainKind::Internal)?.unwrap_or(0_u32); + let start = std::cmp::min(last_ext, last_int); + + let descriptors = self.iter_descriptors()?.collect::>(); + + for index in start..start + 200_u32 { + for item in &descriptors { + let derived_script = item + .descriptor + .as_derived(index, &self.secp) + .script_pubkey(); + if script == &derived_script { + return Ok(Some((item.clone(), index))); + } + } + } + + Ok(None) + } + + fn is_mine(&self, script: &Script) -> Result { + self.db.borrow().is_mine(script) + } +} diff --git a/src/wallet/tx_builder.rs b/src/wallet/tx_builder.rs index 0fee8aa9e..9e137fb02 100644 --- a/src/wallet/tx_builder.rs +++ b/src/wallet/tx_builder.rs @@ -42,16 +42,22 @@ use std::default::Default; use std::marker::PhantomData; use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt}; +use bitcoin::Txid; use bitcoin::{OutPoint, Script, Transaction}; - -use miniscript::descriptor::DescriptorTrait; +use miniscript::DescriptorTrait; use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm}; -use crate::{database::BatchDatabase, Error, Utxo, Wallet}; +use super::multi_tracker::MultiTracker; +use super::utils::SecpCtx; +use crate::database::MemoryDatabase; +use crate::descriptor::ExtendedDescriptor; +use crate::SignOptions; +use crate::Wallet; use crate::{ types::{FeeRate, KeychainKind, LocalUtxo, WeightedUtxo}, TransactionDetails, }; +use crate::{Error, Utxo}; /// Context in which the [`TxBuilder`] is valid pub trait TxBuilderContext: std::fmt::Debug + Default + Clone {} @@ -117,23 +123,24 @@ impl TxBuilderContext for BumpFee {} /// [`finish`]: Self::finish /// [`coin_selection`]: Self::coin_selection #[derive(Debug)] -pub struct TxBuilder<'a, D, Cs, Ctx> { - pub(crate) wallet: &'a Wallet, - pub(crate) params: TxParams, +pub struct TxBuilder<'a, Mt, Cs, Ctx> { + pub(crate) multi_tracker: Mt, + pub(crate) params: TxParams<'a>, pub(crate) coin_selection: Cs, + pub(crate) secp: SecpCtx, pub(crate) phantom: PhantomData, } /// The parameters for transaction creation sans coin selection algorithm. //TODO: TxParams should eventually be exposed publicly. #[derive(Default, Debug, Clone)] -pub(crate) struct TxParams { +pub(crate) struct TxParams<'a> { pub(crate) recipients: Vec<(Script, u64)>, pub(crate) drain_wallet: bool, pub(crate) drain_to: Option