Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Slight rethinking of Wallet constructors (breaking change) #1500

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/wallet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ let mut db =
// Create a wallet with initial wallet data read from the file store.
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 changeset = db.aggregate_changesets().expect("changeset loaded").expect("changeset");
let mut wallet =
Wallet::new_or_load(descriptor, change_descriptor, changeset, Network::Testnet)
Wallet::load(descriptor, change_descriptor, changeset, Network::Testnet)
.expect("create or load wallet");

// Get a new address to receive bitcoin.
Expand Down
264 changes: 110 additions & 154 deletions crates/wallet/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// Method [`load_no_priv`] and [`load`] may return this error.
///
/// [`load_from_changeset`]: Wallet::load_from_changeset
/// [`load_no_priv`]: Wallet::load_no_priv
/// [`load`]: Wallet::load
#[derive(Debug)]
pub enum LoadError {
/// There was a problem with the passed-in descriptor(s).
Expand All @@ -215,34 +216,6 @@ 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.
Expand All @@ -257,7 +230,7 @@ pub enum NewOrLoadError {
/// The network type loaded from persistence.
got: Option<Network>,
},
/// 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<ExtendedDescriptor>,
Expand All @@ -266,17 +239,22 @@ 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 } => {
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:?}")
}
LoadError::LoadedGenesisDoesNotMatch { expected, got } => {
write!(f, "loaded genesis hash is not {}, got {:?}", expected, got)
}
NewOrLoadError::LoadedNetworkDoesNotMatch { expected, got } => {
LoadError::LoadedNetworkDoesNotMatch { expected, got } => {
write!(f, "loaded network type is not {}, got {:?}", expected, got)
}
NewOrLoadError::LoadedDescriptorDoesNotMatch { got, keychain } => {
LoadError::LoadedDescriptorDoesNotMatch { got, keychain } => {
write!(
f,
"loaded descriptor is different from what was provided, got {:?} for keychain {:?}",
Expand All @@ -288,7 +266,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)]
Expand Down Expand Up @@ -336,8 +314,8 @@ impl Wallet {

/// Initialize an empty [`Wallet`] with a custom genesis hash.
///
/// This is like [`Wallet::new`] with an additional `genesis_hash` parameter. This is useful
/// for syncing from alternative networks.
/// This is like [`Wallet::new`] with an additional `genesis_hash` parameter. This is useful for
/// syncing from alternative networks.
pub fn new_with_genesis_hash<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: E,
Expand Down Expand Up @@ -401,7 +379,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_no_priv(changeset)?;
///
/// external_signer_container.signers().into_iter()
/// .for_each(|s| wallet.add_signer(KeychainKind::External, SignerOrdering::default(), s.clone()));
Expand All @@ -411,9 +389,9 @@ impl Wallet {
/// # }
/// ```
///
/// Alternatively, you can call [`Wallet::new_or_load`], which will add the private keys of the
/// Alternatively, you can call [`Wallet::load`], which will add the private keys of the
/// passed-in descriptors to the [`Wallet`].
pub fn load_from_changeset(changeset: ChangeSet) -> Result<Self, LoadError> {
pub fn load_no_priv(changeset: ChangeSet) -> Result<Self, LoadError> {
let secp = Secp256k1::new();
let network = changeset.network.ok_or(LoadError::MissingNetwork)?;
let chain =
Expand Down Expand Up @@ -454,7 +432,8 @@ impl Wallet {
})
}

/// Either loads [`Wallet`] from the given [`ChangeSet`] or initializes it if one does not exist.
/// Loads [`Wallet`] from the given [`ChangeSet`] and add the private keys of the passed-in
/// descriptors to the [`Wallet`].
///
/// This method will fail if the loaded [`ChangeSet`] has different parameters to those provided.
///
Expand All @@ -469,128 +448,105 @@ impl Wallet {
/// 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)?;
/// if let Some(changeset) = changeset {
/// let mut wallet = Wallet::load(external_descriptor, internal_descriptor, changeset, Testnet)?;
/// } else {
/// let mut wallet = Wallet::new(external_descriptor, internal_descriptor, Testnet)?;
/// }
/// # Ok::<(), anyhow::Error>(())
/// ```
pub fn new_or_load<E: IntoWalletDescriptor>(
pub fn load<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: E,
changeset: Option<ChangeSet>,
changeset: ChangeSet,
network: Network,
) -> Result<Self, NewOrLoadError> {
) -> Result<Self, LoadError> {
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<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: E,
changeset: Option<ChangeSet>,
network: Network,
genesis_hash: BlockHash,
) -> Result<Self, NewOrLoadError> {
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,
let mut wallet = Self::load_no_priv(changeset).map_err(|e| match e {
LoadError::Descriptor(e) => LoadError::Descriptor(e),
LoadError::MissingNetwork => LoadError::LoadedNetworkDoesNotMatch {
expected: network,
got: None,
},
LoadError::MissingGenesis => LoadError::LoadedGenesisDoesNotMatch {
expected: genesis_hash,
got: None,
},
LoadError::MissingDescriptor(keychain) => {
LoadError::LoadedDescriptorDoesNotMatch {
got: None,
},
LoadError::MissingDescriptor(keychain) => {
NewOrLoadError::LoadedDescriptorDoesNotMatch {
got: None,
keychain,
}
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(),
)
});
}
// TODO: This is probably not the best way to handle this?
_ => panic!("load_no_priv should only be able to return Descriptor, MissingNetwork, MissingGenesis, MissingDescriptor"),
})?;
if wallet.network != network {
return Err(LoadError::LoadedNetworkDoesNotMatch {
expected: network,
got: Some(wallet.network),
});
}
if wallet.chain.genesis_hash() != genesis_hash {
return Err(LoadError::LoadedGenesisDoesNotMatch {
expected: genesis_hash,
got: Some(wallet.chain.genesis_hash()),
});
}

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(),
)
});
}
let (expected_descriptor, expected_descriptor_keymap) = descriptor
.into_wallet_descriptor(&wallet.secp, 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, 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.
Expand Down Expand Up @@ -648,7 +604,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_no_priv(changeset).expect("load wallet");
/// let next_address = wallet.reveal_next_address(KeychainKind::External);
/// if let Some(changeset) = wallet.take_staged() {
/// db.write(&changeset)?;
Expand Down
Loading
Loading