zcash_address: Add `ZcashAddress::convert_if_network`

This, along with the corresponding `TryFromRawAddress` trait, enables
converting `ZcashAddress` into a network-agnostic type.

Closes zcash/librustzcash#564.
This commit is contained in:
Jack Grigg 2022-06-10 15:19:42 +00:00
parent b6ba216c0f
commit b8e8a0c491
3 changed files with 180 additions and 3 deletions

View File

@ -9,6 +9,8 @@ and this library adheres to Rust's notion of
### Added
- `zcash_address::ConversionError`
- `zcash_address::TryFromAddress`
- `zcash_address::TryFromRawAddress`
- `zcash_address::ZcashAddress::convert_if_network`
### Removed
- `zcash_address::FromAddress` (use `TryFromAddress` instead).

View File

@ -15,6 +15,8 @@ impl fmt::Display for UnsupportedAddress {
/// An error encountered while converting a parsed [`ZcashAddress`] into another type.
#[derive(Debug)]
pub enum ConversionError<E> {
/// The address is for the wrong network.
IncorrectNetwork { expected: Network, actual: Network },
/// The address type is not supported by the target type.
Unsupported(UnsupportedAddress),
/// A conversion error returned by the target type.
@ -30,6 +32,11 @@ impl<E> From<E> for ConversionError<E> {
impl<E: fmt::Display> fmt::Display for ConversionError<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::IncorrectNetwork { expected, actual } => write!(
f,
"Address is for {:?} but we expected {:?}",
actual, expected,
),
Self::Unsupported(e) => e.fmt(f),
Self::User(e) => e.fmt(f),
}
@ -40,12 +47,100 @@ impl Error for UnsupportedAddress {}
impl<E: Error + 'static> Error for ConversionError<E> {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ConversionError::Unsupported(_) => None,
ConversionError::IncorrectNetwork { .. } | ConversionError::Unsupported(_) => None,
ConversionError::User(e) => Some(e),
}
}
}
/// A helper trait for converting a [`ZcashAddress`] into a network-agnostic type.
///
/// A blanket implementation of [`TryFromAddress`] is provided for `(Network, T)` where
/// `T: TryFromRawAddress`.
///
/// [`ZcashAddress`]: crate::ZcashAddress
///
/// # Examples
///
/// ```
/// use zcash_address::{ConversionError, Network, TryFromRawAddress, UnsupportedAddress, ZcashAddress};
///
/// #[derive(Debug, PartialEq)]
/// struct MySapling([u8; 43]);
///
/// // Implement the TryFromRawAddress trait, overriding whichever conversion methods match
/// // your requirements for the resulting type.
/// impl TryFromRawAddress for MySapling {
/// // In this example we aren't checking the validity of the inner Sapling address,
/// // but your code should do so!
/// type Error = &'static str;
///
/// fn try_from_raw_sapling(data: [u8; 43]) -> Result<Self, ConversionError<Self::Error>> {
/// Ok(MySapling(data))
/// }
/// }
///
/// // For a supported address type, the conversion works.
/// let addr_string = "zs1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpq6d8g";
///
/// // You can use `ZcashAddress::convert_if_network` to get your type directly.
/// let addr: ZcashAddress = addr_string.parse().unwrap();
/// let converted = addr.convert_if_network::<MySapling>(Network::Main);
/// assert!(converted.is_ok());
/// assert_eq!(converted.unwrap(), MySapling([0; 43]));
///
/// // Using `ZcashAddress::convert` gives us the tuple `(network, converted_addr)`.
/// let addr: ZcashAddress = addr_string.parse().unwrap();
/// let converted = addr.convert::<(_, MySapling)>();
/// assert!(converted.is_ok());
/// assert_eq!(converted.unwrap(), (Network::Main, MySapling([0; 43])));
///
/// // For an unsupported address type, we get an error.
/// let addr: ZcashAddress = "t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs".parse().unwrap();
/// assert_eq!(
/// addr.convert::<(_, MySapling)>().unwrap_err().to_string(),
/// "Zcash transparent P2PKH addresses are not supported",
/// );
/// ```
pub trait TryFromRawAddress: Sized {
/// Conversion errors for the user type (e.g. failing to parse the data passed to
/// [`Self::try_from_sapling`] as a valid Sapling address).
type Error;
fn try_from_raw_sprout(data: sprout::Data) -> Result<Self, ConversionError<Self::Error>> {
let _ = data;
Err(ConversionError::Unsupported(UnsupportedAddress("Sprout")))
}
fn try_from_raw_sapling(data: sapling::Data) -> Result<Self, ConversionError<Self::Error>> {
let _ = data;
Err(ConversionError::Unsupported(UnsupportedAddress("Sapling")))
}
fn try_from_raw_unified(data: unified::Address) -> Result<Self, ConversionError<Self::Error>> {
let _ = data;
Err(ConversionError::Unsupported(UnsupportedAddress("Unified")))
}
fn try_from_raw_transparent_p2pkh(
data: p2pkh::Data,
) -> Result<Self, ConversionError<Self::Error>> {
let _ = data;
Err(ConversionError::Unsupported(UnsupportedAddress(
"transparent P2PKH",
)))
}
fn try_from_raw_transparent_p2sh(
data: p2sh::Data,
) -> Result<Self, ConversionError<Self::Error>> {
let _ = data;
Err(ConversionError::Unsupported(UnsupportedAddress(
"transparent P2SH",
)))
}
}
/// A helper trait for converting a [`ZcashAddress`] into another type.
///
/// [`ZcashAddress`]: crate::ZcashAddress
@ -58,7 +153,7 @@ impl<E: Error + 'static> Error for ConversionError<E> {
/// #[derive(Debug)]
/// struct MySapling([u8; 43]);
///
/// // Implement the FromAddress trait, overriding whichever conversion methods match your
/// // Implement the TryFromAddress trait, overriding whichever conversion methods match your
/// // requirements for the resulting type.
/// impl TryFromAddress for MySapling {
/// // In this example we aren't checking the validity of the inner Sapling address,
@ -137,6 +232,45 @@ pub trait TryFromAddress: Sized {
}
}
impl<T: TryFromRawAddress> TryFromAddress for (Network, T) {
type Error = T::Error;
fn try_from_sprout(
net: Network,
data: sprout::Data,
) -> Result<Self, ConversionError<Self::Error>> {
T::try_from_raw_sprout(data).map(|addr| (net, addr))
}
fn try_from_sapling(
net: Network,
data: sapling::Data,
) -> Result<Self, ConversionError<Self::Error>> {
T::try_from_raw_sapling(data).map(|addr| (net, addr))
}
fn try_from_unified(
net: Network,
data: unified::Address,
) -> Result<Self, ConversionError<Self::Error>> {
T::try_from_raw_unified(data).map(|addr| (net, addr))
}
fn try_from_transparent_p2pkh(
net: Network,
data: p2pkh::Data,
) -> Result<Self, ConversionError<Self::Error>> {
T::try_from_raw_transparent_p2pkh(data).map(|addr| (net, addr))
}
fn try_from_transparent_p2sh(
net: Network,
data: p2sh::Data,
) -> Result<Self, ConversionError<Self::Error>> {
T::try_from_raw_transparent_p2sh(data).map(|addr| (net, addr))
}
}
/// A helper trait for converting another type into a [`ZcashAddress`].
///
/// This trait is sealed and cannot be implemented for types outside this crate. Its

