Skip to main content

zcash_address/
encoding.rs

1use alloc::string::String;
2use alloc::vec::Vec;
3use core::convert::TryInto;
4use core::fmt;
5use core::str::FromStr;
6
7#[cfg(feature = "std")]
8use std::error::Error;
9
10use bech32::{Bech32, Bech32m, Checksum, Hrp, primitives::decode::CheckedHrpstring};
11use zcash_protocol::consensus::NetworkType;
12
13use crate::constants::{self, mainnet, regtest, testnet};
14use crate::kind::unified::Encoding;
15use crate::{AddressKind, ZcashAddress, kind::*};
16
17/// An error while attempting to parse a string as a Zcash address.
18#[derive(Debug, PartialEq, Eq)]
19pub enum ParseError {
20    /// The string is an invalid encoding.
21    InvalidEncoding,
22    /// The string is not a Zcash address.
23    NotZcash,
24    /// Errors specific to unified addresses.
25    Unified(unified::ParseError),
26}
27
28impl From<unified::ParseError> for ParseError {
29    fn from(e: unified::ParseError) -> Self {
30        match e {
31            unified::ParseError::InvalidEncoding(_) => Self::InvalidEncoding,
32            unified::ParseError::UnknownPrefix(_) => Self::NotZcash,
33            _ => Self::Unified(e),
34        }
35    }
36}
37
38impl fmt::Display for ParseError {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            ParseError::InvalidEncoding => write!(f, "Invalid encoding"),
42            ParseError::NotZcash => write!(f, "Not a Zcash address"),
43            ParseError::Unified(e) => e.fmt(f),
44        }
45    }
46}
47
48#[cfg(feature = "std")]
49impl Error for ParseError {}
50
51impl FromStr for ZcashAddress {
52    type Err = ParseError;
53
54    /// Attempts to parse the given string as a Zcash address.
55    fn from_str(s: &str) -> Result<Self, Self::Err> {
56        // Remove leading and trailing whitespace, to handle copy-paste errors.
57        let s = s.trim();
58
59        // Try decoding as a unified address
60        match unified::Address::decode(s) {
61            Ok((net, _revision, data)) => {
62                return Ok(ZcashAddress {
63                    net,
64                    kind: AddressKind::Unified(data),
65                });
66            }
67            Err(unified::ParseError::NotUnified | unified::ParseError::UnknownPrefix(_)) => {
68                // allow decoding to fall through to Sapling/TEX/Transparent
69            }
70            Err(e) => {
71                return Err(ParseError::from(e));
72            }
73        }
74
75        // Try decoding as a Sapling address (Bech32)
76        if let Ok(parsed) = CheckedHrpstring::new::<Bech32>(s) {
77            // If we reached this point, the encoding is found to be valid Bech32.
78            let net = match parsed.hrp().as_str() {
79                mainnet::HRP_SAPLING_PAYMENT_ADDRESS => NetworkType::Main,
80                testnet::HRP_SAPLING_PAYMENT_ADDRESS => NetworkType::Test,
81                regtest::HRP_SAPLING_PAYMENT_ADDRESS => NetworkType::Regtest,
82                // We will not define new Bech32 address encodings.
83                _ => {
84                    return Err(ParseError::NotZcash);
85                }
86            };
87
88            let data = parsed.byte_iter().collect::<Vec<_>>();
89
90            return data
91                .try_into()
92                .map(AddressKind::Sapling)
93                .map_err(|_| ParseError::InvalidEncoding)
94                .map(|kind| ZcashAddress { net, kind });
95        }
96
97        // Try decoding as a TEX address (Bech32m)
98        if let Ok(parsed) = CheckedHrpstring::new::<Bech32m>(s) {
99            // If we reached this point, the encoding is found to be valid Bech32m.
100            let net = match parsed.hrp().as_str() {
101                mainnet::HRP_TEX_ADDRESS => NetworkType::Main,
102                testnet::HRP_TEX_ADDRESS => NetworkType::Test,
103                regtest::HRP_TEX_ADDRESS => NetworkType::Regtest,
104                // Not recognized as a Zcash address type
105                _ => {
106                    return Err(ParseError::NotZcash);
107                }
108            };
109
110            let data = parsed.byte_iter().collect::<Vec<_>>();
111
112            return data
113                .try_into()
114                .map(AddressKind::Tex)
115                .map_err(|_| ParseError::InvalidEncoding)
116                .map(|kind| ZcashAddress { net, kind });
117        }
118
119        // The rest use Base58Check.
120        if let Ok(decoded) = bs58::decode(s).with_check(None).into_vec()
121            && decoded.len() >= 2
122        {
123            let (prefix, net) = match decoded[..2].try_into().unwrap() {
124                prefix @ (mainnet::B58_PUBKEY_ADDRESS_PREFIX
125                | mainnet::B58_SCRIPT_ADDRESS_PREFIX
126                | mainnet::B58_SPROUT_ADDRESS_PREFIX) => (prefix, NetworkType::Main),
127                prefix @ (testnet::B58_PUBKEY_ADDRESS_PREFIX
128                | testnet::B58_SCRIPT_ADDRESS_PREFIX
129                | testnet::B58_SPROUT_ADDRESS_PREFIX) => (prefix, NetworkType::Test),
130                // We will not define new Base58Check address encodings.
131                _ => return Err(ParseError::NotZcash),
132            };
133
134            return match prefix {
135                mainnet::B58_SPROUT_ADDRESS_PREFIX | testnet::B58_SPROUT_ADDRESS_PREFIX => {
136                    decoded[2..].try_into().map(AddressKind::Sprout)
137                }
138                mainnet::B58_PUBKEY_ADDRESS_PREFIX | testnet::B58_PUBKEY_ADDRESS_PREFIX => {
139                    decoded[2..].try_into().map(AddressKind::P2pkh)
140                }
141                mainnet::B58_SCRIPT_ADDRESS_PREFIX | testnet::B58_SCRIPT_ADDRESS_PREFIX => {
142                    decoded[2..].try_into().map(AddressKind::P2sh)
143                }
144                _ => unreachable!(),
145            }
146            .map_err(|_| ParseError::InvalidEncoding)
147            .map(|kind| ZcashAddress { kind, net });
148        };
149
150        // If it's not valid Bech32, Bech32m, or Base58Check, it's not a Zcash address.
151        Err(ParseError::NotZcash)
152    }
153}
154
155fn encode_bech32<Ck: Checksum>(hrp: &str, data: &[u8]) -> String {
156    bech32::encode::<Ck>(Hrp::parse_unchecked(hrp), data).expect("encoding is short enough")
157}
158
159fn encode_b58(prefix: [u8; 2], data: &[u8]) -> String {
160    let mut bytes = Vec::with_capacity(2 + data.len());
161    bytes.extend_from_slice(&prefix);
162    bytes.extend_from_slice(data);
163    bs58::encode(bytes).with_check().into_string()
164}
165
166impl fmt::Display for ZcashAddress {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        let encoded = match &self.kind {
169            AddressKind::Sprout(data) => {
170                encode_b58(constants::b58_sprout_address_prefix(self.net), data)
171            }
172            AddressKind::Sapling(data) => {
173                encode_bech32::<Bech32>(constants::hrp_sapling_payment_address(self.net), data)
174            }
175            AddressKind::Unified(addr) => addr.encode(&self.net),
176            AddressKind::P2pkh(data) => {
177                encode_b58(constants::b58_pubkey_address_prefix(self.net), data)
178            }
179            AddressKind::P2sh(data) => {
180                encode_b58(constants::b58_script_address_prefix(self.net), data)
181            }
182            AddressKind::Tex(data) => {
183                encode_bech32::<Bech32m>(constants::hrp_tex_address(self.net), data)
184            }
185        };
186        write!(f, "{encoded}")
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use alloc::string::ToString;
193
194    use assert_matches::assert_matches;
195
196    use super::*;
197    use crate::kind::unified;
198    use zcash_protocol::consensus::NetworkType;
199
200    fn encoding(encoded: &str, decoded: ZcashAddress) {
201        assert_eq!(decoded.to_string(), encoded);
202        assert_eq!(encoded.parse(), Ok(decoded));
203    }
204
205    #[test]
206    fn sprout() {
207        encoding(
208            "zc8E5gYid86n4bo2Usdq1cpr7PpfoJGzttwBHEEgGhGkLUg7SPPVFNB2AkRFXZ7usfphup5426dt1buMmY3fkYeRrQGLa8y",
209            ZcashAddress {
210                net: NetworkType::Main,
211                kind: AddressKind::Sprout([0; 64]),
212            },
213        );
214        encoding(
215            "ztJ1EWLKcGwF2S4NA17pAJVdco8Sdkz4AQPxt1cLTEfNuyNswJJc2BbBqYrsRZsp31xbVZwhF7c7a2L9jsF3p3ZwRWpqqyS",
216            ZcashAddress {
217                net: NetworkType::Test,
218                kind: AddressKind::Sprout([0; 64]),
219            },
220        );
221    }
222
223    #[test]
224    fn sapling() {
225        encoding(
226            "zs1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpq6d8g",
227            ZcashAddress {
228                net: NetworkType::Main,
229                kind: AddressKind::Sapling([0; 43]),
230            },
231        );
232        encoding(
233            "ztestsapling1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqfhgwqu",
234            ZcashAddress {
235                net: NetworkType::Test,
236                kind: AddressKind::Sapling([0; 43]),
237            },
238        );
239        encoding(
240            "zregtestsapling1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqknpr3m",
241            ZcashAddress {
242                net: NetworkType::Regtest,
243                kind: AddressKind::Sapling([0; 43]),
244            },
245        );
246    }
247
248    #[test]
249    fn unified() {
250        encoding(
251            "u1qpatys4zruk99pg59gcscrt7y6akvl9vrhcfyhm9yxvxz7h87q6n8cgrzzpe9zru68uq39uhmlpp5uefxu0su5uqyqfe5zp3tycn0ecl",
252            ZcashAddress {
253                net: NetworkType::Main,
254                kind: AddressKind::Unified(unified::Address {
255                    revision: unified::Revision::R0,
256                    items: vec![unified::Uitem::Data(unified::address::Receiver::Sapling(
257                        [0; 43],
258                    ))],
259                }),
260            },
261        );
262        encoding(
263            "utest10c5kutapazdnf8ztl3pu43nkfsjx89fy3uuff8tsmxm6s86j37pe7uz94z5jhkl49pqe8yz75rlsaygexk6jpaxwx0esjr8wm5ut7d5s",
264            ZcashAddress {
265                net: NetworkType::Test,
266                kind: AddressKind::Unified(unified::Address {
267                    revision: unified::Revision::R0,
268                    items: vec![unified::Uitem::Data(unified::address::Receiver::Sapling(
269                        [0; 43],
270                    ))],
271                }),
272            },
273        );
274        encoding(
275            "uregtest15xk7vj4grjkay6mnfl93dhsflc2yeunhxwdh38rul0rq3dfhzzxgm5szjuvtqdha4t4p2q02ks0jgzrhjkrav70z9xlvq0plpcjkd5z3",
276            ZcashAddress {
277                net: NetworkType::Regtest,
278                kind: AddressKind::Unified(unified::Address {
279                    revision: unified::Revision::R0,
280                    items: vec![unified::Uitem::Data(unified::address::Receiver::Sapling(
281                        [0; 43],
282                    ))],
283                }),
284            },
285        );
286
287        let badencoded = "uinvalid1ck5navqwcng43gvsxwrxsplc22p7uzlcag6qfa0zh09e87efq6rq8wsnv25umqjjravw70rl994n5ueuhza2fghge5gl7zrl2qp6cwmp";
288        assert_eq!(
289            badencoded.parse::<ZcashAddress>(),
290            Err(ParseError::NotZcash)
291        );
292    }
293
294    #[test]
295    fn transparent() {
296        encoding(
297            "t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs",
298            ZcashAddress {
299                net: NetworkType::Main,
300                kind: AddressKind::P2pkh([0; 20]),
301            },
302        );
303        encoding(
304            "tm9iMLAuYMzJ6jtFLcA7rzUmfreGuKvr7Ma",
305            ZcashAddress {
306                net: NetworkType::Test,
307                kind: AddressKind::P2pkh([0; 20]),
308            },
309        );
310        encoding(
311            "t3JZcvsuaXE6ygokL4XUiZSTrQBUoPYFnXJ",
312            ZcashAddress {
313                net: NetworkType::Main,
314                kind: AddressKind::P2sh([0; 20]),
315            },
316        );
317        encoding(
318            "t26YoyZ1iPgiMEWL4zGUm74eVWfhyDMXzY2",
319            ZcashAddress {
320                net: NetworkType::Test,
321                kind: AddressKind::P2sh([0; 20]),
322            },
323        );
324    }
325
326    #[test]
327    fn tex() {
328        let p2pkh_str = "t1VmmGiyjVNeCjxDZzg7vZmd99WyzVby9yC";
329        let tex_str = "tex1s2rt77ggv6q989lr49rkgzmh5slsksa9khdgte";
330
331        // Transcode P2PKH to TEX
332        let p2pkh_zaddr: ZcashAddress = p2pkh_str.parse().unwrap();
333        assert_matches!(p2pkh_zaddr.net, NetworkType::Main);
334        if let AddressKind::P2pkh(zaddr_data) = p2pkh_zaddr.kind {
335            let tex_zaddr = ZcashAddress {
336                net: p2pkh_zaddr.net,
337                kind: AddressKind::Tex(zaddr_data),
338            };
339
340            assert_eq!(tex_zaddr.to_string(), tex_str);
341        } else {
342            panic!("Decoded address should have been a P2PKH address.");
343        }
344
345        // Transcode TEX to P2PKH
346        let tex_zaddr: ZcashAddress = tex_str.parse().unwrap();
347        assert_matches!(tex_zaddr.net, NetworkType::Main);
348        if let AddressKind::Tex(zaddr_data) = tex_zaddr.kind {
349            let p2pkh_zaddr = ZcashAddress {
350                net: tex_zaddr.net,
351                kind: AddressKind::P2pkh(zaddr_data),
352            };
353
354            assert_eq!(p2pkh_zaddr.to_string(), p2pkh_str);
355        } else {
356            panic!("Decoded address should have been a TEX address.");
357        }
358    }
359
360    #[test]
361    fn tex_testnet() {
362        let p2pkh_str = "tm9ofD7kHR7AF8MsJomEzLqGcrLCBkD9gDj";
363        let tex_str = "textest1qyqszqgpqyqszqgpqyqszqgpqyqszqgpfcjgfy";
364
365        // Transcode P2PKH to TEX
366        let p2pkh_zaddr: ZcashAddress = p2pkh_str.parse().unwrap();
367        assert_matches!(p2pkh_zaddr.net, NetworkType::Test);
368        if let AddressKind::P2pkh(zaddr_data) = p2pkh_zaddr.kind {
369            let tex_zaddr = ZcashAddress {
370                net: p2pkh_zaddr.net,
371                kind: AddressKind::Tex(zaddr_data),
372            };
373
374            assert_eq!(tex_zaddr.to_string(), tex_str);
375        } else {
376            panic!("Decoded address should have been a P2PKH address.");
377        }
378
379        // Transcode TEX to P2PKH
380        let tex_zaddr: ZcashAddress = tex_str.parse().unwrap();
381        assert_matches!(tex_zaddr.net, NetworkType::Test);
382        if let AddressKind::Tex(zaddr_data) = tex_zaddr.kind {
383            let p2pkh_zaddr = ZcashAddress {
384                net: tex_zaddr.net,
385                kind: AddressKind::P2pkh(zaddr_data),
386            };
387
388            assert_eq!(p2pkh_zaddr.to_string(), p2pkh_str);
389        } else {
390            panic!("Decoded address should have been a TEX address.");
391        }
392    }
393
394    #[test]
395    fn whitespace() {
396        assert_eq!(
397            " t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs".parse(),
398            Ok(ZcashAddress {
399                net: NetworkType::Main,
400                kind: AddressKind::P2pkh([0; 20])
401            }),
402        );
403        assert_eq!(
404            "t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs ".parse(),
405            Ok(ZcashAddress {
406                net: NetworkType::Main,
407                kind: AddressKind::P2pkh([0; 20])
408            }),
409        );
410        assert_eq!(
411            "something t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs".parse::<ZcashAddress>(),
412            Err(ParseError::NotZcash),
413        );
414    }
415}