diff --git a/core/src/types/address.rs b/core/src/types/address.rs
index 79a27a440fa..b4ef7bff76a 100644
--- a/core/src/types/address.rs
+++ b/core/src/types/address.rs
@@ -6,26 +6,20 @@ use std::fmt::{Debug, Display};
use std::hash::Hash;
use std::str::FromStr;
-use bech32::{self, FromBase32, ToBase32, Variant};
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
-use thiserror::Error;
-use crate::types::key;
+use crate::impl_display_and_from_str_via_format;
use crate::types::key::PublicKeyHash;
+use crate::types::{key, string_encoding};
/// The length of an established [`Address`] encoded with Borsh.
pub const ESTABLISHED_ADDRESS_BYTES_LEN: usize = 45;
/// The length of [`Address`] encoded with Bech32m.
-pub const ADDRESS_LEN: usize = 79 + ADDRESS_HRP.len();
+pub const ADDRESS_LEN: usize = 79 + string_encoding::hrp_len::
();
-/// human-readable part of Bech32m encoded address
-// TODO use "a" for live network
-const ADDRESS_HRP: &str = "atest";
-/// We're using "Bech32m" variant
-pub const BECH32M_VARIANT: bech32::Variant = Variant::Bech32m;
pub(crate) const HASH_LEN: usize = 40;
/// An address string before bech32m encoding must be this size.
@@ -71,6 +65,12 @@ mod internal {
"ano::ETH Bridge Address ";
}
+/// Error from decoding address from string
+pub type DecodeError = string_encoding::DecodeError;
+
+/// Result of decoding address from string
+pub type Result = std::result::Result;
+
/// Fixed-length address strings prefix for established addresses.
const PREFIX_ESTABLISHED: &str = "est";
/// Fixed-length address strings prefix for implicit addresses.
@@ -80,24 +80,6 @@ const PREFIX_INTERNAL: &str = "ano";
/// Fixed-length address strings prefix for IBC addresses.
const PREFIX_IBC: &str = "ibc";
-#[allow(missing_docs)]
-#[derive(Error, Debug)]
-pub enum DecodeError {
- #[error("Error decoding address from Bech32m: {0}")]
- DecodeBech32(bech32::Error),
- #[error("Error decoding address from base32: {0}")]
- DecodeBase32(bech32::Error),
- #[error("Unexpected Bech32m human-readable part {0}, expected {1}")]
- UnexpectedBech32Prefix(String, String),
- #[error("Unexpected Bech32m variant {0:?}, expected {BECH32M_VARIANT:?}")]
- UnexpectedBech32Variant(bech32::Variant),
- #[error("Invalid address encoding")]
- InvalidInnerEncoding(std::io::Error),
-}
-
-/// Result of a function that may fail
-pub type Result = std::result::Result;
-
/// An account's address
#[derive(
Clone,
@@ -122,34 +104,12 @@ pub enum Address {
impl Address {
/// Encode an address with Bech32m encoding
pub fn encode(&self) -> String {
- let bytes = self.to_fixed_len_string();
- bech32::encode(ADDRESS_HRP, bytes.to_base32(), BECH32M_VARIANT)
- .unwrap_or_else(|_| {
- panic!(
- "The human-readable part {} should never cause a failure",
- ADDRESS_HRP
- )
- })
+ string_encoding::Format::encode(self)
}
/// Decode an address from Bech32m encoding
pub fn decode(string: impl AsRef) -> Result {
- let (prefix, hash_base32, variant) = bech32::decode(string.as_ref())
- .map_err(DecodeError::DecodeBech32)?;
- if prefix != ADDRESS_HRP {
- return Err(DecodeError::UnexpectedBech32Prefix(
- prefix,
- ADDRESS_HRP.into(),
- ));
- }
- match variant {
- BECH32M_VARIANT => {}
- _ => return Err(DecodeError::UnexpectedBech32Variant(variant)),
- }
- let bytes: Vec = FromBase32::from_base32(&hash_base32)
- .map_err(DecodeError::DecodeBase32)?;
- Self::try_from_fixed_len_string(&mut &bytes[..])
- .map_err(DecodeError::InvalidInnerEncoding)
+ string_encoding::Format::decode(string)
}
/// Try to get a raw hash of an address, only defined for established and
@@ -163,7 +123,7 @@ impl Address {
}
/// Convert an address to a fixed length 7-bit ascii string bytes
- fn to_fixed_len_string(&self) -> Vec {
+ pub fn to_fixed_len_string(&self) -> Vec {
let mut string = match self {
Address::Established(EstablishedAddress { hash }) => {
format!("{}::{}", PREFIX_ESTABLISHED, hash)
@@ -209,7 +169,7 @@ impl Address {
}
/// Try to parse an address from fixed-length utf-8 encoded address string.
- fn try_from_fixed_len_string(buf: &mut &[u8]) -> std::io::Result {
+ pub fn try_from_fixed_len_string(buf: &mut &[u8]) -> std::io::Result {
use std::io::{Error, ErrorKind};
let string = std::str::from_utf8(buf)
.map_err(|err| Error::new(ErrorKind::InvalidData, err))?;
@@ -302,6 +262,20 @@ impl Address {
}
}
+impl string_encoding::Format for Address {
+ const HRP: &'static str = string_encoding::ADDRESS_HRP;
+
+ fn to_bytes(&self) -> Vec {
+ Self::to_fixed_len_string(self)
+ }
+
+ fn decode_bytes(bytes: &[u8]) -> std::result::Result {
+ Self::try_from_fixed_len_string(&mut &bytes[..])
+ }
+}
+
+impl_display_and_from_str_via_format!(Address);
+
impl serde::Serialize for Address {
fn serialize(
&self,
@@ -326,26 +300,12 @@ impl<'de> serde::Deserialize<'de> for Address {
}
}
-impl Display for Address {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}", self.encode())
- }
-}
-
impl Debug for Address {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.pretty_fmt(f)
}
}
-impl FromStr for Address {
- type Err = DecodeError;
-
- fn from_str(s: &str) -> Result {
- Address::decode(s)
- }
-}
-
/// An established address is generated on-chain
#[derive(
Debug,
diff --git a/core/src/types/masp.rs b/core/src/types/masp.rs
index c0ccb67d1ee..7a0fd49205b 100644
--- a/core/src/types/masp.rs
+++ b/core/src/types/masp.rs
@@ -8,17 +8,14 @@ use bech32::{FromBase32, ToBase32};
use borsh::{BorshDeserialize, BorshSerialize};
use sha2::{Digest, Sha256};
-use crate::types::address::{
- masp, Address, DecodeError, BECH32M_VARIANT, HASH_LEN,
+use crate::impl_display_and_from_str_via_format;
+use crate::types::address::{masp, Address, DecodeError, HASH_LEN};
+use crate::types::string_encoding::{
+ self, BECH32M_VARIANT, MASP_EXT_FULL_VIEWING_KEY_HRP,
+ MASP_EXT_SPENDING_KEY_HRP, MASP_PAYMENT_ADDRESS_HRP,
+ MASP_PINNED_PAYMENT_ADDRESS_HRP,
};
-/// human-readable part of Bech32m encoded address
-// TODO remove "test" suffix for live network
-const EXT_FULL_VIEWING_KEY_HRP: &str = "xfvktest";
-const PAYMENT_ADDRESS_HRP: &str = "patest";
-const PINNED_PAYMENT_ADDRESS_HRP: &str = "ppatest";
-const EXT_SPENDING_KEY_HRP: &str = "xsktest";
-
/// Wrapper for masp_primitive's FullViewingKey
#[derive(
Clone,
@@ -34,51 +31,99 @@ const EXT_SPENDING_KEY_HRP: &str = "xsktest";
)]
pub struct ExtendedViewingKey(masp_primitives::zip32::ExtendedFullViewingKey);
-impl Display for ExtendedViewingKey {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+impl ExtendedViewingKey {
+ /// Encode `Self` to bytes
+ pub fn to_bytes(&self) -> Vec {
let mut bytes = [0; 169];
self.0
.write(&mut bytes[..])
.expect("should be able to serialize an ExtendedFullViewingKey");
- let encoded = bech32::encode(
- EXT_FULL_VIEWING_KEY_HRP,
- bytes.to_base32(),
- BECH32M_VARIANT,
+ bytes.to_vec()
+ }
+
+ /// Try to decode `Self` from bytes
+ pub fn decode_bytes(bytes: &[u8]) -> Result {
+ masp_primitives::zip32::ExtendedFullViewingKey::read(&mut &bytes[..])
+ .map(Self)
+ }
+}
+
+impl string_encoding::Format for ExtendedViewingKey {
+ const HRP: &'static str = MASP_EXT_FULL_VIEWING_KEY_HRP;
+
+ fn to_bytes(&self) -> Vec {
+ self.to_bytes()
+ }
+
+ fn decode_bytes(bytes: &[u8]) -> Result {
+ Self::decode_bytes(bytes)
+ }
+}
+
+impl_display_and_from_str_via_format!(ExtendedViewingKey);
+
+impl string_encoding::Format for PaymentAddress {
+ const HRP: &'static str = MASP_PAYMENT_ADDRESS_HRP;
+
+ fn to_bytes(&self) -> Vec {
+ self.to_bytes()
+ }
+
+ fn decode_bytes(_bytes: &[u8]) -> Result {
+ unimplemented!(
+ "Cannot determine if the PaymentAddress is pinned from bytes. Use \
+ `PaymentAddress::decode_bytes(bytes, is_pinned)` instead."
)
- .unwrap_or_else(|_| {
+ }
+
+ // We override `encode` because we need to determine whether the address
+ // is pinned from its HRP
+ fn encode(&self) -> String {
+ let hrp = if self.is_pinned() {
+ MASP_PINNED_PAYMENT_ADDRESS_HRP
+ } else {
+ MASP_PAYMENT_ADDRESS_HRP
+ };
+ let base32 = self.to_bytes().to_base32();
+ bech32::encode(hrp, base32, BECH32M_VARIANT).unwrap_or_else(|_| {
panic!(
"The human-readable part {} should never cause a failure",
- EXT_FULL_VIEWING_KEY_HRP
+ hrp
)
- });
- write!(f, "{encoded}")
+ })
}
-}
-
-impl FromStr for ExtendedViewingKey {
- type Err = DecodeError;
- fn from_str(string: &str) -> Result {
- let (prefix, base32, variant) =
- bech32::decode(string).map_err(DecodeError::DecodeBech32)?;
- if prefix != EXT_FULL_VIEWING_KEY_HRP {
- return Err(DecodeError::UnexpectedBech32Prefix(
+ // We override `decode` because we need to use different HRP for pinned and
+ // non-pinned address
+ fn decode(
+ string: impl AsRef,
+ ) -> Result {
+ let (prefix, base32, variant) = bech32::decode(string.as_ref())
+ .map_err(DecodeError::DecodeBech32)?;
+ let is_pinned = if prefix == MASP_PAYMENT_ADDRESS_HRP {
+ false
+ } else if prefix == MASP_PINNED_PAYMENT_ADDRESS_HRP {
+ true
+ } else {
+ return Err(DecodeError::UnexpectedBech32Hrp(
prefix,
- EXT_FULL_VIEWING_KEY_HRP.into(),
+ MASP_PAYMENT_ADDRESS_HRP.into(),
));
- }
+ };
match variant {
BECH32M_VARIANT => {}
_ => return Err(DecodeError::UnexpectedBech32Variant(variant)),
}
let bytes: Vec = FromBase32::from_base32(&base32)
.map_err(DecodeError::DecodeBase32)?;
- masp_primitives::zip32::ExtendedFullViewingKey::read(&mut &bytes[..])
- .map_err(DecodeError::InvalidInnerEncoding)
- .map(Self)
+
+ PaymentAddress::decode_bytes(&bytes, is_pinned)
+ .map_err(DecodeError::InvalidBytes)
}
}
+impl_display_and_from_str_via_format!(PaymentAddress);
+
impl From
for masp_primitives::zip32::ExtendedFullViewingKey
{
@@ -155,78 +200,45 @@ impl PaymentAddress {
// hex of the first 40 chars of the hash
format!("{:.width$X}", hasher.finalize(), width = HASH_LEN)
}
-}
-impl From for masp_primitives::primitives::PaymentAddress {
- fn from(addr: PaymentAddress) -> Self {
- addr.0
- }
-}
-
-impl From for PaymentAddress {
- fn from(addr: masp_primitives::primitives::PaymentAddress) -> Self {
- Self(addr, false)
- }
-}
-
-impl Display for PaymentAddress {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let bytes = self.0.to_bytes();
- let hrp = if self.1 {
- PINNED_PAYMENT_ADDRESS_HRP
- } else {
- PAYMENT_ADDRESS_HRP
- };
- let encoded = bech32::encode(hrp, bytes.to_base32(), BECH32M_VARIANT)
- .unwrap_or_else(|_| {
- panic!(
- "The human-readable part {} should never cause a failure",
- PAYMENT_ADDRESS_HRP
- )
- });
- write!(f, "{encoded}")
+ /// Encode `Self` to bytes
+ pub fn to_bytes(&self) -> Vec {
+ self.0.to_bytes().to_vec()
}
-}
-
-impl FromStr for PaymentAddress {
- type Err = DecodeError;
- fn from_str(string: &str) -> Result {
- let (prefix, base32, variant) =
- bech32::decode(string).map_err(DecodeError::DecodeBech32)?;
- let pinned = if prefix == PAYMENT_ADDRESS_HRP {
- false
- } else if prefix == PINNED_PAYMENT_ADDRESS_HRP {
- true
- } else {
- return Err(DecodeError::UnexpectedBech32Prefix(
- prefix,
- PAYMENT_ADDRESS_HRP.into(),
- ));
- };
- match variant {
- BECH32M_VARIANT => {}
- _ => return Err(DecodeError::UnexpectedBech32Variant(variant)),
- }
+ /// Try to decode `Self` from bytes
+ pub fn decode_bytes(
+ bytes: &[u8],
+ is_pinned: bool,
+ ) -> Result {
let addr_len_err = |_| {
- DecodeError::InvalidInnerEncoding(Error::new(
+ Error::new(
ErrorKind::InvalidData,
"expected 43 bytes for the payment address",
- ))
+ )
};
let addr_data_err = || {
- DecodeError::InvalidInnerEncoding(Error::new(
+ Error::new(
ErrorKind::InvalidData,
"invalid payment address provided",
- ))
+ )
};
- let bytes: Vec = FromBase32::from_base32(&base32)
- .map_err(DecodeError::DecodeBase32)?;
- masp_primitives::primitives::PaymentAddress::from_bytes(
- &bytes.try_into().map_err(addr_len_err)?,
- )
- .ok_or_else(addr_data_err)
- .map(|x| Self(x, pinned))
+ let bytes: &[u8; 43] = &bytes.try_into().map_err(addr_len_err)?;
+ masp_primitives::primitives::PaymentAddress::from_bytes(bytes)
+ .ok_or_else(addr_data_err)
+ .map(|addr| Self(addr, is_pinned))
+ }
+}
+
+impl From for masp_primitives::primitives::PaymentAddress {
+ fn from(addr: PaymentAddress) -> Self {
+ addr.0
+ }
+}
+
+impl From for PaymentAddress {
+ fn from(addr: masp_primitives::primitives::PaymentAddress) -> Self {
+ Self(addr, false)
}
}
@@ -258,51 +270,25 @@ impl<'de> serde::Deserialize<'de> for PaymentAddress {
#[derive(Clone, Debug, Copy, BorshSerialize, BorshDeserialize)]
pub struct ExtendedSpendingKey(masp_primitives::zip32::ExtendedSpendingKey);
-impl Display for ExtendedSpendingKey {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+impl string_encoding::Format for ExtendedSpendingKey {
+ const HRP: &'static str = MASP_EXT_SPENDING_KEY_HRP;
+
+ fn to_bytes(&self) -> Vec {
let mut bytes = [0; 169];
self.0
.write(&mut &mut bytes[..])
.expect("should be able to serialize an ExtendedSpendingKey");
- let encoded = bech32::encode(
- EXT_SPENDING_KEY_HRP,
- bytes.to_base32(),
- BECH32M_VARIANT,
- )
- .unwrap_or_else(|_| {
- panic!(
- "The human-readable part {} should never cause a failure",
- EXT_SPENDING_KEY_HRP
- )
- });
- write!(f, "{encoded}")
+ bytes.to_vec()
}
-}
-impl FromStr for ExtendedSpendingKey {
- type Err = DecodeError;
-
- fn from_str(string: &str) -> Result {
- let (prefix, base32, variant) =
- bech32::decode(string).map_err(DecodeError::DecodeBech32)?;
- if prefix != EXT_SPENDING_KEY_HRP {
- return Err(DecodeError::UnexpectedBech32Prefix(
- prefix,
- EXT_SPENDING_KEY_HRP.into(),
- ));
- }
- match variant {
- BECH32M_VARIANT => {}
- _ => return Err(DecodeError::UnexpectedBech32Variant(variant)),
- }
- let bytes: Vec = FromBase32::from_base32(&base32)
- .map_err(DecodeError::DecodeBase32)?;
+ fn decode_bytes(bytes: &[u8]) -> Result {
masp_primitives::zip32::ExtendedSpendingKey::read(&mut &bytes[..])
- .map_err(DecodeError::InvalidInnerEncoding)
.map(Self)
}
}
+impl_display_and_from_str_via_format!(ExtendedSpendingKey);
+
impl From for masp_primitives::zip32::ExtendedSpendingKey {
fn from(key: ExtendedSpendingKey) -> Self {
key.0
diff --git a/core/src/types/mod.rs b/core/src/types/mod.rs
index 05500604985..2c8af14d324 100644
--- a/core/src/types/mod.rs
+++ b/core/src/types/mod.rs
@@ -9,6 +9,7 @@ pub mod internal;
pub mod key;
pub mod masp;
pub mod storage;
+pub mod string_encoding;
pub mod time;
pub mod token;
pub mod transaction;
diff --git a/core/src/types/string_encoding.rs b/core/src/types/string_encoding.rs
new file mode 100644
index 00000000000..afa6594e2a3
--- /dev/null
+++ b/core/src/types/string_encoding.rs
@@ -0,0 +1,121 @@
+//! Namada's standard string encoding for public types.
+//!
+//! We're using [bech32m](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki),
+//! a format with a human-readable, followed by base32 encoding with a limited
+//! character set with checksum check.
+//!
+//! To use this encoding for a new type, add a HRP (human-readable part) const
+//! below and use it to `impl string_encoding::Format for YourType`.
+
+use bech32::{self, FromBase32, ToBase32, Variant};
+use thiserror::Error;
+
+/// We're using "Bech32m" variant
+pub const BECH32M_VARIANT: bech32::Variant = Variant::Bech32m;
+
+// Human-readable parts of Bech32m encoding
+//
+// Invariant: HRPs must be unique !!!
+//
+// TODO: remove "test" suffix for live network
+/// `Address` human-readable part
+pub const ADDRESS_HRP: &str = "atest";
+/// MASP extended viewing key human-readable part
+pub const MASP_EXT_FULL_VIEWING_KEY_HRP: &str = "xfvktest";
+/// MASP payment address (not pinned) human-readable part
+pub const MASP_PAYMENT_ADDRESS_HRP: &str = "patest";
+/// MASP pinned payment address human-readable part
+pub const MASP_PINNED_PAYMENT_ADDRESS_HRP: &str = "ppatest";
+/// MASP extended spending key human-readable part
+pub const MASP_EXT_SPENDING_KEY_HRP: &str = "xsktest";
+
+#[allow(missing_docs)]
+#[derive(Error, Debug)]
+pub enum DecodeError {
+ #[error("Error decoding from Bech32m: {0}")]
+ DecodeBech32(bech32::Error),
+ #[error("Error decoding from base32: {0}")]
+ DecodeBase32(bech32::Error),
+ #[error("Unexpected Bech32m human-readable part {0}, expected {1}")]
+ UnexpectedBech32Hrp(String, String),
+ #[error("Unexpected Bech32m variant {0:?}, expected {BECH32M_VARIANT:?}")]
+ UnexpectedBech32Variant(bech32::Variant),
+ #[error("Invalid bytes: {0}")]
+ InvalidBytes(std::io::Error),
+}
+
+/// Format to string with bech32m
+pub trait Format: Sized {
+ /// Human-readable part
+ const HRP: &'static str;
+
+ /// Encode `Self` to a string
+ fn encode(&self) -> String {
+ let base32 = self.to_bytes().to_base32();
+ bech32::encode(Self::HRP, base32, BECH32M_VARIANT).unwrap_or_else(
+ |_| {
+ panic!(
+ "The human-readable part {} should never cause a failure",
+ Self::HRP
+ )
+ },
+ )
+ }
+
+ /// Try to decode `Self` from a string
+ fn decode(string: impl AsRef) -> Result {
+ let (hrp, hash_base32, variant) = bech32::decode(string.as_ref())
+ .map_err(DecodeError::DecodeBech32)?;
+ if hrp != Self::HRP {
+ return Err(DecodeError::UnexpectedBech32Hrp(
+ hrp,
+ Self::HRP.into(),
+ ));
+ }
+ match variant {
+ BECH32M_VARIANT => {}
+ _ => return Err(DecodeError::UnexpectedBech32Variant(variant)),
+ }
+ let bytes: Vec = FromBase32::from_base32(&hash_base32)
+ .map_err(DecodeError::DecodeBase32)?;
+
+ Self::decode_bytes(&bytes).map_err(DecodeError::InvalidBytes)
+ }
+
+ /// Encode `Self` to bytes
+ fn to_bytes(&self) -> Vec;
+
+ /// Try to decode `Self` from bytes
+ fn decode_bytes(bytes: &[u8]) -> Result;
+}
+
+/// Implement [`std::fmt::Display`] and [`std::str::FromStr`] via
+/// [`Format`].
+#[macro_export]
+macro_rules! impl_display_and_from_str_via_format {
+ ($t:path) => {
+ impl std::fmt::Display for $t {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{}",
+ $crate::types::string_encoding::Format::encode(self)
+ )
+ }
+ }
+
+ impl std::str::FromStr for $t {
+ type Err = $crate::types::string_encoding::DecodeError;
+
+ fn from_str(s: &str) -> std::result::Result {
+ $crate::types::string_encoding::Format::decode(s)
+ }
+ }
+ };
+}
+
+/// Get the length of the human-readable part
+// Not in the `Format` trait, cause functions in traits cannot be const
+pub const fn hrp_len() -> usize {
+ T::HRP.len()
+}