causeway_sapling/
address.rs1use thiserror::Error;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Network {
27 Mainnet,
28 Testnet,
29 Regtest,
30}
31
32impl Network {
33 pub fn hrp(self) -> &'static str {
35 match self {
36 Network::Mainnet => "zs",
37 Network::Testnet => "ztestsapling",
38 Network::Regtest => "zregtestsapling",
39 }
40 }
41
42 pub fn from_hrp(hrp: &str) -> Option<Self> {
46 match hrp {
47 "zs" => Some(Network::Mainnet),
48 "ztestsapling" => Some(Network::Testnet),
49 "zregtestsapling" => Some(Network::Regtest),
50 _ => None,
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct SaplingAddress {
58 pub network: Network,
59 pub raw: [u8; 43],
61}
62
63#[derive(Debug, Error)]
64pub enum SaplingError {
65 #[error("bech32 decode: {0}")]
66 Bech32Decode(String),
67 #[error("bech32 encode: {0}")]
68 Bech32Encode(String),
69 #[error("hrp '{0}' is not a Sapling hrp (expected zs / ztestsapling / zregtestsapling)")]
70 UnknownHrp(String),
71 #[error("payload not 43 bytes: got {0}")]
72 PayloadLength(usize),
73 #[error("hrp parse: {0}")]
74 HrpParse(String),
75}
76
77pub fn decode_sapling_address(addr: &str) -> Result<SaplingAddress, SaplingError> {
79 let (hrp, words) = bech32::decode(addr)
80 .map_err(|e| SaplingError::Bech32Decode(e.to_string()))?;
81 let network = Network::from_hrp(hrp.as_str())
82 .ok_or_else(|| SaplingError::UnknownHrp(hrp.as_str().to_string()))?;
83 if words.len() != 43 {
84 return Err(SaplingError::PayloadLength(words.len()));
85 }
86 let mut raw = [0u8; 43];
87 raw.copy_from_slice(&words);
88 Ok(SaplingAddress { network, raw })
89}
90
91pub fn encode_sapling_address(network: Network, raw: &[u8]) -> Result<String, SaplingError> {
93 if raw.len() != 43 {
94 return Err(SaplingError::PayloadLength(raw.len()));
95 }
96 let hrp = bech32::Hrp::parse(network.hrp())
97 .map_err(|e| SaplingError::HrpParse(e.to_string()))?;
98 bech32::encode::<bech32::Bech32>(hrp, raw)
99 .map_err(|e| SaplingError::Bech32Encode(e.to_string()))
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 const REGTEST_VAULT: &str = "zregtestsapling1euldd485nn489mlc9qs7g0vt9em845mfzehcp8sverxtwczhyuwhu8jzexhk8z6w4xt2wld40jr";
110
111 #[test]
112 fn round_trip_regtest() {
113 let parsed = decode_sapling_address(REGTEST_VAULT).expect("decode");
114 assert_eq!(parsed.network, Network::Regtest);
115 assert_eq!(parsed.raw.len(), 43);
116 let re_encoded = encode_sapling_address(parsed.network, &parsed.raw).expect("encode");
117 assert_eq!(re_encoded, REGTEST_VAULT);
118 }
119
120 #[test]
121 fn unknown_hrp_rejected() {
122 let bad = "ztestxyz1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqgmtkdv";
126 let res = decode_sapling_address(bad);
127 assert!(res.is_err(), "non-sapling hrp must be rejected");
128 }
129
130 #[test]
131 fn payload_length_enforced() {
132 let res = encode_sapling_address(Network::Regtest, &[0u8; 42]);
136 assert!(matches!(res, Err(SaplingError::PayloadLength(42))));
137 }
138
139 #[test]
140 fn network_hrps() {
141 assert_eq!(Network::Mainnet.hrp(), "zs");
142 assert_eq!(Network::Testnet.hrp(), "ztestsapling");
143 assert_eq!(Network::Regtest.hrp(), "zregtestsapling");
144 assert_eq!(Network::from_hrp("zs"), Some(Network::Mainnet));
145 assert_eq!(Network::from_hrp("ztestsapling"), Some(Network::Testnet));
146 assert_eq!(Network::from_hrp("zregtestsapling"), Some(Network::Regtest));
147 assert_eq!(Network::from_hrp("u"), None);
148 }
149}