diff --git a/crates/bdk/src/error.rs b/crates/bdk/src/error.rs index 3bd3dc6d..fcb5a6f7 100644 --- a/crates/bdk/src/error.rs +++ b/crates/bdk/src/error.rs @@ -199,12 +199,3 @@ impl_error!(miniscript::Error, Miniscript); impl_error!(MiniscriptPsbtError, MiniscriptPsbt); impl_error!(bitcoin::bip32::Error, Bip32); impl_error!(bitcoin::psbt::Error, Psbt); - -impl From for Error { - fn from(e: crate::wallet::NewNoPersistError) -> Self { - match e { - wallet::NewNoPersistError::Descriptor(e) => Error::Descriptor(e), - unknown_network_err => Error::Generic(format!("{}", unknown_network_err)), - } - } -} diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 275487a5..05a83426 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -128,12 +128,18 @@ pub struct ChangeSet { ConfirmationTimeHeightAnchor, keychain::ChangeSet, >, + + /// Stores the network type of the wallet. + pub network: Option, } impl Append for ChangeSet { fn append(&mut self, other: Self) { Append::append(&mut self.chain, other.chain); Append::append(&mut self.indexed_tx_graph, other.indexed_tx_graph); + if other.network.is_some() { + self.network = other.network; + } } fn is_empty(&self) -> bool { @@ -225,75 +231,81 @@ impl Wallet { descriptor: E, change_descriptor: Option, network: Network, - ) -> Result { + ) -> Result { Self::new(descriptor, change_descriptor, (), network).map_err(|e| match e { - NewError::Descriptor(e) => NewNoPersistError::Descriptor(e), - NewError::Persist(_) | NewError::InvalidPersistenceGenesis => { - unreachable!("no persistence so it can't fail") - } - NewError::UnknownNetwork => NewNoPersistError::UnknownNetwork, + NewError::Descriptor(e) => e, + NewError::Write(_) => unreachable!("mock-write must always succeed"), }) } -} -/// Error returned from [`Wallet::new_no_persist`] -#[derive(Debug)] -pub enum NewNoPersistError { - /// There was problem with the descriptors passed in - Descriptor(crate::descriptor::DescriptorError), - /// We cannot determine the genesis hash from the network. - UnknownNetwork, -} - -impl fmt::Display for NewNoPersistError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - NewNoPersistError::Descriptor(e) => e.fmt(f), - NewNoPersistError::UnknownNetwork => write!( - f, - "unknown network - genesis block hash needs to be provided explicitly" - ), - } + /// Creates a wallet that does not persist data, with a custom genesis hash. + pub fn new_no_persist_with_genesis_hash( + descriptor: E, + change_descriptor: Option, + network: Network, + genesis_hash: BlockHash, + ) -> Result { + Self::new_with_genesis_hash(descriptor, change_descriptor, (), network, genesis_hash) + .map_err(|e| match e { + NewError::Descriptor(e) => e, + NewError::Write(_) => unreachable!("mock-write must always succeed"), + }) } } -#[cfg(feature = "std")] -impl std::error::Error for NewNoPersistError {} - #[derive(Debug)] /// Error returned from [`Wallet::new`] -pub enum NewError { - /// There was problem with the descriptors passed in +pub enum NewError { + /// There was problem with the passed-in descriptor(s). Descriptor(crate::descriptor::DescriptorError), - /// We were unable to load the wallet's data from the persistence backend - Persist(PE), - /// We cannot determine the genesis hash from the network - UnknownNetwork, - /// The genesis block hash is either missing from persistence or has an unexpected value - InvalidPersistenceGenesis, + /// We were unable to write the wallet's data to the persistence backend. + Write(W), } -impl fmt::Display for NewError +impl fmt::Display for NewError where - PE: fmt::Display, + W: fmt::Display, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { NewError::Descriptor(e) => e.fmt(f), - NewError::Persist(e) => { - write!(f, "failed to load wallet from persistence backend: {}", e) - } - NewError::UnknownNetwork => write!( - f, - "unknown network - genesis block hash needs to be provided explicitly" - ), - NewError::InvalidPersistenceGenesis => write!(f, "the genesis block hash is either missing from persistence or has an unexpected value"), + NewError::Write(e) => e.fmt(f), } } } #[cfg(feature = "std")] -impl std::error::Error for NewError where PE: core::fmt::Display + core::fmt::Debug {} +impl std::error::Error for NewError where W: core::fmt::Display + core::fmt::Debug {} + +/// An error that may occur when loading a [`Wallet`] from persistence. +#[derive(Debug)] +pub enum LoadError { + /// There was a problem with the passed-in descriptor(s). + Descriptor(crate::descriptor::DescriptorError), + /// Loading data from the persistence backend failed. + Load(L), + /// Data loaded from persistence is missing network type. + MissingNetwork, + /// Data loaded from persistence is missing genesis hash. + MissingGenesis, +} + +impl fmt::Display for LoadError +where + L: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LoadError::Descriptor(e) => e.fmt(f), + LoadError::Load(e) => e.fmt(f), + LoadError::MissingNetwork => write!(f, "loaded data is missing network type"), + LoadError::MissingGenesis => write!(f, "loaded data is missing genesis hash"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for LoadError where L: core::fmt::Display + core::fmt::Debug {} /// An error that may occur when inserting a transaction into [`Wallet`]. #[derive(Debug)] @@ -316,30 +328,29 @@ impl Wallet { change_descriptor: Option, db: D, network: Network, - ) -> Result> + ) -> Result> where D: PersistBackend, { - Self::with_custom_genesis_hash(descriptor, change_descriptor, db, network, None) + let genesis_hash = genesis_block(network).block_hash(); + Self::new_with_genesis_hash(descriptor, change_descriptor, db, network, genesis_hash) } /// Create a new [`Wallet`] with a custom genesis hash. /// /// This is like [`Wallet::new`] with an additional `custom_genesis_hash` parameter. - pub fn with_custom_genesis_hash( + pub fn new_with_genesis_hash( descriptor: E, change_descriptor: Option, - mut db: D, + db: D, network: Network, - custom_genesis_hash: Option, - ) -> Result> + genesis_hash: BlockHash, + ) -> Result> where D: PersistBackend, { let secp = Secp256k1::new(); - let genesis_hash = - custom_genesis_hash.unwrap_or_else(|| genesis_block(network).block_hash()); - let (mut chain, _) = LocalChain::from_genesis_hash(genesis_hash); + let (chain, _) = LocalChain::from_genesis_hash(genesis_hash); let mut indexed_graph = IndexedTxGraph::< ConfirmationTimeHeightAnchor, KeychainTxOutIndex, @@ -351,34 +362,27 @@ impl Wallet { .index .add_keychain(KeychainKind::External, descriptor.clone()); let signers = Arc::new(SignersContainer::build(keymap, &descriptor, &secp)); - let change_signers = match change_descriptor { + + let change_signers = Arc::new(match change_descriptor { Some(desc) => { - let (change_descriptor, change_keymap) = - into_wallet_descriptor_checked(desc, &secp, network) - .map_err(NewError::Descriptor)?; - - let change_signers = Arc::new(SignersContainer::build( - change_keymap, - &change_descriptor, - &secp, - )); - + let (descriptor, keymap) = into_wallet_descriptor_checked(desc, &secp, network) + .map_err(NewError::Descriptor)?; + let signers = SignersContainer::build(keymap, &descriptor, &secp); indexed_graph .index - .add_keychain(KeychainKind::Internal, change_descriptor); - - change_signers + .add_keychain(KeychainKind::Internal, descriptor); + signers } - None => Arc::new(SignersContainer::new()), - }; + None => SignersContainer::new(), + }); - let changeset = db.load_from_persistence().map_err(NewError::Persist)?; - chain - .apply_changeset(&changeset.chain) - .map_err(|_| NewError::InvalidPersistenceGenesis)?; - indexed_graph.apply_changeset(changeset.indexed_tx_graph); - - let persist = Persist::new(db); + let mut persist = Persist::new(db); + persist.stage(ChangeSet { + chain: chain.initial_changeset(), + indexed_tx_graph: indexed_graph.initial_changeset(), + network: Some(network), + }); + persist.commit().map_err(NewError::Write)?; Ok(Wallet { signers, @@ -391,6 +395,56 @@ impl Wallet { }) } + /// Load [`Wallet`] from persistence. + pub fn load( + descriptor: E, + change_descriptor: Option, + mut db: D, + ) -> Result> + where + D: PersistBackend, + { + let secp = Secp256k1::new(); + + let changeset = db.load_from_persistence().map_err(LoadError::Load)?; + 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, keymap) = into_wallet_descriptor_checked(descriptor, &secp, network) + .map_err(LoadError::Descriptor)?; + let signers = Arc::new(SignersContainer::build(keymap, &descriptor, &secp)); + index.add_keychain(KeychainKind::External, descriptor); + + let change_signers = Arc::new(match change_descriptor { + Some(descriptor) => { + let (descriptor, keymap) = + into_wallet_descriptor_checked(descriptor, &secp, network) + .map_err(LoadError::Descriptor)?; + let signers = SignersContainer::build(keymap, &descriptor, &secp); + index.add_keychain(KeychainKind::Internal, descriptor); + signers + } + None => SignersContainer::new(), + }); + + let indexed_graph = IndexedTxGraph::new(index); + let persist = Persist::new(db); + + Ok(Wallet { + signers, + change_signers, + chain, + indexed_graph, + persist, + network, + secp, + }) + } + /// Get the Bitcoin network the wallet is using. pub fn network(&self) -> Network { self.network diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index a6d7ca52..da0c5e5d 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -18,16 +18,20 @@ use bdk_file_store::Store; fn main() -> Result<(), Box> { let db_path = std::env::temp_dir().join("bdk-electrum-example"); - let db = Store::::new_from_path(DB_MAGIC.as_bytes(), db_path)?; + let mut db = Store::::new_from_path(DB_MAGIC.as_bytes(), db_path)?; let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; - let mut wallet = Wallet::new( - external_descriptor, - Some(internal_descriptor), - db, - Network::Testnet, - )?; + let mut wallet = if db.is_empty()? { + Wallet::new( + external_descriptor, + Some(internal_descriptor), + db, + Network::Testnet, + )? + } else { + Wallet::load(external_descriptor, Some(internal_descriptor), db)? + }; let address = wallet.get_address(bdk::wallet::AddressIndex::New); println!("Generated Address: {}", address); diff --git a/example-crates/wallet_esplora_async/src/main.rs b/example-crates/wallet_esplora_async/src/main.rs index ff1bbfb6..5c7d09d5 100644 --- a/example-crates/wallet_esplora_async/src/main.rs +++ b/example-crates/wallet_esplora_async/src/main.rs @@ -16,16 +16,20 @@ const PARALLEL_REQUESTS: usize = 5; #[tokio::main] async fn main() -> Result<(), Box> { let db_path = std::env::temp_dir().join("bdk-esplora-async-example"); - let db = Store::::new_from_path(DB_MAGIC.as_bytes(), db_path)?; + let mut db = Store::::new_from_path(DB_MAGIC.as_bytes(), db_path)?; let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; - let mut wallet = Wallet::new( - external_descriptor, - Some(internal_descriptor), - db, - Network::Testnet, - )?; + let mut wallet = if db.is_empty()? { + Wallet::new( + external_descriptor, + Some(internal_descriptor), + db, + Network::Testnet, + )? + } else { + Wallet::load(external_descriptor, Some(internal_descriptor), db)? + }; let address = wallet.get_address(AddressIndex::New); println!("Generated Address: {}", address); diff --git a/example-crates/wallet_esplora_blocking/src/main.rs b/example-crates/wallet_esplora_blocking/src/main.rs index 71554b0a..f4de498c 100644 --- a/example-crates/wallet_esplora_blocking/src/main.rs +++ b/example-crates/wallet_esplora_blocking/src/main.rs @@ -15,16 +15,20 @@ use bdk_file_store::Store; fn main() -> Result<(), Box> { let db_path = std::env::temp_dir().join("bdk-esplora-example"); - let db = Store::::new_from_path(DB_MAGIC.as_bytes(), db_path)?; + let mut db = Store::::new_from_path(DB_MAGIC.as_bytes(), db_path)?; let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; - let mut wallet = Wallet::new( - external_descriptor, - Some(internal_descriptor), - db, - Network::Testnet, - )?; + let mut wallet = if db.is_empty()? { + Wallet::new( + external_descriptor, + Some(internal_descriptor), + db, + Network::Testnet, + )? + } else { + Wallet::load(external_descriptor, Some(internal_descriptor), db)? + }; let address = wallet.get_address(AddressIndex::New); println!("Generated Address: {}", address);