feat(bdk)!: have separate methods for creating and loading Wallet

`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.
This commit is contained in:
志宇 2023-10-26 06:20:37 +08:00
parent 7d5f31f6cc
commit 6cf3963c6c
No known key found for this signature in database
GPG Key ID: F6345C9837C2BDE8
5 changed files with 166 additions and 109 deletions

View File

@ -199,12 +199,3 @@ impl_error!(miniscript::Error, Miniscript);
impl_error!(MiniscriptPsbtError, MiniscriptPsbt); impl_error!(MiniscriptPsbtError, MiniscriptPsbt);
impl_error!(bitcoin::bip32::Error, Bip32); impl_error!(bitcoin::bip32::Error, Bip32);
impl_error!(bitcoin::psbt::Error, Psbt); 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)),
}
}
}

View File

@ -128,12 +128,18 @@ pub struct ChangeSet {
ConfirmationTimeHeightAnchor, ConfirmationTimeHeightAnchor,
keychain::ChangeSet<KeychainKind>, keychain::ChangeSet<KeychainKind>,
>, >,
/// Stores the network type of the wallet.
pub network: Option<Network>,
} }
impl Append for ChangeSet { impl Append for ChangeSet {
fn append(&mut self, other: Self) { fn append(&mut self, other: Self) {
Append::append(&mut self.chain, other.chain); Append::append(&mut self.chain, other.chain);
Append::append(&mut self.indexed_tx_graph, other.indexed_tx_graph); 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 { fn is_empty(&self) -> bool {
@ -225,75 +231,81 @@ impl Wallet {
descriptor: E, descriptor: E,
change_descriptor: Option<E>, change_descriptor: Option<E>,
network: Network, network: Network,
) -> Result<Self, NewNoPersistError> { ) -> Result<Self, crate::descriptor::DescriptorError> {
Self::new(descriptor, change_descriptor, (), network).map_err(|e| match e { Self::new(descriptor, change_descriptor, (), network).map_err(|e| match e {
NewError::Descriptor(e) => NewNoPersistError::Descriptor(e), NewError::Descriptor(e) => e,
NewError::Persist(_) | NewError::InvalidPersistenceGenesis => { NewError::Write(_) => unreachable!("mock-write must always succeed"),
unreachable!("no persistence so it can't fail")
}
NewError::UnknownNetwork => NewNoPersistError::UnknownNetwork,
}) })
} }
}
/// Error returned from [`Wallet::new_no_persist`] /// Creates a wallet that does not persist data, with a custom genesis hash.
#[derive(Debug)] pub fn new_no_persist_with_genesis_hash<E: IntoWalletDescriptor>(
pub enum NewNoPersistError { descriptor: E,
/// There was problem with the descriptors passed in change_descriptor: Option<E>,
Descriptor(crate::descriptor::DescriptorError), network: Network,
/// We cannot determine the genesis hash from the network. genesis_hash: BlockHash,
UnknownNetwork, ) -> Result<Self, crate::descriptor::DescriptorError> {
} Self::new_with_genesis_hash(descriptor, change_descriptor, (), network, genesis_hash)
.map_err(|e| match e {
impl fmt::Display for NewNoPersistError { NewError::Descriptor(e) => e,
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { NewError::Write(_) => unreachable!("mock-write must always succeed"),
match self { })
NewNoPersistError::Descriptor(e) => e.fmt(f),
NewNoPersistError::UnknownNetwork => write!(
f,
"unknown network - genesis block hash needs to be provided explicitly"
),
}
} }
} }
#[cfg(feature = "std")]
impl std::error::Error for NewNoPersistError {}
#[derive(Debug)] #[derive(Debug)]
/// Error returned from [`Wallet::new`] /// Error returned from [`Wallet::new`]
pub enum NewError<PE> { pub enum NewError<W> {
/// There was problem with the descriptors passed in /// There was problem with the passed-in descriptor(s).
Descriptor(crate::descriptor::DescriptorError), Descriptor(crate::descriptor::DescriptorError),
/// We were unable to load the wallet's data from the persistence backend /// We were unable to write the wallet's data to the persistence backend.
Persist(PE), Write(W),
/// 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,
} }
impl<PE> fmt::Display for NewError<PE> impl<W> fmt::Display for NewError<W>
where where
PE: fmt::Display, W: fmt::Display,
{ {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
NewError::Descriptor(e) => e.fmt(f), NewError::Descriptor(e) => e.fmt(f),
NewError::Persist(e) => { NewError::Write(e) => e.fmt(f),
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"),
} }
} }
} }
#[cfg(feature = "std")] #[cfg(feature = "std")]
impl<PE> std::error::Error for NewError<PE> where PE: core::fmt::Display + core::fmt::Debug {} 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)]
pub enum LoadError<L> {
/// 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<L> fmt::Display for LoadError<L>
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<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`]. /// An error that may occur when inserting a transaction into [`Wallet`].
#[derive(Debug)] #[derive(Debug)]
@ -316,30 +328,29 @@ impl<D> Wallet<D> {
change_descriptor: Option<E>, change_descriptor: Option<E>,
db: D, db: D,
network: Network, network: Network,
) -> Result<Self, NewError<D::LoadError>> ) -> Result<Self, NewError<D::WriteError>>
where where
D: PersistBackend<ChangeSet>, 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. /// Create a new [`Wallet`] with a custom genesis hash.
/// ///
/// This is like [`Wallet::new`] with an additional `custom_genesis_hash` parameter. /// 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, descriptor: E,
change_descriptor: Option<E>, change_descriptor: Option<E>,
mut db: D, db: D,
network: Network, network: Network,
custom_genesis_hash: Option<BlockHash>, genesis_hash: BlockHash,
) -> Result<Self, NewError<D::LoadError>> ) -> Result<Self, NewError<D::WriteError>>
where where
D: PersistBackend<ChangeSet>, D: PersistBackend<ChangeSet>,
{ {
let secp = Secp256k1::new(); let secp = Secp256k1::new();
let genesis_hash = let (chain, _) = LocalChain::from_genesis_hash(genesis_hash);
custom_genesis_hash.unwrap_or_else(|| genesis_block(network).block_hash());
let (mut chain, _) = LocalChain::from_genesis_hash(genesis_hash);
let mut indexed_graph = IndexedTxGraph::< let mut indexed_graph = IndexedTxGraph::<
ConfirmationTimeHeightAnchor, ConfirmationTimeHeightAnchor,
KeychainTxOutIndex<KeychainKind>, KeychainTxOutIndex<KeychainKind>,
@ -351,34 +362,27 @@ impl<D> Wallet<D> {
.index .index
.add_keychain(KeychainKind::External, descriptor.clone()); .add_keychain(KeychainKind::External, descriptor.clone());
let signers = Arc::new(SignersContainer::build(keymap, &descriptor, &secp)); 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) => { Some(desc) => {
let (change_descriptor, change_keymap) = let (descriptor, keymap) = into_wallet_descriptor_checked(desc, &secp, network)
into_wallet_descriptor_checked(desc, &secp, network) .map_err(NewError::Descriptor)?;
.map_err(NewError::Descriptor)?; let signers = SignersContainer::build(keymap, &descriptor, &secp);
let change_signers = Arc::new(SignersContainer::build(
change_keymap,
&change_descriptor,
&secp,
));
indexed_graph indexed_graph
.index .index
.add_keychain(KeychainKind::Internal, change_descriptor); .add_keychain(KeychainKind::Internal, descriptor);
signers
change_signers
} }
None => Arc::new(SignersContainer::new()), None => SignersContainer::new(),
}; });
let changeset = db.load_from_persistence().map_err(NewError::Persist)?; let mut persist = Persist::new(db);
chain persist.stage(ChangeSet {
.apply_changeset(&changeset.chain) chain: chain.initial_changeset(),
.map_err(|_| NewError::InvalidPersistenceGenesis)?; indexed_tx_graph: indexed_graph.initial_changeset(),
indexed_graph.apply_changeset(changeset.indexed_tx_graph); network: Some(network),
});
let persist = Persist::new(db); persist.commit().map_err(NewError::Write)?;
Ok(Wallet { Ok(Wallet {
signers, signers,
@ -391,6 +395,56 @@ impl<D> Wallet<D> {
}) })
} }
/// 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,
chain,
indexed_graph,
persist,
network,
secp,
})
}
/// Get the Bitcoin network the wallet is using. /// Get the Bitcoin network the wallet is using.
pub fn network(&self) -> Network { pub fn network(&self) -> Network {
self.network self.network

View File

@ -18,16 +18,20 @@ use bdk_file_store::Store;
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let db_path = std::env::temp_dir().join("bdk-electrum-example"); 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 external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
let mut wallet = Wallet::new( let mut wallet = if db.is_empty()? {
external_descriptor, Wallet::new(
Some(internal_descriptor), external_descriptor,
db, Some(internal_descriptor),
Network::Testnet, db,
)?; Network::Testnet,
)?
} else {
Wallet::load(external_descriptor, Some(internal_descriptor), db)?
};
let address = wallet.get_address(bdk::wallet::AddressIndex::New); let address = wallet.get_address(bdk::wallet::AddressIndex::New);
println!("Generated Address: {}", address); println!("Generated Address: {}", address);

View File

@ -16,16 +16,20 @@ const PARALLEL_REQUESTS: usize = 5;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
let db_path = std::env::temp_dir().join("bdk-esplora-async-example"); 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 external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
let mut wallet = Wallet::new( let mut wallet = if db.is_empty()? {
external_descriptor, Wallet::new(
Some(internal_descriptor), external_descriptor,
db, Some(internal_descriptor),
Network::Testnet, db,
)?; Network::Testnet,
)?
} else {
Wallet::load(external_descriptor, Some(internal_descriptor), db)?
};
let address = wallet.get_address(AddressIndex::New); let address = wallet.get_address(AddressIndex::New);
println!("Generated Address: {}", address); println!("Generated Address: {}", address);

View File

@ -15,16 +15,20 @@ use bdk_file_store::Store;
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let db_path = std::env::temp_dir().join("bdk-esplora-example"); 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 external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
let mut wallet = Wallet::new( let mut wallet = if db.is_empty()? {
external_descriptor, Wallet::new(
Some(internal_descriptor), external_descriptor,
db, Some(internal_descriptor),
Network::Testnet, db,
)?; Network::Testnet,
)?
} else {
Wallet::load(external_descriptor, Some(internal_descriptor), db)?
};
let address = wallet.get_address(AddressIndex::New); let address = wallet.get_address(AddressIndex::New);
println!("Generated Address: {}", address); println!("Generated Address: {}", address);