From 7528f6c9e9886ec0936bd2e2d6900caf21f466d0 Mon Sep 17 00:00:00 2001 From: nickfarrow Date: Thu, 17 Feb 2022 02:22:20 +1100 Subject: [PATCH 1/5] AddressIndex::LastUnused look back further than current_index Signed-off-by: nickfarrow --- src/wallet/mod.rs | 93 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 27 deletions(-) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 75669559c..67e523144 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -30,6 +30,7 @@ use bitcoin::util::psbt::Input; use bitcoin::util::psbt::PartiallySignedTransaction as Psbt; use bitcoin::{Address, Network, OutPoint, Script, SigHashType, Transaction, TxOut, Txid}; +use miniscript::descriptor::Descriptor; use miniscript::descriptor::DescriptorTrait; use miniscript::psbt::PsbtInputSatisfier; @@ -61,9 +62,9 @@ use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils, SyncTime}; 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, DerivedDescriptor, DerivedDescriptorKey, + DerivedDescriptorMeta, DescriptorMeta, DescriptorScripts, ExtendedDescriptor, ExtractPolicy, + IntoWalletDescriptor, Policy, XKeyUtils, }; use crate::error::Error; use crate::psbt::PsbtUtils; @@ -253,35 +254,73 @@ where .map_err(|_| Error::ScriptDoesntHaveAddressForm) } - // Return the the last previously derived address for `keychain` if it has not been used in a - // received transaction. Otherwise return a new address using [`Wallet::get_new_address`]. - fn get_unused_address(&self, keychain: KeychainKind) -> Result { + // Return vector of unused address indexes + fn get_unused_key_indexes(&self, keychain: KeychainKind) -> Result, Error> { let current_index = self.fetch_index(keychain)?; - let derived_key = self - .get_descriptor_for_keychain(keychain) - .as_derived(current_index, &self.secp); - - let script_pubkey = derived_key.script_pubkey(); + let derived_keys: Vec> = (0..current_index + 1) + .map(|i| { + self.get_descriptor_for_keychain(keychain) + .as_derived(i, &self.secp) + }) + .collect(); - let found_used = self - .list_transactions(true)? + let unused_key_indexes: Vec = derived_keys .iter() - .flat_map(|tx_details| tx_details.transaction.as_ref()) - .flat_map(|tx| tx.output.iter()) - .any(|o| o.script_pubkey == script_pubkey); + .cloned() + .enumerate() + .filter_map(|(i, key)| { + if !self + .list_transactions(true) + .expect("getting transaction list") + .iter() + .flat_map(|tx_details| tx_details.transaction.as_ref()) + .flat_map(|tx| tx.output.iter()) + .any(|o| o.script_pubkey == key.script_pubkey()) + { + Some(i as u32) + } else { + None + } + }) + .collect(); - if found_used { - self.get_new_address(keychain) - } else { - derived_key - .address(self.network) - .map(|address| AddressInfo { - address, - index: current_index, - keychain, - }) - .map_err(|_| Error::ScriptDoesntHaveAddressForm) + Ok(unused_key_indexes) + } + + // Return the the last previously derived address for `keychain` if it has not been used in a + // received transaction. Otherwise return a new address using [`Wallet::get_new_address`]. + fn get_unused_address(&self, keychain: KeychainKind) -> Result { + let mut unused_key_indexes = self.get_unused_key_indexes(keychain)?; + match unused_key_indexes.pop() { + None => self.get_new_address(keychain), + Some(address_index) => { + let derived_key = self + .get_descriptor_for_keychain(keychain) + .as_derived(address_index, &self.secp); + + let script_pubkey = derived_key.script_pubkey(); + + let found_used = self + .list_transactions(true)? + .iter() + .flat_map(|tx_details| tx_details.transaction.as_ref()) + .flat_map(|tx| tx.output.iter()) + .any(|o| o.script_pubkey == script_pubkey); + + if found_used { + self.get_new_address(keychain) + } else { + derived_key + .address(self.network) + .map(|address| AddressInfo { + address, + index: address_index, + keychain, + }) + .map_err(|_| Error::ScriptDoesntHaveAddressForm) + } + } } } From ef793e4ed3282992e5b6d8504b8a0e1ae148004f Mon Sep 17 00:00:00 2001 From: nickfarrow Date: Thu, 17 Feb 2022 02:25:56 +1100 Subject: [PATCH 2/5] add AddressIndex::FirstUnused Signed-off-by: nickfarrow --- src/wallet/mod.rs | 65 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 67e523144..8d56c3c6d 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -116,6 +116,13 @@ pub enum AddressIndex { /// caller is untrusted; for example when deriving donation addresses on-demand for a public /// web page. LastUnused, + /// Return the address for the first address in the keychain that has not been used in a received + /// transaction. Otherwise return a new address as with [`AddressIndex::New`]. + /// + /// Use with caution, if the wallet has not yet detected an address has been used it could + /// return an already used address. This function is primarily meant for making use of addresses earlier + /// in the keychain that were infact never used. + FirstUnused, /// Return the address for a specific descriptor index. Does not change the current descriptor /// index used by `AddressIndex::New` and `AddressIndex::LastUsed`. /// @@ -290,7 +297,7 @@ where // Return the the last previously derived address for `keychain` if it has not been used in a // received transaction. Otherwise return a new address using [`Wallet::get_new_address`]. - fn get_unused_address(&self, keychain: KeychainKind) -> Result { + fn get_last_unused_address(&self, keychain: KeychainKind) -> Result { let mut unused_key_indexes = self.get_unused_key_indexes(keychain)?; match unused_key_indexes.pop() { None => self.get_new_address(keychain), @@ -324,6 +331,27 @@ where } } + // Return the the first address in the keychain which has not been a recipient of a transaction + fn get_first_unused_address(&self, keychain: KeychainKind) -> Result { + let unused_key_indexes = self.get_unused_key_indexes(keychain)?; + if unused_key_indexes.is_empty() { + self.get_new_address(keychain) + } else { + let derived_key = self + .get_descriptor_for_keychain(keychain) + .as_derived(unused_key_indexes[0], &self.secp); + + derived_key + .address(self.network) + .map(|address| AddressInfo { + address, + index: unused_key_indexes[0], + keychain, + }) + .map_err(|_| Error::ScriptDoesntHaveAddressForm) + } + } + // Return derived address for the descriptor of given [`KeychainKind`] at a specific index fn peek_address(&self, index: u32, keychain: KeychainKind) -> Result { self.get_descriptor_for_keychain(keychain) @@ -378,7 +406,8 @@ where ) -> Result { match address_index { AddressIndex::New => self.get_new_address(keychain), - AddressIndex::LastUnused => self.get_unused_address(keychain), + AddressIndex::LastUnused => self.get_last_unused_address(keychain), + AddressIndex::FirstUnused => self.get_first_unused_address(keychain), AddressIndex::Peek(index) => self.peek_address(index, keychain), AddressIndex::Reset(index) => self.reset_address(index, keychain), } @@ -1703,7 +1732,7 @@ pub(crate) mod test { use super::*; use crate::signer::{SignOptions, SignerError}; - use crate::wallet::AddressIndex::{LastUnused, New, Peek, Reset}; + use crate::wallet::AddressIndex::{FirstUnused, LastUnused, New, Peek, Reset}; #[test] fn test_cache_addresses_fixed() { @@ -3911,6 +3940,36 @@ pub(crate) mod test { ); } + #[test] + fn test_firstunused_address() { + let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; + let descriptors = testutils!(@descriptors (descriptor)); + let wallet = Wallet::new( + &descriptors.0, + None, + Network::Testnet, + MemoryDatabase::new(), + ) + .unwrap(); + + assert_eq!( + wallet.get_address(FirstUnused).unwrap().to_string(), + "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" + ); + + // use the first address + crate::populate_test_db!( + wallet.database.borrow_mut(), + testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)), + Some(100), + ); + + assert_eq!( + wallet.get_address(FirstUnused).unwrap().to_string(), + "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" + ); + } + #[test] fn test_peek_address_at_index() { let db = MemoryDatabase::new(); From 0a8e36863569549ba07684c7a96b03d9464321d1 Mon Sep 17 00:00:00 2001 From: nickfarrow Date: Sun, 10 Apr 2022 17:06:13 +1000 Subject: [PATCH 3/5] add get_batch_unused_addresses Signed-off-by: nickfarrow --- CHANGELOG.md | 3 ++ src/wallet/mod.rs | 84 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cb9f31c3..87ed4a390 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `get_internal_address` to allow you to get internal addresses just as you get external addresses. - added `ensure_addresses_cached` to `Wallet` to let offline wallets load and cache addresses in their database - Add `is_spent` field to `LocalUtxo`; when we notice that a utxo has been spent we set `is_spent` field to true instead of deleting it from the db. +- Changed `AddressIndex::LastUnused` to look back further than `current_index`, and only return a new address if all have been used. +- Add `AddressIndex::FirstUnused` to get unused addresses from the beginning of the keychain. +- Add `wallet.get_batch_unused_addresses` to return vector of N unused addresses, populating any remaining with new addresses. ### Sync API change diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 8d56c3c6d..6b2878877 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -457,6 +457,39 @@ where Ok(new_addresses_cached) } + /// Return vector of n unused addresses from the [`KeychainKind`]. + /// If less than n unused addresses are returned, the rest will be populated by new addresses. + /// The unused addresses returned are in order of oldest in keychain first, with increasing index. + pub fn get_batch_unused_addresses( + &self, + n: usize, + keychain: KeychainKind, + ) -> Result, Error> { + let unused_key_indexes = self.get_unused_key_indexes(keychain)?; + let mut addresses = unused_key_indexes + .iter() + .map(|i| { + let derived_key = self + .get_descriptor_for_keychain(keychain) + .as_derived(*i, &self.secp); + + derived_key + .address(self.network) + .map(|address| AddressInfo { + address, + index: *i, + keychain, + }) + .map_err(|_| Error::ScriptDoesntHaveAddressForm) + }) + .take(n) + .collect::, _>>()?; + for _ in 0..(n - addresses.len()) { + addresses.push(self.get_new_address(keychain)?) + } + Ok(addresses) + } + /// Return whether or not a `script` is part of this wallet (either internal or external) pub fn is_mine(&self, script: &Script) -> Result { self.database.borrow().is_mine(script) @@ -3970,6 +4003,57 @@ pub(crate) mod test { ); } + #[test] + fn test_batch_unused_addresses() { + let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; + let descriptors = testutils!(@descriptors (descriptor)); + let wallet = Wallet::new( + &descriptors.0, + None, + Network::Testnet, + MemoryDatabase::new(), + ) + .unwrap(); + + // get first two addresses, moving index + for _ in 0..2 { + let _ = wallet.get_address(New); + } + + // use the second address + crate::populate_test_db!( + wallet.database.borrow_mut(), + testutils! (@tx ( (@external descriptors, 1) => 25_000 ) (@confirmations 1)), + Some(100), + ); + + assert_eq!( + wallet + .get_batch_unused_addresses(3, KeychainKind::External) + .unwrap(), + vec![ + AddressInfo { + index: 0, + address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a") + .unwrap(), + keychain: KeychainKind::External, + }, + AddressInfo { + index: 2, + address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2") + .unwrap(), + keychain: KeychainKind::External, + }, + AddressInfo { + index: 3, + address: Address::from_str("tb1q32a23q6u3yy89l8svrt80a54h06qvn7gnuvsen") + .unwrap(), + keychain: KeychainKind::External, + } + ] + ); + } + #[test] fn test_peek_address_at_index() { let db = MemoryDatabase::new(); From 0adc52a78a3c15c9a35284b955a22432b48ef7b8 Mon Sep 17 00:00:00 2001 From: nickfarrow Date: Mon, 2 May 2022 21:13:48 +1000 Subject: [PATCH 4/5] PR feedback * get_batch_unused_addresses loops through address indexes `from_front = true` (for firstUnused) or `false` (for lastUnused). * Relies on database having up to date script_pubkeys in such a manner that script_pks.len() == self.fetch_index(keychain) * 1 script pubkey per address index? * Must work with current_address_index = 0 Signed-off-by: nickfarrow --- src/wallet/mod.rs | 167 +++++++++++++++++----------------------------- 1 file changed, 60 insertions(+), 107 deletions(-) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 6b2878877..914a65c58 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -30,7 +30,6 @@ use bitcoin::util::psbt::Input; use bitcoin::util::psbt::PartiallySignedTransaction as Psbt; use bitcoin::{Address, Network, OutPoint, Script, SigHashType, Transaction, TxOut, Txid}; -use miniscript::descriptor::Descriptor; use miniscript::descriptor::DescriptorTrait; use miniscript::psbt::PsbtInputSatisfier; @@ -62,9 +61,9 @@ use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils, SyncTime}; use crate::descriptor::derived::AsDerived; use crate::descriptor::policy::BuildSatisfaction; use crate::descriptor::{ - get_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DerivedDescriptorKey, - DerivedDescriptorMeta, DescriptorMeta, DescriptorScripts, ExtendedDescriptor, ExtractPolicy, - IntoWalletDescriptor, Policy, XKeyUtils, + get_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DerivedDescriptorMeta, + DescriptorMeta, DescriptorScripts, ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, + Policy, XKeyUtils, }; use crate::error::Error; use crate::psbt::PsbtUtils; @@ -261,95 +260,26 @@ where .map_err(|_| Error::ScriptDoesntHaveAddressForm) } - // Return vector of unused address indexes - fn get_unused_key_indexes(&self, keychain: KeychainKind) -> Result, Error> { - let current_index = self.fetch_index(keychain)?; - - let derived_keys: Vec> = (0..current_index + 1) - .map(|i| { - self.get_descriptor_for_keychain(keychain) - .as_derived(i, &self.secp) - }) - .collect(); - - let unused_key_indexes: Vec = derived_keys - .iter() - .cloned() - .enumerate() - .filter_map(|(i, key)| { - if !self - .list_transactions(true) - .expect("getting transaction list") - .iter() - .flat_map(|tx_details| tx_details.transaction.as_ref()) - .flat_map(|tx| tx.output.iter()) - .any(|o| o.script_pubkey == key.script_pubkey()) - { - Some(i as u32) - } else { - None - } - }) - .collect(); - - Ok(unused_key_indexes) + // Return whether this address has been used in a transaction + fn has_address_been_used(&self, script_pk: &Script) -> bool { + let txns = self.list_transactions(true).unwrap_or_else(|_| vec![]); + txns.iter() + .flat_map(|tx_details| tx_details.transaction.as_ref()) + .flat_map(|tx| tx.output.iter()) + .any(|o| o.script_pubkey == *script_pk) } // Return the the last previously derived address for `keychain` if it has not been used in a // received transaction. Otherwise return a new address using [`Wallet::get_new_address`]. fn get_last_unused_address(&self, keychain: KeychainKind) -> Result { - let mut unused_key_indexes = self.get_unused_key_indexes(keychain)?; - match unused_key_indexes.pop() { - None => self.get_new_address(keychain), - Some(address_index) => { - let derived_key = self - .get_descriptor_for_keychain(keychain) - .as_derived(address_index, &self.secp); - - let script_pubkey = derived_key.script_pubkey(); - - let found_used = self - .list_transactions(true)? - .iter() - .flat_map(|tx_details| tx_details.transaction.as_ref()) - .flat_map(|tx| tx.output.iter()) - .any(|o| o.script_pubkey == script_pubkey); - - if found_used { - self.get_new_address(keychain) - } else { - derived_key - .address(self.network) - .map(|address| AddressInfo { - address, - index: address_index, - keychain, - }) - .map_err(|_| Error::ScriptDoesntHaveAddressForm) - } - } - } + let mut unused_addresses = self.get_batch_unused_addresses(1, false, keychain)?; + Ok(unused_addresses.remove(0)) } // Return the the first address in the keychain which has not been a recipient of a transaction fn get_first_unused_address(&self, keychain: KeychainKind) -> Result { - let unused_key_indexes = self.get_unused_key_indexes(keychain)?; - if unused_key_indexes.is_empty() { - self.get_new_address(keychain) - } else { - let derived_key = self - .get_descriptor_for_keychain(keychain) - .as_derived(unused_key_indexes[0], &self.secp); - - derived_key - .address(self.network) - .map(|address| AddressInfo { - address, - index: unused_key_indexes[0], - keychain, - }) - .map_err(|_| Error::ScriptDoesntHaveAddressForm) - } + let mut unused_addresses = self.get_batch_unused_addresses(1, true, keychain)?; + Ok(unused_addresses.remove(0)) } // Return derived address for the descriptor of given [`KeychainKind`] at a specific index @@ -463,31 +393,54 @@ where pub fn get_batch_unused_addresses( &self, n: usize, + from_front: bool, keychain: KeychainKind, ) -> Result, Error> { - let unused_key_indexes = self.get_unused_key_indexes(keychain)?; - let mut addresses = unused_key_indexes - .iter() - .map(|i| { - let derived_key = self - .get_descriptor_for_keychain(keychain) - .as_derived(*i, &self.secp); - - derived_key - .address(self.network) - .map(|address| AddressInfo { - address, - index: *i, - keychain, - }) - .map_err(|_| Error::ScriptDoesntHaveAddressForm) - }) - .take(n) - .collect::, _>>()?; - for _ in 0..(n - addresses.len()) { - addresses.push(self.get_new_address(keychain)?) + let script_pubkeys = self + .database + .borrow() + .iter_script_pubkeys(Some(keychain)) + .unwrap_or_else(|_| vec![]); + + let current_address_index = self.fetch_index(keychain)? as usize; + let check_indexes = if from_front { + (0..=current_address_index).collect::>() + } else { + (0..=current_address_index).rev().collect::>() + }; + + let mut unused_addresses = vec![]; + for i in check_indexes { + // if we have made a pubkey at this index, check whether the address has been used. + if i < script_pubkeys.len() { + let script_pk = &script_pubkeys[i]; + if self.has_address_been_used(script_pk) { + continue; + } + } + if let Ok(unused_address) = self + .get_descriptor_for_keychain(keychain) + .as_derived(i as u32, &self.secp) + .address(self.network) + .map(|address| AddressInfo { + address, + index: i as u32, + keychain, + }) + .map_err(|_| Error::ScriptDoesntHaveAddressForm) + { + unused_addresses.push(unused_address); + } + + if unused_addresses.len() >= n { + break; + } + } + + for _ in 0..(n - unused_addresses.len()) { + unused_addresses.push(self.get_new_address(keychain)?) } - Ok(addresses) + Ok(unused_addresses) } /// Return whether or not a `script` is part of this wallet (either internal or external) @@ -4029,7 +3982,7 @@ pub(crate) mod test { assert_eq!( wallet - .get_batch_unused_addresses(3, KeychainKind::External) + .get_batch_unused_addresses(3, true, KeychainKind::External) .unwrap(), vec![ AddressInfo { From bcfee8f444fdad1728ee5c1f277e3a1e0a861a00 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 29 Jul 2022 00:58:16 -0500 Subject: [PATCH 5/5] Behave like original LastUnused and get all unused indexes (#1) * remove batch getting `n` unused addresses, just get them all (much simpler) * use next_back() and next() for last and first unused * test more cases for get_unused_address_indexes * inline functions and simplified next addr * create HashSet of txn scripts before checking unused * add firstunused testcase for repeated unused --- src/wallet/mod.rs | 169 +++++++++++++++++++++++----------------------- 1 file changed, 85 insertions(+), 84 deletions(-) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 914a65c58..fc591a6b5 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -260,26 +260,25 @@ where .map_err(|_| Error::ScriptDoesntHaveAddressForm) } - // Return whether this address has been used in a transaction - fn has_address_been_used(&self, script_pk: &Script) -> bool { - let txns = self.list_transactions(true).unwrap_or_else(|_| vec![]); - txns.iter() - .flat_map(|tx_details| tx_details.transaction.as_ref()) - .flat_map(|tx| tx.output.iter()) - .any(|o| o.script_pubkey == *script_pk) - } - // Return the the last previously derived address for `keychain` if it has not been used in a // received transaction. Otherwise return a new address using [`Wallet::get_new_address`]. fn get_last_unused_address(&self, keychain: KeychainKind) -> Result { - let mut unused_addresses = self.get_batch_unused_addresses(1, false, keychain)?; - Ok(unused_addresses.remove(0)) + let unused_script_indexes = self.get_unused_script_indexes(keychain)?; + let current_index = &self.fetch_index(keychain)?; + if unused_script_indexes.contains(current_index) { + self.get_address(AddressIndex::Peek(*current_index)) + } else { + self.get_new_address(keychain) + } } - // Return the the first address in the keychain which has not been a recipient of a transaction + // Return the the first address in the keychain which has not been used in a recieved transaction + // If they have all been used, return a new address using [`Wallet::get_new_address`]. fn get_first_unused_address(&self, keychain: KeychainKind) -> Result { - let mut unused_addresses = self.get_batch_unused_addresses(1, true, keychain)?; - Ok(unused_addresses.remove(0)) + self.get_unused_script_indexes(keychain)? + .get(0) + .map(|index| self.get_address(AddressIndex::Peek(*index))) + .unwrap_or_else(|| self.get_new_address(keychain)) } // Return derived address for the descriptor of given [`KeychainKind`] at a specific index @@ -387,60 +386,39 @@ where Ok(new_addresses_cached) } - /// Return vector of n unused addresses from the [`KeychainKind`]. - /// If less than n unused addresses are returned, the rest will be populated by new addresses. - /// The unused addresses returned are in order of oldest in keychain first, with increasing index. - pub fn get_batch_unused_addresses( - &self, - n: usize, - from_front: bool, - keychain: KeychainKind, - ) -> Result, Error> { + /// Return set of unused script indexes for the [`KeychainKind`]. + pub fn get_unused_script_indexes(&self, keychain: KeychainKind) -> Result, Error> { let script_pubkeys = self .database .borrow() .iter_script_pubkeys(Some(keychain)) .unwrap_or_else(|_| vec![]); - + let txs = self.list_transactions(true).unwrap_or_else(|_| vec![]); + let tx_scripts: HashSet<&Script> = txs + .iter() + .flat_map(|tx_details| tx_details.transaction.as_ref()) + .flat_map(|tx| tx.output.iter()) + .map(|o| &o.script_pubkey) + .collect(); let current_address_index = self.fetch_index(keychain)? as usize; - let check_indexes = if from_front { - (0..=current_address_index).collect::>() - } else { - (0..=current_address_index).rev().collect::>() - }; - let mut unused_addresses = vec![]; - for i in check_indexes { - // if we have made a pubkey at this index, check whether the address has been used. - if i < script_pubkeys.len() { - let script_pk = &script_pubkeys[i]; - if self.has_address_been_used(script_pk) { - continue; + let mut scripts_not_used: Vec = script_pubkeys + .iter() + .take(current_address_index + 1) + .enumerate() + .filter_map(|(i, script_pubkey)| { + if !tx_scripts.contains(script_pubkey) { + Some(i as u32) + } else { + None } - } - if let Ok(unused_address) = self - .get_descriptor_for_keychain(keychain) - .as_derived(i as u32, &self.secp) - .address(self.network) - .map(|address| AddressInfo { - address, - index: i as u32, - keychain, - }) - .map_err(|_| Error::ScriptDoesntHaveAddressForm) - { - unused_addresses.push(unused_address); - } - - if unused_addresses.len() >= n { - break; - } + }) + .collect(); + if script_pubkeys.is_empty() { + scripts_not_used.push(0); } - for _ in 0..(n - unused_addresses.len()) { - unused_addresses.push(self.get_new_address(keychain)?) - } - Ok(unused_addresses) + Ok(scripts_not_used) } /// Return whether or not a `script` is part of this wallet (either internal or external) @@ -3927,7 +3905,7 @@ pub(crate) mod test { } #[test] - fn test_firstunused_address() { + fn test_first_unused_address() { let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; let descriptors = testutils!(@descriptors (descriptor)); let wallet = Wallet::new( @@ -3954,10 +3932,22 @@ pub(crate) mod test { wallet.get_address(FirstUnused).unwrap().to_string(), "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" ); + + // use the third address + crate::populate_test_db!( + wallet.database.borrow_mut(), + testutils! (@tx ( (@external descriptors, 2) => 25_000 ) (@confirmations 1)), + Some(100), + ); + + assert_eq!( + wallet.get_address(FirstUnused).unwrap().to_string(), + "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" + ); } #[test] - fn test_batch_unused_addresses() { + fn test_get_unused_address_indexes() { let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; let descriptors = testutils!(@descriptors (descriptor)); let wallet = Wallet::new( @@ -3968,42 +3958,53 @@ pub(crate) mod test { ) .unwrap(); - // get first two addresses, moving index - for _ in 0..2 { + assert_eq!( + wallet + .get_unused_script_indexes(KeychainKind::External) + .unwrap(), + vec![0] + ); + + // get four more addresses, moving index to five + for _ in 0..4 { let _ = wallet.get_address(New); } + assert_eq!( + wallet + .get_unused_script_indexes(KeychainKind::External) + .unwrap(), + vec![0, 1, 2, 3, 4] + ); - // use the second address + // use the second and fifth address crate::populate_test_db!( wallet.database.borrow_mut(), testutils! (@tx ( (@external descriptors, 1) => 25_000 ) (@confirmations 1)), Some(100), ); + crate::populate_test_db!( + wallet.database.borrow_mut(), + testutils! (@tx ( (@external descriptors, 4) => 25_000 ) (@confirmations 1)), + Some(100), + ); + assert_eq!( + wallet + .get_unused_script_indexes(KeychainKind::External) + .unwrap(), + vec![0, 2, 3] + ); + // use the first address + crate::populate_test_db!( + wallet.database.borrow_mut(), + testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)), + Some(100), + ); assert_eq!( wallet - .get_batch_unused_addresses(3, true, KeychainKind::External) + .get_unused_script_indexes(KeychainKind::External) .unwrap(), - vec![ - AddressInfo { - index: 0, - address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a") - .unwrap(), - keychain: KeychainKind::External, - }, - AddressInfo { - index: 2, - address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2") - .unwrap(), - keychain: KeychainKind::External, - }, - AddressInfo { - index: 3, - address: Address::from_str("tb1q32a23q6u3yy89l8svrt80a54h06qvn7gnuvsen") - .unwrap(), - keychain: KeychainKind::External, - } - ] + vec![2, 3] ); }