]> Untitled Git - bdk/commitdiff
feat(bdk)!: have separate methods for creating and loading `Wallet`
author志宇 <hello@evanlinjin.me>
Wed, 25 Oct 2023 22:20:37 +0000 (06:20 +0800)
committer志宇 <hello@evanlinjin.me>
Wed, 15 Nov 2023 23:04:08 +0000 (07:04 +0800)
`Wallet::new` now creates a new wallet. `Wallet::load` loads an existing
wallet. The network type is now recoverable from persistence. Error
types have been simplified.

crates/bdk/src/error.rs
crates/bdk/src/wallet/mod.rs
example-crates/wallet_electrum/src/main.rs
example-crates/wallet_esplora_async/src/main.rs
example-crates/wallet_esplora_blocking/src/main.rs

index 3bd3dc6d32169a77c73368687be7bf78a3b9d4fe..fcb5a6f7b17ba159e15cedb46c3ae38e9aee18f8 100644 (file)
@@ -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<crate::wallet::NewNoPersistError> 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)),
-        }
-    }
-}
index 275487a581ff513cd4f96cccaead4bfb806bb7b0..05a8342664140254004f6dbb255ee0db4e6b1344 100644 (file)
@@ -128,12 +128,18 @@ pub struct ChangeSet {
         ConfirmationTimeHeightAnchor,
         keychain::ChangeSet<KeychainKind>,
     >,
+
+    /// Stores the network type of the wallet.
+    pub network: Option<Network>,
 }
 
 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<E>,
         network: Network,
-    ) -> Result<Self, NewNoPersistError> {
+    ) -> Result<Self, crate::descriptor::DescriptorError> {
         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"),
         })
     }
+
+    /// Creates a wallet that does not persist data, with a custom genesis hash.
+    pub fn new_no_persist_with_genesis_hash<E: IntoWalletDescriptor>(
+        descriptor: E,
+        change_descriptor: Option<E>,
+        network: Network,
+        genesis_hash: BlockHash,
+    ) -> Result<Self, crate::descriptor::DescriptorError> {
+        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"),
+            })
+    }
 }
 
-/// Error returned from [`Wallet::new_no_persist`]
 #[derive(Debug)]
-pub enum NewNoPersistError {
-    /// There was problem with the descriptors passed in
+/// Error returned from [`Wallet::new`]
+pub enum NewError<W> {
+    /// There was problem with the passed-in descriptor(s).
     Descriptor(crate::descriptor::DescriptorError),
-    /// We cannot determine the genesis hash from the network.
-    UnknownNetwork,
+    /// We were unable to write the wallet's data to the persistence backend.
+    Write(W),
 }
 
-impl fmt::Display for NewNoPersistError {
+impl<W> fmt::Display for NewError<W>
+where
+    W: fmt::Display,
+{
     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"
-            ),
+            NewError::Descriptor(e) => e.fmt(f),
+            NewError::Write(e) => e.fmt(f),
         }
     }
 }
 
 #[cfg(feature = "std")]
-impl std::error::Error for NewNoPersistError {}
+impl<W> std::error::Error for NewError<W> where W: core::fmt::Display + core::fmt::Debug {}
 
+/// An error that may occur when loading a [`Wallet`] from persistence.
 #[derive(Debug)]
-/// Error returned from [`Wallet::new`]
-pub enum NewError<PE> {
-    /// There was problem with the descriptors passed in
+pub enum LoadError<L> {
+    /// There was a 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,
+    /// 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<PE> fmt::Display for NewError<PE>
+impl<L> fmt::Display for LoadError<L>
 where
-    PE: fmt::Display,
+    L: 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"),
+            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<PE> std::error::Error for NewError<PE> where PE: core::fmt::Display + core::fmt::Debug {}
+impl<L> std::error::Error for LoadError<L> 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<D> Wallet<D> {
         change_descriptor: Option<E>,
         db: D,
         network: Network,
-    ) -> Result<Self, NewError<D::LoadError>>
+    ) -> Result<Self, NewError<D::WriteError>>
     where
         D: PersistBackend<ChangeSet>,
     {
-        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<E: IntoWalletDescriptor>(
+    pub fn new_with_genesis_hash<E: IntoWalletDescriptor>(
         descriptor: E,
         change_descriptor: Option<E>,
-        mut db: D,
+        db: D,
         network: Network,
-        custom_genesis_hash: Option<BlockHash>,
-    ) -> Result<Self, NewError<D::LoadError>>
+        genesis_hash: BlockHash,
+    ) -> Result<Self, NewError<D::WriteError>>
     where
         D: PersistBackend<ChangeSet>,
     {
         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<KeychainKind>,
@@ -351,42 +362,85 @@ impl<D> Wallet<D> {
             .index
             .add_keychain(KeychainKind::External, descriptor.clone());
         let signers = Arc::new(SignersContainer::build(keymap, &descriptor, &secp));
-        let change_signers = 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 change_signers = Arc::new(match change_descriptor {
+            Some(desc) => {
+                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 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,
+            change_signers,
+            network,
+            chain,
+            indexed_graph,
+            persist,
+            secp,
+        })
+    }
 
-        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);
+    /// Load [`Wallet`] from persistence.
+    pub fn load<E: IntoWalletDescriptor>(
+        descriptor: E,
+        change_descriptor: Option<E>,
+        mut db: D,
+    ) -> Result<Self, LoadError<D::LoadError>>
+    where
+        D: PersistBackend<ChangeSet>,
+    {
+        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::<KeychainKind>::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,
-            network,
             chain,
             indexed_graph,
             persist,
+            network,
             secp,
         })
     }
index a6d7ca52069b2f09ea64a2fe1c2def86c3e99762..da0c5e5dd3835f0facf150f0710f97b9e0cd829c 100644 (file)
@@ -18,16 +18,20 @@ use bdk_file_store::Store;
 
 fn main() -> Result<(), Box<dyn std::error::Error>> {
     let db_path = std::env::temp_dir().join("bdk-electrum-example");
-    let db = Store::<bdk::wallet::ChangeSet>::new_from_path(DB_MAGIC.as_bytes(), db_path)?;
+    let mut db = Store::<bdk::wallet::ChangeSet>::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);
index ff1bbfb6d9eb4312e1a2c53b9d8dac18d10fa1a5..5c7d09d59ac160b6c10da04e92c7a46df3a2f7d9 100644 (file)
@@ -16,16 +16,20 @@ const PARALLEL_REQUESTS: usize = 5;
 #[tokio::main]
 async fn main() -> Result<(), Box<dyn std::error::Error>> {
     let db_path = std::env::temp_dir().join("bdk-esplora-async-example");
-    let db = Store::<bdk::wallet::ChangeSet>::new_from_path(DB_MAGIC.as_bytes(), db_path)?;
+    let mut db = Store::<bdk::wallet::ChangeSet>::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);
index 71554b0a819c661ff67e6d7cadc4acf316a19565..f4de498cdb86335edaa53de837ac3d784f889196 100644 (file)
@@ -15,16 +15,20 @@ use bdk_file_store::Store;
 
 fn main() -> Result<(), Box<dyn std::error::Error>> {
     let db_path = std::env::temp_dir().join("bdk-esplora-example");
-    let db = Store::<bdk::wallet::ChangeSet>::new_from_path(DB_MAGIC.as_bytes(), db_path)?;
+    let mut db = Store::<bdk::wallet::ChangeSet>::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);