diff --git a/crates/hwi/src/lib.rs b/crates/hwi/src/lib.rs index 73a8fc539..9d415b4ba 100644 --- a/crates/hwi/src/lib.rs +++ b/crates/hwi/src/lib.rs @@ -20,7 +20,7 @@ //! //! # let mut wallet = Wallet::new( //! # "", -//! # "", +//! # Some(""), //! # Network::Testnet, //! # )?; //! # diff --git a/crates/wallet/README.md b/crates/wallet/README.md index be780b6c3..f518f64d0 100644 --- a/crates/wallet/README.md +++ b/crates/wallet/README.md @@ -79,9 +79,11 @@ let mut db = let descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)"; let change_descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/1/*)"; let changeset = db.aggregate_changesets().expect("changeset loaded"); -let mut wallet = - Wallet::new_or_load(descriptor, change_descriptor, changeset, Network::Testnet) - .expect("create or load wallet"); +let mut wallet = if let Some(changeset) = changeset { + Wallet::load(changeset).expect("loaded wallet") +} else { + Wallet::new(descriptor, Some(change_descriptor), Network::Testnet).expect("created new wallet") +}; // Get a new address to receive bitcoin. let receive_address = wallet.reveal_next_address(KeychainKind::External); diff --git a/crates/wallet/examples/compiler.rs b/crates/wallet/examples/compiler.rs index 13b905ad9..7fae71dd5 100644 --- a/crates/wallet/examples/compiler.rs +++ b/crates/wallet/examples/compiler.rs @@ -77,7 +77,7 @@ fn main() -> Result<(), Box> { ); // Create a new wallet from descriptors - let mut wallet = Wallet::new(&descriptor, &internal_descriptor, Network::Regtest)?; + let mut wallet = Wallet::new(&descriptor, Some(&internal_descriptor), Network::Regtest)?; println!( "First derived address from the descriptor: \n{}", diff --git a/crates/wallet/src/descriptor/template.rs b/crates/wallet/src/descriptor/template.rs index 3ee346d31..4f858a5ed 100644 --- a/crates/wallet/src/descriptor/template.rs +++ b/crates/wallet/src/descriptor/template.rs @@ -81,7 +81,11 @@ impl IntoWalletDescriptor for T { /// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; /// let key_internal = /// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?; -/// let mut wallet = Wallet::new(P2Pkh(key_external), P2Pkh(key_internal), Network::Testnet)?; +/// let mut wallet = Wallet::new( +/// P2Pkh(key_external), +/// Some(P2Pkh(key_internal)), +/// Network::Testnet, +/// )?; /// /// assert_eq!( /// wallet @@ -115,7 +119,7 @@ impl> DescriptorTemplate for P2Pkh { /// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?; /// let mut wallet = Wallet::new( /// P2Wpkh_P2Sh(key_external), -/// P2Wpkh_P2Sh(key_internal), +/// Some(P2Wpkh_P2Sh(key_internal)), /// Network::Testnet, /// )?; /// @@ -150,7 +154,11 @@ impl> DescriptorTemplate for P2Wpkh_P2Sh { /// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; /// let key_internal = /// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?; -/// let mut wallet = Wallet::new(P2Wpkh(key_external), P2Wpkh(key_internal), Network::Testnet)?; +/// let mut wallet = Wallet::new( +/// P2Wpkh(key_external), +/// Some(P2Wpkh(key_internal)), +/// Network::Testnet, +/// )?; /// /// assert_eq!( /// wallet @@ -182,7 +190,11 @@ impl> DescriptorTemplate for P2Wpkh { /// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; /// let key_internal = /// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?; -/// let mut wallet = Wallet::new(P2TR(key_external), P2TR(key_internal), Network::Testnet)?; +/// let mut wallet = Wallet::new( +/// P2TR(key_external), +/// Some(P2TR(key_internal)), +/// Network::Testnet, +/// )?; /// /// assert_eq!( /// wallet @@ -217,7 +229,7 @@ impl> DescriptorTemplate for P2TR { /// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; /// let mut wallet = Wallet::new( /// Bip44(key.clone(), KeychainKind::External), -/// Bip44(key, KeychainKind::Internal), +/// Some(Bip44(key, KeychainKind::Internal)), /// Network::Testnet, /// )?; /// @@ -254,7 +266,7 @@ impl> DescriptorTemplate for Bip44 { /// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?; /// let mut wallet = Wallet::new( /// Bip44Public(key.clone(), fingerprint, KeychainKind::External), -/// Bip44Public(key, fingerprint, KeychainKind::Internal), +/// Some(Bip44Public(key, fingerprint, KeychainKind::Internal)), /// Network::Testnet, /// )?; /// @@ -290,7 +302,7 @@ impl> DescriptorTemplate for Bip44Public { /// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; /// let mut wallet = Wallet::new( /// Bip49(key.clone(), KeychainKind::External), -/// Bip49(key, KeychainKind::Internal), +/// Some(Bip49(key, KeychainKind::Internal)), /// Network::Testnet, /// )?; /// @@ -327,7 +339,7 @@ impl> DescriptorTemplate for Bip49 { /// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?; /// let mut wallet = Wallet::new( /// Bip49Public(key.clone(), fingerprint, KeychainKind::External), -/// Bip49Public(key, fingerprint, KeychainKind::Internal), +/// Some(Bip49Public(key, fingerprint, KeychainKind::Internal)), /// Network::Testnet, /// )?; /// @@ -363,7 +375,7 @@ impl> DescriptorTemplate for Bip49Public { /// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; /// let mut wallet = Wallet::new( /// Bip84(key.clone(), KeychainKind::External), -/// Bip84(key, KeychainKind::Internal), +/// Some(Bip84(key, KeychainKind::Internal)), /// Network::Testnet, /// )?; /// @@ -400,7 +412,7 @@ impl> DescriptorTemplate for Bip84 { /// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?; /// let mut wallet = Wallet::new( /// Bip84Public(key.clone(), fingerprint, KeychainKind::External), -/// Bip84Public(key, fingerprint, KeychainKind::Internal), +/// Some(Bip84Public(key, fingerprint, KeychainKind::Internal)), /// Network::Testnet, /// )?; /// @@ -436,7 +448,7 @@ impl> DescriptorTemplate for Bip84Public { /// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; /// let mut wallet = Wallet::new( /// Bip86(key.clone(), KeychainKind::External), -/// Bip86(key, KeychainKind::Internal), +/// Some(Bip86(key, KeychainKind::Internal)), /// Network::Testnet, /// )?; /// @@ -473,7 +485,7 @@ impl> DescriptorTemplate for Bip86 { /// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?; /// let mut wallet = Wallet::new( /// Bip86Public(key.clone(), fingerprint, KeychainKind::External), -/// Bip86Public(key, fingerprint, KeychainKind::Internal), +/// Some(Bip86Public(key, fingerprint, KeychainKind::Internal)), /// Network::Testnet, /// )?; /// diff --git a/crates/wallet/src/wallet/export.rs b/crates/wallet/src/wallet/export.rs index 4b49db144..959e401b8 100644 --- a/crates/wallet/src/wallet/export.rs +++ b/crates/wallet/src/wallet/export.rs @@ -31,7 +31,7 @@ //! let import = FullyNodedExport::from_str(import)?; //! let wallet = Wallet::new( //! &import.descriptor(), -//! &import.change_descriptor().expect("change descriptor"), +//! Some(&import.change_descriptor().expect("change descriptor")), //! Network::Testnet, //! )?; //! # Ok::<_, Box>(()) @@ -44,7 +44,7 @@ //! # use bdk_wallet::*; //! let wallet = Wallet::new( //! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)", -//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)", +//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"), //! Network::Testnet, //! )?; //! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true).unwrap(); @@ -224,7 +224,7 @@ mod test { fn get_test_wallet(descriptor: &str, change_descriptor: &str, network: Network) -> Wallet { use crate::wallet::Update; use bdk_chain::TxGraph; - let mut wallet = Wallet::new(descriptor, change_descriptor, network).unwrap(); + let mut wallet = Wallet::new(descriptor, Some(change_descriptor), network).unwrap(); let transaction = Transaction { input: vec![], output: vec![], diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 9db21ac71..53ab573bd 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -202,9 +202,10 @@ impl std::error::Error for NewError {} /// The error type when loading a [`Wallet`] from a [`ChangeSet`]. /// -/// Method [`load_from_changeset`] may return this error. +/// Methods [`load`] and [`load_with_descriptors`] may return this error. /// -/// [`load_from_changeset`]: Wallet::load_from_changeset +/// [`load`]: Wallet::load +/// [`load_with_descriptors`]: Wallet::load_with_descriptors #[derive(Debug)] pub enum LoadError { /// There was a problem with the passed-in descriptor(s). @@ -215,49 +216,7 @@ pub enum LoadError { MissingGenesis, /// Data loaded from persistence is missing descriptor. MissingDescriptor(KeychainKind), -} - -impl fmt::Display for LoadError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - LoadError::Descriptor(e) => e.fmt(f), - LoadError::MissingNetwork => write!(f, "loaded data is missing network type"), - LoadError::MissingGenesis => write!(f, "loaded data is missing genesis hash"), - LoadError::MissingDescriptor(k) => { - write!(f, "loaded data is missing descriptor for keychain {k:?}") - } - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for LoadError {} - -/// Error type for when we try load a [`Wallet`] from persistence and creating it if non-existent. -/// -/// Methods [`new_or_load`] and [`new_or_load_with_genesis_hash`] may return this error. -/// -/// [`new_or_load`]: Wallet::new_or_load -/// [`new_or_load_with_genesis_hash`]: Wallet::new_or_load_with_genesis_hash -#[derive(Debug)] -pub enum NewOrLoadError { - /// There is a problem with the passed-in descriptor. - Descriptor(crate::descriptor::DescriptorError), - /// The loaded genesis hash does not match what was provided. - LoadedGenesisDoesNotMatch { - /// The expected genesis block hash. - expected: BlockHash, - /// The block hash loaded from persistence. - got: Option, - }, - /// The loaded network type does not match what was provided. - LoadedNetworkDoesNotMatch { - /// The expected network type. - expected: Network, - /// The network type loaded from persistence. - got: Option, - }, - /// The loaded desccriptor does not match what was provided. + /// The loaded descriptor does not match what was provided. LoadedDescriptorDoesNotMatch { /// The descriptor loaded from persistence. got: Option, @@ -266,17 +225,16 @@ pub enum NewOrLoadError { }, } -impl fmt::Display for NewOrLoadError { +impl fmt::Display for LoadError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - NewOrLoadError::Descriptor(e) => e.fmt(f), - NewOrLoadError::LoadedGenesisDoesNotMatch { expected, got } => { - write!(f, "loaded genesis hash is not {}, got {:?}", expected, got) - } - NewOrLoadError::LoadedNetworkDoesNotMatch { expected, got } => { - write!(f, "loaded network type is not {}, got {:?}", expected, got) + LoadError::Descriptor(e) => e.fmt(f), + LoadError::MissingNetwork => write!(f, "loaded data is missing network type"), + LoadError::MissingGenesis => write!(f, "loaded data is missing genesis hash"), + LoadError::MissingDescriptor(k) => { + write!(f, "loaded data is missing descriptor for keychain {k:?}") } - NewOrLoadError::LoadedDescriptorDoesNotMatch { got, keychain } => { + LoadError::LoadedDescriptorDoesNotMatch { got, keychain } => { write!( f, "loaded descriptor is different from what was provided, got {:?} for keychain {:?}", @@ -288,7 +246,7 @@ impl fmt::Display for NewOrLoadError { } #[cfg(feature = "std")] -impl std::error::Error for NewOrLoadError {} +impl std::error::Error for LoadError {} /// An error that may occur when applying a block to [`Wallet`]. #[derive(Debug)] @@ -325,9 +283,18 @@ impl std::error::Error for ApplyBlockError {} impl Wallet { /// Initialize an empty [`Wallet`]. + /// + /// The `descriptor` is used to derive external (receive) addresses and the `change_descriptor` + /// to derive internal (change) addresses. + /// + /// If only public keys are included in the descriptors then the wallet will be able to create + /// spending transactions but not sign them. Use [`Wallet::add_signer`] to manually add signers. + /// + /// If private keys are included in the descriptors then signers for those keys will be added + /// automatically to the new [`Wallet`]. pub fn new( descriptor: E, - change_descriptor: E, + change_descriptor: Option, network: Network, ) -> Result { let genesis_hash = genesis_block(network).block_hash(); @@ -340,7 +307,7 @@ impl Wallet { /// for syncing from alternative networks. pub fn new_with_genesis_hash( descriptor: E, - change_descriptor: E, + change_descriptor: Option, network: Network, genesis_hash: BlockHash, ) -> Result { @@ -401,7 +368,7 @@ impl Wallet { /// let external_signer_container = SignersContainer::build(external_keymap, &external_descriptor, &secp); /// let internal_signer_container = SignersContainer::build(internal_keymap, &internal_descriptor, &secp); /// let changeset = db.read()?.expect("there must be an existing changeset"); - /// let mut wallet = Wallet::load_from_changeset(changeset)?; + /// let mut wallet = Wallet::load(changeset)?; /// /// external_signer_container.signers().into_iter() /// .for_each(|s| wallet.add_signer(KeychainKind::External, SignerOrdering::default(), s.clone())); @@ -411,32 +378,14 @@ impl Wallet { /// # } /// ``` /// - /// Alternatively, you can call [`Wallet::new_or_load`], which will add the private keys of the - /// passed-in descriptors to the [`Wallet`]. - pub fn load_from_changeset(changeset: ChangeSet) -> Result { + /// Alternatively, you can call [`Wallet::load_with_descriptors`], which will add the private + /// keys (if any) of the passed-in descriptors to the [`Wallet`]. + pub fn load(changeset: ChangeSet) -> Result { let secp = Secp256k1::new(); let network = changeset.network.ok_or(LoadError::MissingNetwork)?; let chain = LocalChain::from_changeset(changeset.chain).map_err(|_| LoadError::MissingGenesis)?; - let mut index = KeychainTxOutIndex::::default(); - let descriptor = changeset - .indexed_tx_graph - .indexer - .keychains_added - .get(&KeychainKind::External) - .ok_or(LoadError::MissingDescriptor(KeychainKind::External))? - .clone(); - let change_descriptor = changeset - .indexed_tx_graph - .indexer - .keychains_added - .get(&KeychainKind::Internal) - .ok_or(LoadError::MissingDescriptor(KeychainKind::Internal))? - .clone(); - - let (signers, change_signers) = - create_signers(&mut index, &secp, descriptor, change_descriptor, network) - .expect("Can't fail: we passed in valid descriptors, recovered from the changeset"); + let index = KeychainTxOutIndex::::default(); let mut indexed_graph = IndexedTxGraph::new(index); indexed_graph.apply_changeset(changeset.indexed_tx_graph); @@ -444,8 +393,8 @@ impl Wallet { let stage = ChangeSet::default(); Ok(Wallet { - signers, - change_signers, + signers: Arc::new(SignersContainer::new()), + change_signers: Arc::new(SignersContainer::new()), chain, indexed_graph, stage, @@ -454,9 +403,13 @@ impl Wallet { }) } - /// Either loads [`Wallet`] from the given [`ChangeSet`] or initializes it if one does not exist. + /// Loads [`Wallet`] from the previously persisted [`ChangeSet`] and descriptors. /// - /// This method will fail if the loaded [`ChangeSet`] has different parameters to those provided. + /// This method will fail if the given descriptors don't match those included in the + /// [`ChangeSet`]. + /// + /// If the provided descriptors contain private keys the signers for those keys will be added + /// to the [`Wallet`]. /// /// ```rust,no_run /// # use bdk_wallet::Wallet; @@ -464,133 +417,74 @@ impl Wallet { /// # use bitcoin::Network::Testnet; /// let conn = Connection::open_in_memory().expect("must open connection"); /// let mut db = Store::new(conn).expect("must create db"); - /// let changeset = db.read()?; + /// let changeset = db.read()?.expect("changeset must exist"); /// /// let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; /// let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; /// - /// let mut wallet = Wallet::new_or_load(external_descriptor, internal_descriptor, changeset, Testnet)?; + /// let mut wallet = Wallet::load_with_descriptors(external_descriptor, internal_descriptor, changeset)?; /// # Ok::<(), anyhow::Error>(()) /// ``` - pub fn new_or_load( + pub fn load_with_descriptors( descriptor: E, change_descriptor: E, - changeset: Option, - network: Network, - ) -> Result { - let genesis_hash = genesis_block(network).block_hash(); - Self::new_or_load_with_genesis_hash( - descriptor, - change_descriptor, - changeset, - network, - genesis_hash, - ) - } - - /// Either loads [`Wallet`] from a [`ChangeSet`] or initializes it if one does not exist, using the - /// provided descriptor, change descriptor, network, and custom genesis hash. - /// - /// This method will fail if the loaded [`ChangeSet`] has different parameters to those provided. - /// This is like [`Wallet::new_or_load`] with an additional `genesis_hash` parameter. This is - /// useful for syncing from alternative networks. - pub fn new_or_load_with_genesis_hash( - descriptor: E, - change_descriptor: E, - changeset: Option, - network: Network, - genesis_hash: BlockHash, - ) -> Result { - if let Some(changeset) = changeset { - let mut wallet = Self::load_from_changeset(changeset).map_err(|e| match e { - LoadError::Descriptor(e) => NewOrLoadError::Descriptor(e), - LoadError::MissingNetwork => NewOrLoadError::LoadedNetworkDoesNotMatch { - expected: network, - got: None, - }, - LoadError::MissingGenesis => NewOrLoadError::LoadedGenesisDoesNotMatch { - expected: genesis_hash, - got: None, - }, - LoadError::MissingDescriptor(keychain) => { - NewOrLoadError::LoadedDescriptorDoesNotMatch { - got: None, - keychain, - } - } - })?; - if wallet.network != network { - return Err(NewOrLoadError::LoadedNetworkDoesNotMatch { - expected: network, - got: Some(wallet.network), - }); - } - if wallet.chain.genesis_hash() != genesis_hash { - return Err(NewOrLoadError::LoadedGenesisDoesNotMatch { - expected: genesis_hash, - got: Some(wallet.chain.genesis_hash()), - }); - } - - let (expected_descriptor, expected_descriptor_keymap) = descriptor - .into_wallet_descriptor(&wallet.secp, network) - .map_err(NewOrLoadError::Descriptor)?; - let wallet_descriptor = wallet.public_descriptor(KeychainKind::External); - if wallet_descriptor != &expected_descriptor { - return Err(NewOrLoadError::LoadedDescriptorDoesNotMatch { - got: Some(wallet_descriptor.clone()), - keychain: KeychainKind::External, - }); - } - // if expected descriptor has private keys add them as new signers - if !expected_descriptor_keymap.is_empty() { - let signer_container = SignersContainer::build( - expected_descriptor_keymap, - &expected_descriptor, - &wallet.secp, - ); - signer_container.signers().into_iter().for_each(|signer| { - wallet.add_signer( - KeychainKind::External, - SignerOrdering::default(), - signer.clone(), - ) - }); - } - - let (expected_change_descriptor, expected_change_descriptor_keymap) = change_descriptor - .into_wallet_descriptor(&wallet.secp, network) - .map_err(NewOrLoadError::Descriptor)?; - let wallet_change_descriptor = wallet.public_descriptor(KeychainKind::Internal); - if wallet_change_descriptor != &expected_change_descriptor { - return Err(NewOrLoadError::LoadedDescriptorDoesNotMatch { - got: Some(wallet_change_descriptor.clone()), - keychain: KeychainKind::Internal, - }); - } - // if expected change descriptor has private keys add them as new signers - if !expected_change_descriptor_keymap.is_empty() { - let signer_container = SignersContainer::build( - expected_change_descriptor_keymap, - &expected_change_descriptor, - &wallet.secp, - ); - signer_container.signers().into_iter().for_each(|signer| { - wallet.add_signer( - KeychainKind::Internal, - SignerOrdering::default(), - signer.clone(), - ) - }); - } + changeset: ChangeSet, + ) -> Result { + let mut wallet = Self::load(changeset)?; + + let (expected_descriptor, expected_descriptor_keymap) = descriptor + .into_wallet_descriptor(&wallet.secp, wallet.network) + .map_err(LoadError::Descriptor)?; + let wallet_descriptor = wallet.public_descriptor(KeychainKind::External); + if wallet_descriptor != &expected_descriptor { + return Err(LoadError::LoadedDescriptorDoesNotMatch { + got: Some(wallet_descriptor.clone()), + keychain: KeychainKind::External, + }); + } + // if expected descriptor has private keys add them as new signers + if !expected_descriptor_keymap.is_empty() { + let signer_container = SignersContainer::build( + expected_descriptor_keymap, + &expected_descriptor, + &wallet.secp, + ); + signer_container.signers().into_iter().for_each(|signer| { + wallet.add_signer( + KeychainKind::External, + SignerOrdering::default(), + signer.clone(), + ) + }); + } - Ok(wallet) - } else { - Self::new_with_genesis_hash(descriptor, change_descriptor, network, genesis_hash) - .map_err(|e| match e { - NewError::Descriptor(e) => NewOrLoadError::Descriptor(e), - }) + let (expected_change_descriptor, expected_change_descriptor_keymap) = change_descriptor + .into_wallet_descriptor(&wallet.secp, wallet.network) + .map_err(LoadError::Descriptor)?; + let wallet_change_descriptor = wallet.public_descriptor(KeychainKind::Internal); + if wallet_change_descriptor != &expected_change_descriptor { + return Err(LoadError::LoadedDescriptorDoesNotMatch { + got: Some(wallet_change_descriptor.clone()), + keychain: KeychainKind::Internal, + }); + } + // if expected change descriptor has private keys add them as new signers + if !expected_change_descriptor_keymap.is_empty() { + let signer_container = SignersContainer::build( + expected_change_descriptor_keymap, + &expected_change_descriptor, + &wallet.secp, + ); + signer_container.signers().into_iter().for_each(|signer| { + wallet.add_signer( + KeychainKind::Internal, + SignerOrdering::default(), + signer.clone(), + ) + }); } + + Ok(wallet) } /// Get the Bitcoin network the wallet is using. @@ -648,7 +542,7 @@ impl Wallet { /// let conn = Connection::open_in_memory().expect("must open connection"); /// let mut db = Store::new(conn).expect("must create store"); /// # let changeset = ChangeSet::default(); - /// # let mut wallet = Wallet::load_from_changeset(changeset).expect("load wallet"); + /// # let mut wallet = Wallet::load(changeset).expect("load wallet"); /// let next_address = wallet.reveal_next_address(KeychainKind::External); /// if let Some(changeset) = wallet.take_staged() { /// db.write(&changeset)?; @@ -1119,7 +1013,7 @@ impl Wallet { /// # use bdk_wallet::bitcoin::Network; /// let descriptor = "wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/1'/0'/0/*)"; /// let change_descriptor = "wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/1'/0'/1/*)"; - /// let wallet = Wallet::new(descriptor, change_descriptor, Network::Testnet)?; + /// let wallet = Wallet::new(descriptor, Some(change_descriptor), Network::Testnet)?; /// for secret_key in wallet.get_signers(KeychainKind::External).signers().iter().filter_map(|s| s.descriptor_secret_key()) { /// // secret_key: tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/* /// println!("secret_key: {}", secret_key); @@ -1178,14 +1072,10 @@ impl Wallet { ) -> Result { let keychains: BTreeMap<_, _> = self.indexed_graph.index.keychains().collect(); let external_descriptor = keychains.get(&KeychainKind::External).expect("must exist"); - let internal_descriptor = keychains.get(&KeychainKind::Internal).expect("must exist"); let external_policy = external_descriptor .extract_policy(&self.signers, BuildSatisfaction::None, &self.secp)? .unwrap(); - let internal_policy = internal_descriptor - .extract_policy(&self.change_signers, BuildSatisfaction::None, &self.secp)? - .unwrap(); // The policy allows spending external outputs, but it requires a policy path that hasn't been // provided @@ -1197,15 +1087,6 @@ impl Wallet { KeychainKind::External, )); }; - // Same for the internal_policy path - if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeForbidden - && internal_policy.requires_path() - && params.internal_policy_path.is_none() - { - return Err(CreateTxError::SpendingPolicyRequired( - KeychainKind::Internal, - )); - }; let external_requirements = external_policy.get_condition( params @@ -1213,14 +1094,32 @@ impl Wallet { .as_ref() .unwrap_or(&BTreeMap::new()), )?; - let internal_requirements = internal_policy.get_condition( - params - .internal_policy_path - .as_ref() - .unwrap_or(&BTreeMap::new()), - )?; + let mut requirements = external_requirements; - let requirements = external_requirements.merge(&internal_requirements)?; + if let Some(internal_descriptor) = keychains.get(&KeychainKind::Internal) { + let internal_policy = internal_descriptor + .extract_policy(&self.change_signers, BuildSatisfaction::None, &self.secp)? + .unwrap(); + + // Same for the internal_policy path + if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeForbidden + && internal_policy.requires_path() + && params.internal_policy_path.is_none() + { + return Err(CreateTxError::SpendingPolicyRequired( + KeychainKind::Internal, + )); + }; + + let internal_requirements = internal_policy.get_condition( + params + .internal_policy_path + .as_ref() + .unwrap_or(&BTreeMap::new()), + )?; + + requirements = requirements.merge(&internal_requirements)?; + } let version = match params.version { Some(tx_builder::Version(0)) => return Err(CreateTxError::Version0), @@ -1386,12 +1285,18 @@ impl Wallet { let drain_script = match params.drain_to { Some(ref drain_recipient) => drain_recipient.clone(), None => { - let change_keychain = KeychainKind::Internal; + let mut change_keychain = KeychainKind::Internal; let ((index, spk), index_changeset) = self .indexed_graph .index .next_unused_spk(&change_keychain) - .expect("keychain must exist"); + .unwrap_or_else(|| { + change_keychain = KeychainKind::External; + self.indexed_graph + .index + .next_unused_spk(&change_keychain) + .expect("") + }); self.indexed_graph.index.mark_used(change_keychain, index); self.stage.merge(index_changeset.into()); spk @@ -1635,12 +1540,8 @@ impl Wallet { if tx.output.len() > 1 { let mut change_index = None; for (index, txout) in tx.output.iter().enumerate() { - let change_keychain = KeychainKind::Internal; - match txout_index.index_of_spk(&txout.script_pubkey) { - Some((keychain, _)) if *keychain == change_keychain => { - change_index = Some(index) - } - _ => {} + if self.is_mine(&txout.script_pubkey) { + change_index = Some(index) } } @@ -2428,32 +2329,38 @@ fn create_signers( index: &mut KeychainTxOutIndex, secp: &Secp256k1, descriptor: E, - change_descriptor: E, + change_descriptor: Option, network: Network, ) -> Result<(Arc, Arc), DescriptorError> { let descriptor = into_wallet_descriptor_checked(descriptor, secp, network)?; - let change_descriptor = into_wallet_descriptor_checked(change_descriptor, secp, network)?; + let change_descriptor = change_descriptor + .map(|cd| into_wallet_descriptor_checked(cd, secp, network)) + .transpose()?; let (descriptor, keymap) = descriptor; let signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp)); let _ = index .insert_descriptor(KeychainKind::External, descriptor) .expect("this is the first descriptor we're inserting"); - let (descriptor, keymap) = change_descriptor; - let change_signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp)); - let _ = index - .insert_descriptor(KeychainKind::Internal, descriptor) - .map_err(|e| { - use bdk_chain::indexer::keychain_txout::InsertDescriptorError; - match e { - InsertDescriptorError::DescriptorAlreadyAssigned { .. } => { - crate::descriptor::error::Error::ExternalAndInternalAreTheSame - } - InsertDescriptorError::KeychainAlreadyAssigned { .. } => { - unreachable!("this is the first time we're assigning internal") + let change_signers = if let Some((descriptor, keymap)) = change_descriptor { + let change_signer = Arc::new(SignersContainer::build(keymap, &descriptor, secp)); + let _ = index + .insert_descriptor(KeychainKind::Internal, descriptor) + .map_err(|e| { + use bdk_chain::indexer::keychain_txout::InsertDescriptorError; + match e { + InsertDescriptorError::DescriptorAlreadyAssigned { .. } => { + crate::descriptor::error::Error::ExternalAndInternalAreTheSame + } + InsertDescriptorError::KeychainAlreadyAssigned { .. } => { + unreachable!("this is the first time we're assigning internal") + } } - } - })?; + })?; + change_signer + } else { + Arc::new(SignersContainer::new()) + }; Ok((signers, change_signers)) } @@ -2483,7 +2390,7 @@ macro_rules! doctest_wallet { let mut wallet = Wallet::new( descriptor, - change_descriptor, + Some(change_descriptor), Network::Regtest, ) .unwrap(); diff --git a/crates/wallet/src/wallet/signer.rs b/crates/wallet/src/wallet/signer.rs index 4cd86ae9d..e1a69f33e 100644 --- a/crates/wallet/src/wallet/signer.rs +++ b/crates/wallet/src/wallet/signer.rs @@ -69,7 +69,7 @@ //! //! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/0/*)"; //! let change_descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/1/*)"; -//! let mut wallet = Wallet::new(descriptor, change_descriptor, Network::Testnet)?; +//! let mut wallet = Wallet::new(descriptor, Some(change_descriptor), Network::Testnet)?; //! wallet.add_signer( //! KeychainKind::External, //! SignerOrdering(200), diff --git a/crates/wallet/tests/common.rs b/crates/wallet/tests/common.rs index 9774ec985..c90b75763 100644 --- a/crates/wallet/tests/common.rs +++ b/crates/wallet/tests/common.rs @@ -15,7 +15,12 @@ use std::str::FromStr; /// The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 /// to a foreign address and one returning 50_000 back to the wallet. The remaining 1000 /// sats are the transaction fee. -pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet, bitcoin::Txid) { +/// +/// Note: no change descriptor is used internal (change) SPKs will be derived from the external descriptor. +pub fn get_funded_wallet_with_change( + descriptor: &str, + change: Option<&str>, +) -> (Wallet, bitcoin::Txid) { let mut wallet = Wallet::new(descriptor, change, Network::Regtest).unwrap(); let receive_address = wallet.peek_address(KeychainKind::External, 0).address; let sendto_address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5") @@ -113,16 +118,13 @@ pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet, /// to a foreign address and one returning 50_000 back to the wallet. The remaining 1000 /// sats are the transaction fee. /// -/// Note: the change descriptor will have script type `p2wpkh`. If passing some other script type -/// as argument, make sure you're ok with getting a wallet where the keychains have potentially -/// different script types. Otherwise, use `get_funded_wallet_with_change`. +/// Note: no change descriptor is used internal (change) SPKs will be derived from the external descriptor. pub fn get_funded_wallet(descriptor: &str) -> (Wallet, bitcoin::Txid) { - let change = get_test_wpkh_change(); - get_funded_wallet_with_change(descriptor, change) + get_funded_wallet_with_change(descriptor, None) } pub fn get_funded_wallet_wpkh() -> (Wallet, bitcoin::Txid) { - get_funded_wallet_with_change(get_test_wpkh(), get_test_wpkh_change()) + get_funded_wallet_with_change(get_test_wpkh(), None) } pub fn get_test_wpkh() -> &'static str { diff --git a/crates/wallet/tests/psbt.rs b/crates/wallet/tests/psbt.rs index 155bb143a..7640b884d 100644 --- a/crates/wallet/tests/psbt.rs +++ b/crates/wallet/tests/psbt.rs @@ -144,7 +144,7 @@ fn test_psbt_fee_rate_with_missing_txout() { let desc = "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/0)"; let change_desc = "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/1)"; - let (mut pkh_wallet, _) = get_funded_wallet_with_change(desc, change_desc); + let (mut pkh_wallet, _) = get_funded_wallet_with_change(desc, Some(change_desc)); let addr = pkh_wallet.peek_address(KeychainKind::External, 0); let mut builder = pkh_wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); @@ -173,7 +173,7 @@ fn test_psbt_multiple_internalkey_signers() { let keypair = Keypair::from_secret_key(&secp, &prv.inner); let change_desc = "tr(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"; - let (mut wallet, _) = get_funded_wallet_with_change(&desc, change_desc); + let (mut wallet, _) = get_funded_wallet_with_change(&desc, Some(change_desc)); let to_spend = wallet.balance().total(); let send_to = wallet.peek_address(KeychainKind::External, 0); let mut builder = wallet.build_tx(); diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index 15e73958f..b16c722e8 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -123,7 +123,7 @@ fn load_recovers_wallet() -> anyhow::Result<()> { // create new wallet let wallet_spk_index = { let mut wallet = - Wallet::new(desc, change_desc, Network::Testnet).expect("must init wallet"); + Wallet::new(desc, Some(change_desc), Network::Testnet).expect("must init wallet"); wallet.reveal_next_address(KeychainKind::External); @@ -141,7 +141,7 @@ fn load_recovers_wallet() -> anyhow::Result<()> { let db = &mut recover(&file_path).expect("must recover db"); let changeset = read(db).expect("must recover wallet").expect("changeset"); - let wallet = Wallet::load_from_changeset(changeset).expect("must recover wallet"); + let wallet = Wallet::load(changeset).expect("must recover wallet"); assert_eq!(wallet.network(), Network::Testnet); assert_eq!( wallet.spk_index().keychains().collect::>(), @@ -182,15 +182,15 @@ fn load_recovers_wallet() -> anyhow::Result<()> { } #[test] -fn new_or_load() -> anyhow::Result<()> { - fn run( +fn new_and_load() -> anyhow::Result<()> { + fn run( filename: &str, - new_or_load: NewOrRecover, + load: Recover, read: Read, write: Write, ) -> anyhow::Result<()> where - NewOrRecover: Fn(&Path) -> anyhow::Result, + Recover: Fn(&Path) -> anyhow::Result, Read: Fn(&mut Db) -> anyhow::Result>, Write: Fn(&mut Db, &ChangeSet) -> anyhow::Result<()>, { @@ -198,62 +198,16 @@ fn new_or_load() -> anyhow::Result<()> { let file_path = temp_dir.path().join(filename); let (desc, change_desc) = get_test_wpkh_with_change_desc(); - // init wallet when non-existent + // init wallet let wallet_keychains: BTreeMap<_, _> = { - let wallet = &mut Wallet::new_or_load(desc, change_desc, None, Network::Testnet) + let wallet = &mut Wallet::new(desc, Some(change_desc), Network::Testnet) .expect("must init wallet"); - let mut db = new_or_load(&file_path).expect("must create db"); - if let Some(changeset) = wallet.take_staged() { - write(&mut db, &changeset)?; - } + let changeset = wallet.take_staged().expect("must have changeset"); + let db = &mut load(&file_path).expect("must open db"); + write(db, &changeset)?; wallet.keychains().map(|(k, v)| (*k, v.clone())).collect() }; - // wrong network - { - let mut db = new_or_load(&file_path).expect("must create db"); - let changeset = read(&mut db)?; - let err = Wallet::new_or_load(desc, change_desc, changeset, Network::Bitcoin) - .expect_err("wrong network"); - assert!( - matches!( - err, - bdk_wallet::wallet::NewOrLoadError::LoadedNetworkDoesNotMatch { - got: Some(Network::Testnet), - expected: Network::Bitcoin - } - ), - "err: {}", - err, - ); - } - - // wrong genesis hash - { - let exp_blockhash = BlockHash::all_zeros(); - let got_blockhash = bitcoin::constants::genesis_block(Network::Testnet).block_hash(); - - let db = &mut new_or_load(&file_path).expect("must open db"); - let changeset = read(db)?; - let err = Wallet::new_or_load_with_genesis_hash( - desc, - change_desc, - changeset, - Network::Testnet, - exp_blockhash, - ) - .expect_err("wrong genesis hash"); - assert!( - matches!( - err, - bdk_wallet::wallet::NewOrLoadError::LoadedGenesisDoesNotMatch { got, expected } - if got == Some(got_blockhash) && expected == exp_blockhash - ), - "err: {}", - err, - ); - } - // wrong external descriptor { let (exp_descriptor, exp_change_desc) = get_test_tr_single_sig_xprv_with_change_desc(); @@ -262,15 +216,14 @@ fn new_or_load() -> anyhow::Result<()> { .unwrap() .0; - let db = &mut new_or_load(&file_path).expect("must open db"); - let changeset = read(db)?; - let err = - Wallet::new_or_load(exp_descriptor, exp_change_desc, changeset, Network::Testnet) - .expect_err("wrong external descriptor"); + let db = &mut load(&file_path).expect("must open db"); + let changeset = read(db)?.expect("changeset must exist"); + let err = Wallet::load_with_descriptors(exp_descriptor, exp_change_desc, changeset) + .expect_err("wrong external descriptor"); assert!( matches!( err, - bdk_wallet::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain } + bdk_wallet::wallet::LoadError::LoadedDescriptorDoesNotMatch { ref got, keychain } if got == &Some(got_descriptor) && keychain == KeychainKind::External ), "err: {}", @@ -286,14 +239,14 @@ fn new_or_load() -> anyhow::Result<()> { .unwrap() .0; - let db = &mut new_or_load(&file_path).expect("must open db"); - let changeset = read(db)?; - let err = Wallet::new_or_load(desc, exp_descriptor, changeset, Network::Testnet) + let db = &mut load(&file_path).expect("must open db"); + let changeset = read(db)?.expect("changeset must exist"); + let err = Wallet::load_with_descriptors(desc, exp_descriptor, changeset) .expect_err("wrong internal descriptor"); assert!( matches!( err, - bdk_wallet::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain } + bdk_wallet::wallet::LoadError::LoadedDescriptorDoesNotMatch { ref got, keychain } if got == &Some(got_descriptor) && keychain == KeychainKind::Internal ), "err: {}", @@ -303,9 +256,9 @@ fn new_or_load() -> anyhow::Result<()> { // all parameters match { - let db = &mut new_or_load(&file_path).expect("must open db"); - let changeset = read(db)?; - let wallet = Wallet::new_or_load(desc, change_desc, changeset, Network::Testnet) + let db = &mut load(&file_path).expect("must open db"); + let changeset = read(db)?.expect("changeset must exist"); + let wallet = Wallet::load_with_descriptors(desc, change_desc, changeset) .expect("must recover wallet"); assert_eq!(wallet.network(), Network::Testnet); assert!(wallet @@ -313,6 +266,23 @@ fn new_or_load() -> anyhow::Result<()> { .map(|(k, v)| (*k, v.clone())) .eq(wallet_keychains)); } + + // signers added + { + let db = &mut load(&file_path).expect("must open db"); + let changeset = read(db)?.expect("changeset must exist"); + let wallet = Wallet::load_with_descriptors(desc, change_desc, changeset) + .expect("must recover wallet"); + assert_eq!( + wallet.get_signers(KeychainKind::External).signers().len(), + 1 + ); + assert_eq!( + wallet.get_signers(KeychainKind::Internal).signers().len(), + 1 + ); + } + Ok(()) } @@ -336,7 +306,7 @@ fn new_or_load() -> anyhow::Result<()> { fn test_error_external_and_internal_are_the_same() { // identical descriptors should fail to create wallet let desc = get_test_wpkh(); - let err = Wallet::new(desc, desc, Network::Testnet); + let err = Wallet::new(desc, Some(desc), Network::Testnet); assert!( matches!( &err, @@ -351,7 +321,7 @@ fn test_error_external_and_internal_are_the_same() { // public + private of same descriptor should fail to create wallet let desc = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)"; let change_desc = "wpkh([3c31d632/84'/1'/0']tpubDCYwFkks2cg78N7eoYbBatsFEGje8vW8arSKW4rLwD1AU1s9KJMDRHE32JkvYERuiFjArrsH7qpWSpJATed5ShZbG9KsskA5Rmi6NSYgYN2/0/*)"; - let err = Wallet::new(desc, change_desc, Network::Testnet); + let err = Wallet::new(desc, Some(change_desc), Network::Testnet); assert!( matches!( err, @@ -1311,13 +1281,16 @@ fn test_create_tx_policy_path_required() { .assume_checked(); let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), Amount::from_sat(10_000)); + // let mut path = BTreeMap::new(); + // path.insert("pgpxrq3k".to_string(), vec![0, 1]); + // builder.policy_path(path, External); builder.finish().unwrap(); } #[test] fn test_create_tx_policy_path_no_csv() { let (desc, change_desc) = get_test_wpkh_with_change_desc(); - let mut wallet = Wallet::new(desc, change_desc, Network::Regtest).expect("wallet"); + let mut wallet = Wallet::new(desc, Some(change_desc), Network::Regtest).expect("wallet"); let tx = Transaction { version: transaction::Version::non_standard(0), @@ -1408,7 +1381,7 @@ fn test_create_tx_global_xpubs_with_origin() { let fingerprint = bip32::Fingerprint::from_hex("73756c7f").unwrap(); let path = bip32::DerivationPath::from_str("m/48'/0'/0'/2'").unwrap(); - assert_eq!(psbt.xpub.len(), 2); + assert_eq!(psbt.xpub.len(), 1); assert_eq!(psbt.xpub.get(&key), Some(&(fingerprint, path))); } @@ -1682,7 +1655,7 @@ fn test_create_tx_global_xpubs_master_without_origin() { let key = bip32::Xpub::from_str("tpubD6NzVbkrYhZ4Y55A58Gv9RSNF5hy84b5AJqYy7sCcjFrkcLpPre8kmgfit6kY1Zs3BLgeypTDBZJM222guPpdz7Cup5yzaMu62u7mYGbwFL").unwrap(); let fingerprint = bip32::Fingerprint::from_hex("997a323b").unwrap(); - assert_eq!(psbt.xpub.len(), 2); + assert_eq!(psbt.xpub.len(), 1); assert_eq!( psbt.xpub.get(&key), Some(&(fingerprint, bip32::DerivationPath::default())) @@ -2229,8 +2202,9 @@ fn test_bump_fee_absolute_add_input() { ); let tx = &psbt.unsigned_tx; + dbg!(&tx); assert_eq!(tx.input.len(), 2); - assert_eq!(tx.output.len(), 2); + //assert_eq!(tx.output.len(), 2); assert_eq!( tx.output .iter() @@ -2733,7 +2707,7 @@ fn test_sign_single_xprv_no_hd_keypaths() { fn test_include_output_redeem_witness_script() { let desc = get_test_wpkh(); let change_desc = "sh(wsh(multi(1,cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW,cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu)))"; - let (mut wallet, _) = get_funded_wallet_with_change(desc, change_desc); + let (mut wallet, _) = get_funded_wallet_with_change(desc, Some(change_desc)); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") .unwrap() .assume_checked(); @@ -2929,7 +2903,7 @@ fn test_sign_nonstandard_sighash() { fn test_unused_address() { let desc = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; let change_desc = get_test_wpkh(); - let mut wallet = Wallet::new(desc, change_desc, Network::Testnet).expect("wallet"); + let mut wallet = Wallet::new(desc, Some(change_desc), Network::Testnet).expect("wallet"); // `list_unused_addresses` should be empty if we haven't revealed any assert!(wallet @@ -2957,7 +2931,7 @@ fn test_unused_address() { fn test_next_unused_address() { let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; let change = get_test_wpkh(); - let mut wallet = Wallet::new(descriptor, change, Network::Testnet).expect("wallet"); + let mut wallet = Wallet::new(descriptor, Some(change), Network::Testnet).expect("wallet"); assert_eq!(wallet.derivation_index(KeychainKind::External), None); assert_eq!( @@ -3004,7 +2978,7 @@ fn test_next_unused_address() { fn test_peek_address_at_index() { let desc = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; let change_desc = get_test_wpkh(); - let mut wallet = Wallet::new(desc, change_desc, Network::Testnet).unwrap(); + let mut wallet = Wallet::new(desc, Some(change_desc), Network::Testnet).unwrap(); assert_eq!( wallet.peek_address(KeychainKind::External, 1).to_string(), @@ -3040,7 +3014,7 @@ fn test_peek_address_at_index() { #[test] fn test_peek_address_at_index_not_derivable() { let wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/1)", - get_test_wpkh(), Network::Testnet).unwrap(); + Some(get_test_wpkh()), Network::Testnet).unwrap(); assert_eq!( wallet.peek_address(KeychainKind::External, 1).to_string(), @@ -3061,7 +3035,7 @@ fn test_peek_address_at_index_not_derivable() { #[test] fn test_returns_index_and_address() { let mut wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", - get_test_wpkh(), Network::Testnet).unwrap(); + Some(get_test_wpkh()), Network::Testnet).unwrap(); // new index 0 assert_eq!( @@ -3129,7 +3103,7 @@ fn test_get_address() { let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); let wallet = Wallet::new( Bip84(key, KeychainKind::External), - Bip84(key, KeychainKind::Internal), + Some(Bip84(key, KeychainKind::Internal)), Network::Regtest, ) .unwrap(); @@ -3157,10 +3131,37 @@ fn test_get_address() { ); } +#[test] +#[should_panic(expected = "keychain must exist")] +fn test_get_address_no_internal() { + use bdk_wallet::descriptor::template::Bip84; + let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + let wallet = Wallet::new( + Bip84(key, KeychainKind::External), + None, + Network::Regtest, + ) + .unwrap(); + + assert_eq!( + wallet.peek_address(KeychainKind::External, 0), + AddressInfo { + index: 0, + address: Address::from_str("bcrt1qrhgaqu0zvf5q2d0gwwz04w0dh0cuehhqvzpp4w") + .unwrap() + .assume_checked(), + keychain: KeychainKind::External, + } + ); + + // this should panic + wallet.peek_address(KeychainKind::Internal, 0); +} + #[test] fn test_reveal_addresses() { let (desc, change_desc) = get_test_tr_single_sig_xprv_with_change_desc(); - let mut wallet = Wallet::new(desc, change_desc, Network::Signet).unwrap(); + let mut wallet = Wallet::new(desc, Some(change_desc), Network::Signet).unwrap(); let keychain = KeychainKind::External; let last_revealed_addr = wallet.reveal_addresses_to(keychain, 9).last().unwrap(); @@ -3183,7 +3184,7 @@ fn test_get_address_no_reuse() { let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); let mut wallet = Wallet::new( Bip84(key, KeychainKind::External), - Bip84(key, KeychainKind::Internal), + Some(Bip84(key, KeychainKind::Internal)), Network::Regtest, ) .unwrap(); @@ -3202,7 +3203,7 @@ fn test_get_address_no_reuse() { #[test] fn test_taproot_psbt_populate_tap_key_origins() { let (desc, change_desc) = get_test_tr_single_sig_xprv_with_change_desc(); - let (mut wallet, _) = get_funded_wallet_with_change(desc, change_desc); + let (mut wallet, _) = get_funded_wallet_with_change(desc, Some(change_desc)); let addr = wallet.reveal_next_address(KeychainKind::External); let mut builder = wallet.build_tx(); @@ -3238,7 +3239,7 @@ fn test_taproot_psbt_populate_tap_key_origins() { #[test] fn test_taproot_psbt_populate_tap_key_origins_repeated_key() { let (mut wallet, _) = - get_funded_wallet_with_change(get_test_tr_repeated_key(), get_test_tr_single_sig()); + get_funded_wallet_with_change(get_test_tr_repeated_key(), Some(get_test_tr_single_sig())); let addr = wallet.reveal_next_address(KeychainKind::External); let path = vec![("rn4nre9c".to_string(), vec![0])] @@ -3657,7 +3658,7 @@ fn test_taproot_sign_derive_index_from_psbt() { // re-create the wallet with an empty db let wallet_empty = Wallet::new( get_test_tr_single_sig_xprv(), - get_test_tr_single_sig(), + Some(get_test_tr_single_sig()), Network::Regtest, ) .unwrap(); @@ -3760,7 +3761,7 @@ fn test_taproot_sign_non_default_sighash() { #[test] fn test_spend_coinbase() { let (desc, change_desc) = get_test_wpkh_with_change_desc(); - let mut wallet = Wallet::new(desc, change_desc, Network::Regtest).unwrap(); + let mut wallet = Wallet::new(desc, Some(change_desc), Network::Regtest).unwrap(); let confirmation_height = 5; wallet @@ -4025,7 +4026,8 @@ fn test_keychains_with_overlapping_spks() { let wildcard_keychain = "wpkh(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*)"; let non_wildcard_keychain = "wpkh(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/1)"; - let (mut wallet, _) = get_funded_wallet_with_change(wildcard_keychain, non_wildcard_keychain); + let (mut wallet, _) = + get_funded_wallet_with_change(wildcard_keychain, Some(non_wildcard_keychain)); assert_eq!(wallet.balance().confirmed, Amount::from_sat(50000)); let addr = wallet @@ -4063,7 +4065,7 @@ fn test_tx_cancellation() { } let (mut wallet, _) = - get_funded_wallet_with_change(get_test_wpkh(), get_test_tr_single_sig_xprv()); + get_funded_wallet_with_change(get_test_wpkh(), Some(get_test_tr_single_sig_xprv())); let psbt1 = new_tx!(wallet); let change_derivation_1 = psbt1 diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index bda0e91cd..ee4ec700f 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -24,12 +24,15 @@ fn main() -> Result<(), anyhow::Error> { let changeset = db .aggregate_changesets() .map_err(|e| anyhow!("load changes error: {}", e))?; - let mut wallet = Wallet::new_or_load( - external_descriptor, - internal_descriptor, - changeset, - Network::Testnet, - )?; + let mut wallet = if let Some(changeset) = changeset { + Wallet::load_with_descriptors(external_descriptor, internal_descriptor, changeset)? + } else { + Wallet::new( + external_descriptor, + Some(internal_descriptor), + Network::Testnet, + )? + }; let address = wallet.next_unused_address(KeychainKind::External); if let Some(changeset) = wallet.take_staged() { diff --git a/example-crates/wallet_esplora_async/src/main.rs b/example-crates/wallet_esplora_async/src/main.rs index 0fd82b985..e6120e74d 100644 --- a/example-crates/wallet_esplora_async/src/main.rs +++ b/example-crates/wallet_esplora_async/src/main.rs @@ -21,12 +21,15 @@ async fn main() -> Result<(), anyhow::Error> { let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; let changeset = db.read()?; - let mut wallet = Wallet::new_or_load( - external_descriptor, - internal_descriptor, - changeset, - Network::Signet, - )?; + let mut wallet = if let Some(changeset) = changeset { + Wallet::load_with_descriptors(external_descriptor, internal_descriptor, changeset)? + } else { + Wallet::new( + external_descriptor, + Some(internal_descriptor), + Network::Testnet, + )? + }; let address = wallet.next_unused_address(KeychainKind::External); if let Some(changeset) = wallet.take_staged() { diff --git a/example-crates/wallet_esplora_blocking/src/main.rs b/example-crates/wallet_esplora_blocking/src/main.rs index 32211b04b..439d2ed37 100644 --- a/example-crates/wallet_esplora_blocking/src/main.rs +++ b/example-crates/wallet_esplora_blocking/src/main.rs @@ -20,12 +20,15 @@ fn main() -> Result<(), anyhow::Error> { let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; let changeset = db.aggregate_changesets()?; - let mut wallet = Wallet::new_or_load( - external_descriptor, - internal_descriptor, - changeset, - Network::Testnet, - )?; + let mut wallet = if let Some(changeset) = changeset { + Wallet::load_with_descriptors(external_descriptor, internal_descriptor, changeset)? + } else { + Wallet::new( + external_descriptor, + Some(internal_descriptor), + Network::Testnet, + )? + }; let address = wallet.next_unused_address(KeychainKind::External); if let Some(changeset) = wallet.take_staged() { diff --git a/example-crates/wallet_rpc/src/main.rs b/example-crates/wallet_rpc/src/main.rs index e09e6d762..83e321c40 100644 --- a/example-crates/wallet_rpc/src/main.rs +++ b/example-crates/wallet_rpc/src/main.rs @@ -92,12 +92,16 @@ fn main() -> anyhow::Result<()> { )?; let changeset = db.aggregate_changesets()?; - let mut wallet = Wallet::new_or_load( - &args.descriptor, - &args.change_descriptor, - changeset, - args.network, - )?; + let mut wallet = if let Some(changeset) = changeset { + Wallet::load_with_descriptors(&args.descriptor, &args.change_descriptor, changeset)? + } else { + Wallet::new( + &args.descriptor, + Some(&args.change_descriptor), + Network::Testnet, + )? + }; + println!( "Loaded wallet in {}s", start_load_wallet.elapsed().as_secs_f32()