View File

@ -5,7 +5,9 @@ mod kind;
#[cfg(test)]
mod test_vectors;
pub use convert::{ConversionError, ToAddress, TryFromAddress, UnsupportedAddress};
pub use convert::{
ConversionError, ToAddress, TryFromAddress, TryFromRawAddress, UnsupportedAddress,
};
pub use encoding::ParseError;
pub use kind::unified;
@ -104,4 +106,43 @@ impl ZcashAddress {
AddressKind::P2sh(data) => T::try_from_transparent_p2sh(self.net, data),
}
}
/// Converts this address into another type, if it matches the expected network.
///
/// `convert_if_network` can convert into any type that implements the
/// [`TryFromRawAddress`] trait. This enables `ZcashAddress` to be used as a common
/// parsing and serialization interface for Zcash addresses, while delegating
/// operations on those addresses (such as constructing transactions) to downstream
/// crates.
///
/// If you want to get the encoded string for this address, use the [`encode`]
/// method or the [`Display` implementation] via [`address.to_string()`] instead.
///
/// [`encode`]: Self::encode
/// [`Display` implementation]: std::fmt::Display
/// [`address.to_string()`]: std::string::ToString
pub fn convert_if_network<T: TryFromRawAddress>(
self,
net: Network,
) -> Result<T, ConversionError<T::Error>> {
let network_matches = self.net == net;
// The Sprout and transparent address encodings use the same prefix for testnet
// and regtest, so we need to allow parsing testnet addresses as regtest.
let regtest_exception =
network_matches || (self.net == Network::Test && net == Network::Regtest);
match self.kind {
AddressKind::Sprout(data) if regtest_exception => T::try_from_raw_sprout(data),
AddressKind::Sapling(data) if network_matches => T::try_from_raw_sapling(data),
AddressKind::Unified(data) if network_matches => T::try_from_raw_unified(data),
AddressKind::P2pkh(data) if regtest_exception => {
T::try_from_raw_transparent_p2pkh(data)
}
AddressKind::P2sh(data) if regtest_exception => T::try_from_raw_transparent_p2sh(data),
_ => Err(ConversionError::IncorrectNetwork {
expected: net,
actual: self.net,
}),
}
}
}