iota_stardust_types/block/address/
bech32.rs

1// Copyright (c) 2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! Bech32 encoding/decoding for addresses.
5
6use alloc::{
7    string::{String, ToString},
8    vec::Vec,
9};
10use core::str::FromStr;
11
12use bech32::{FromBase32, ToBase32, Variant};
13use derive_more::{AsRef, Deref};
14use packable::{
15    Packable, PackableExt,
16    error::{UnpackError, UnpackErrorExt},
17    packer::Packer,
18    unpacker::Unpacker,
19};
20
21use super::Address;
22use crate::block::Error;
23
24/// Human-readable part for bech32 addresses.
25#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
26pub struct Hrp {
27    inner: [u8; 83],
28    len: u8,
29}
30
31impl Hrp {
32    /// Convert a string to an Hrp without checking validity.
33    pub const fn from_str_unchecked(hrp: &str) -> Self {
34        let len = hrp.len();
35        let mut bytes = [0; 83];
36        let hrp = hrp.as_bytes();
37        let mut i = 0;
38        while i < len {
39            bytes[i] = hrp[i];
40            i += 1;
41        }
42        Self {
43            inner: bytes,
44            len: len as _,
45        }
46    }
47
48    /// Parse a string into an Hrp, returning a Result.
49    pub fn from_str_result(hrp: &str) -> Result<Self, Error> {
50        hrp.parse()
51    }
52}
53
54impl FromStr for Hrp {
55    type Err = Error;
56
57    fn from_str(hrp: &str) -> Result<Self, Self::Err> {
58        let len = hrp.len();
59        if hrp.is_ascii() && len <= 83 {
60            let mut bytes = [0; 83];
61            bytes[..len].copy_from_slice(hrp.as_bytes());
62            Ok(Self {
63                inner: bytes,
64                len: len as _,
65            })
66        } else {
67            Err(Error::InvalidBech32Hrp(hrp.to_string()))
68        }
69    }
70}
71
72impl core::fmt::Display for Hrp {
73    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
74        let hrp_str = self.inner[..self.len as usize]
75            .iter()
76            .map(|b| *b as char)
77            .collect::<String>();
78        f.write_str(&hrp_str)
79    }
80}
81
82#[cfg(feature = "serde")]
83impl serde::Serialize for Hrp {
84    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
85    where
86        S: serde::Serializer,
87    {
88        serializer.serialize_str(&self.to_string())
89    }
90}
91
92#[cfg(feature = "serde")]
93impl<'de> serde::Deserialize<'de> for Hrp {
94    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
95    where
96        D: serde::Deserializer<'de>,
97    {
98        let s = String::deserialize(deserializer)?;
99        Self::from_str(&s).map_err(serde::de::Error::custom)
100    }
101}
102
103impl Packable for Hrp {
104    type UnpackError = Error;
105    type UnpackVisitor = ();
106
107    #[inline]
108    fn pack<P: Packer>(&self, packer: &mut P) -> Result<(), P::Error> {
109        self.len.pack(packer)?;
110        packer.pack_bytes(&self.inner[..self.len as usize])?;
111        Ok(())
112    }
113
114    #[inline]
115    fn unpack<U: Unpacker, const VERIFY: bool>(
116        unpacker: &mut U,
117        visitor: &Self::UnpackVisitor,
118    ) -> Result<Self, UnpackError<Self::UnpackError, U::Error>> {
119        let len = u8::unpack::<_, VERIFY>(unpacker, visitor).coerce()?;
120
121        if len > 83 {
122            return Err(UnpackError::Packable(Error::InvalidBech32Hrp(
123                "hrp len above 83".to_string(),
124            )));
125        }
126
127        let mut bytes = alloc::vec![0u8; len as usize];
128        unpacker.unpack_bytes(&mut bytes)?;
129
130        let mut inner = [0; 83];
131        inner[..len as usize].copy_from_slice(&bytes);
132
133        Ok(Self { inner, len })
134    }
135}
136
137impl PartialEq<String> for Hrp {
138    fn eq(&self, other: &String) -> bool {
139        self.to_string().eq(other)
140    }
141}
142
143impl PartialEq<&str> for Hrp {
144    fn eq(&self, other: &&str) -> bool {
145        self.to_string().eq(other)
146    }
147}
148
149impl PartialEq<str> for Hrp {
150    fn eq(&self, other: &str) -> bool {
151        self.to_string().eq(other)
152    }
153}
154
155/// An address with its network type (bech32 human-readable part).
156#[derive(Copy, Clone, Eq, PartialEq, Hash, AsRef, Deref, Ord, PartialOrd)]
157pub struct Bech32Address {
158    pub(crate) hrp: Hrp,
159    #[as_ref]
160    #[deref]
161    pub(crate) inner: Address,
162}
163
164impl FromStr for Bech32Address {
165    type Err = Error;
166
167    fn from_str(address: &str) -> Result<Self, Self::Err> {
168        match bech32::decode(address) {
169            Ok((hrp, data, _)) => {
170                let hrp = hrp.parse()?;
171                let bytes = Vec::<u8>::from_base32(&data).map_err(|_| Error::InvalidAddress)?;
172                Address::unpack_verified(bytes.as_slice(), &())
173                    .map_err(|_| Error::InvalidAddress)
174                    .map(|address| Self {
175                        hrp,
176                        inner: address,
177                    })
178            }
179            Err(_) => Err(Error::InvalidAddress),
180        }
181    }
182}
183
184impl Bech32Address {
185    /// Creates a new address wrapper.
186    pub fn new(hrp: Hrp, inner: impl Into<Address>) -> Self {
187        Self {
188            hrp,
189            inner: inner.into(),
190        }
191    }
192
193    /// Creates a new address wrapper by parsing a string HRP.
194    pub fn try_new(hrp: impl AsRef<str>, inner: impl Into<Address>) -> Result<Self, Error> {
195        Ok(Self {
196            hrp: Hrp::from_str(hrp.as_ref())?,
197            inner: inner.into(),
198        })
199    }
200
201    /// Gets the human readable part.
202    pub fn hrp(&self) -> &Hrp {
203        &self.hrp
204    }
205
206    /// Gets the address part.
207    pub fn inner(&self) -> &Address {
208        &self.inner
209    }
210
211    /// Discard the hrp and get the address.
212    pub fn into_inner(self) -> Address {
213        self.inner
214    }
215
216    /// Parses a bech32 address string.
217    pub fn try_from_str(address: impl AsRef<str>) -> Result<Self, Error> {
218        Self::from_str(address.as_ref())
219    }
220}
221
222impl core::fmt::Display for Bech32Address {
223    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
224        write!(
225            f,
226            "{}",
227            bech32::encode(
228                &self.hrp.to_string(),
229                self.inner.pack_to_vec().to_base32(),
230                Variant::Bech32
231            )
232            .unwrap()
233        )
234    }
235}
236
237impl core::fmt::Debug for Bech32Address {
238    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
239        write!(f, "Bech32Address({self})")
240    }
241}
242
243impl PartialEq<String> for Bech32Address {
244    fn eq(&self, other: &String) -> bool {
245        self.to_string().eq(other)
246    }
247}
248
249impl PartialEq<&str> for Bech32Address {
250    fn eq(&self, other: &&str) -> bool {
251        self.to_string().eq(other)
252    }
253}
254
255impl PartialEq<str> for Bech32Address {
256    fn eq(&self, other: &str) -> bool {
257        self.to_string().eq(other)
258    }
259}
260
261impl<T: core::borrow::Borrow<Bech32Address>> From<T> for Address {
262    fn from(value: T) -> Self {
263        value.borrow().inner
264    }
265}
266
267/// Trait extension to convert to bech32.
268pub trait ToBech32Ext: Sized {
269    /// Try to encode this address to a bech32 string with the given Human
270    /// Readable Part as prefix.
271    fn try_to_bech32(self, hrp: impl AsRef<str>) -> Result<Bech32Address, Error>;
272
273    /// Encodes this address to a bech32 string with the given Human Readable
274    /// Part as prefix.
275    fn to_bech32(self, hrp: Hrp) -> Bech32Address;
276
277    /// Encodes this address to a bech32 string with the given Human Readable
278    /// Part as prefix without checking validity.
279    fn to_bech32_unchecked(self, hrp: impl AsRef<str>) -> Bech32Address;
280}
281
282impl<T: Into<Address>> ToBech32Ext for T {
283    fn try_to_bech32(self, hrp: impl AsRef<str>) -> Result<Bech32Address, Error> {
284        Bech32Address::try_new(hrp, self)
285    }
286
287    fn to_bech32(self, hrp: Hrp) -> Bech32Address {
288        Bech32Address::new(hrp, self)
289    }
290
291    fn to_bech32_unchecked(self, hrp: impl AsRef<str>) -> Bech32Address {
292        Bech32Address::new(Hrp::from_str_unchecked(hrp.as_ref()), self)
293    }
294}