use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Network {
Mainnet,
Testnet,
Regtest,
}
impl Network {
pub fn hrp(self) -> &'static str {
match self {
Network::Mainnet => "zs",
Network::Testnet => "ztestsapling",
Network::Regtest => "zregtestsapling",
}
}
pub fn from_hrp(hrp: &str) -> Option<Self> {
match hrp {
"zs" => Some(Network::Mainnet),
"ztestsapling" => Some(Network::Testnet),
"zregtestsapling" => Some(Network::Regtest),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SaplingAddress {
pub network: Network,
pub raw: [u8; 43],
}
#[derive(Debug, Error)]
pub enum SaplingError {
#[error("bech32 decode: {0}")]
Bech32Decode(String),
#[error("bech32 encode: {0}")]
Bech32Encode(String),
#[error("hrp '{0}' is not a Sapling hrp (expected zs / ztestsapling / zregtestsapling)")]
UnknownHrp(String),
#[error("payload not 43 bytes: got {0}")]
PayloadLength(usize),
#[error("hrp parse: {0}")]
HrpParse(String),
}
pub fn decode_sapling_address(addr: &str) -> Result<SaplingAddress, SaplingError> {
let (hrp, words) = bech32::decode(addr)
.map_err(|e| SaplingError::Bech32Decode(e.to_string()))?;
let network = Network::from_hrp(hrp.as_str())
.ok_or_else(|| SaplingError::UnknownHrp(hrp.as_str().to_string()))?;
if words.len() != 43 {
return Err(SaplingError::PayloadLength(words.len()));
}
let mut raw = [0u8; 43];
raw.copy_from_slice(&words);
Ok(SaplingAddress { network, raw })
}
pub fn encode_sapling_address(network: Network, raw: &[u8]) -> Result<String, SaplingError> {
if raw.len() != 43 {
return Err(SaplingError::PayloadLength(raw.len()));
}
let hrp = bech32::Hrp::parse(network.hrp())
.map_err(|e| SaplingError::HrpParse(e.to_string()))?;
bech32::encode::<bech32::Bech32>(hrp, raw)
.map_err(|e| SaplingError::Bech32Encode(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
const REGTEST_VAULT: &str = "zregtestsapling1euldd485nn489mlc9qs7g0vt9em845mfzehcp8sverxtwczhyuwhu8jzexhk8z6w4xt2wld40jr";
#[test]
fn round_trip_regtest() {
let parsed = decode_sapling_address(REGTEST_VAULT).expect("decode");
assert_eq!(parsed.network, Network::Regtest);
assert_eq!(parsed.raw.len(), 43);
let re_encoded = encode_sapling_address(parsed.network, &parsed.raw).expect("encode");
assert_eq!(re_encoded, REGTEST_VAULT);
}
#[test]
fn unknown_hrp_rejected() {
let bad = "ztestxyz1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqgmtkdv";
let res = decode_sapling_address(bad);
assert!(res.is_err(), "non-sapling hrp must be rejected");
}
#[test]
fn payload_length_enforced() {
let res = encode_sapling_address(Network::Regtest, &[0u8; 42]);
assert!(matches!(res, Err(SaplingError::PayloadLength(42))));
}
#[test]
fn network_hrps() {
assert_eq!(Network::Mainnet.hrp(), "zs");
assert_eq!(Network::Testnet.hrp(), "ztestsapling");
assert_eq!(Network::Regtest.hrp(), "zregtestsapling");
assert_eq!(Network::from_hrp("zs"), Some(Network::Mainnet));
assert_eq!(Network::from_hrp("ztestsapling"), Some(Network::Testnet));
assert_eq!(Network::from_hrp("zregtestsapling"), Some(Network::Regtest));
assert_eq!(Network::from_hrp("u"), None);
}
}