miden_objects/account/account_id/
mod.rs

1pub(crate) mod v0;
2pub use v0::{AccountIdPrefixV0, AccountIdV0};
3
4mod id_prefix;
5pub use id_prefix::AccountIdPrefix;
6
7mod seed;
8
9mod network_id;
10pub use network_id::NetworkId;
11
12mod address_type;
13pub use address_type::AddressType;
14
15mod account_type;
16pub use account_type::AccountType;
17
18mod storage_mode;
19pub use storage_mode::AccountStorageMode;
20
21mod id_version;
22use alloc::string::{String, ToString};
23use core::fmt;
24
25pub use id_version::AccountIdVersion;
26use miden_crypto::utils::hex_to_bytes;
27use vm_core::{
28    Felt, Word,
29    utils::{ByteReader, Deserializable, Serializable},
30};
31use vm_processor::{DeserializationError, Digest};
32
33use crate::{AccountError, errors::AccountIdError};
34
35/// The identifier of an [`Account`](crate::account::Account).
36///
37/// This enum is a wrapper around concrete versions of IDs. The following documents version 0.
38///
39/// # Layout
40///
41/// An `AccountId` consists of two field elements, where the first is called the prefix and the
42/// second is called the suffix. It is laid out as follows:
43///
44/// ```text
45/// prefix: [hash (56 bits) | storage mode (2 bits) | type (2 bits) | version (4 bits)]
46/// suffix: [zero bit | hash (55 bits) | 8 zero bits]
47/// ```
48///
49/// # Generation
50///
51/// An `AccountId` is a commitment to a user-generated seed and the code and storage of an account.
52/// An id is generated by first creating the account's initial storage and code. Then a random seed
53/// is picked and the hash of `(SEED, CODE_COMMITMENT, STORAGE_COMMITMENT, EMPTY_WORD)` is computed.
54/// This process is repeated until the hash's first element has the desired storage mode, account
55/// type and version and the suffix' most significant bit is zero.
56///
57/// The prefix of the ID is exactly the first element of the hash. The suffix of the ID is the
58/// second element of the hash, but its lower 8 bits are zeroed. Thus, the prefix of the ID must
59/// derive exactly from the hash, while only the first 56 bits of the suffix are derived from the
60/// hash.
61///
62/// In total, due to requiring specific bits for storage mode, type, version and the most
63/// significant bit in the suffix, generating an ID requires 9 bits of Proof-of-Work.
64///
65/// # Constraints
66///
67/// Constructors will return an error if:
68///
69/// - The prefix contains account ID metadata (storage mode, type or version) that does not match
70///   any of the known values.
71/// - The most significant bit of the suffix is not zero.
72/// - The lower 8 bits of the suffix are not zero, although [`AccountId::new`] ensures this is the
73///   case rather than return an error.
74///
75/// # Design Rationale
76///
77/// The rationale behind the above layout is as follows.
78///
79/// - The prefix is the output of a hash function so it will be a valid field element without
80///   requiring additional constraints.
81/// - The version is placed at a static offset such that future ID versions which may change the
82///   number of type or storage mode bits will not cause the version to be at a different offset.
83///   This is important so that a parser can always reliably read the version and then parse the
84///   remainder of the ID depending on the version. Having only 4 bits for the version is a trade
85///   off between future proofing to allow introducing more versions and the version requiring Proof
86///   of Work as part of the ID generation.
87/// - The version, type and storage mode are part of the prefix which is included in the
88///   representation of a non-fungible asset. The prefix alone is enough to determine all of these
89///   properties about the ID.
90/// - The most significant bit of the suffix must be zero to ensure the value of the suffix is
91///   always a valid felt, even if the lower 8 bits are all set to `1`. The lower 8 bits of the
92///   suffix may be overwritten when the ID is embedded in other layouts such as the
93///   [`NoteMetadata`](crate::note::NoteMetadata). In that case, it can happen that all lower bits
94///   of the encoded suffix are one, so having the zero bit constraint is important for validity.
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
96pub enum AccountId {
97    V0(AccountIdV0),
98}
99
100impl AccountId {
101    // CONSTANTS
102    // --------------------------------------------------------------------------------------------
103
104    /// The serialized size of an [`AccountId`] in bytes.
105    pub const SERIALIZED_SIZE: usize = 15;
106
107    // CONSTRUCTORS
108    // --------------------------------------------------------------------------------------------
109
110    /// Creates an [`AccountId`] by hashing the given `seed`, `code_commitment`,
111    /// `storage_commitment` and using the resulting first and second element of the hash as the
112    /// prefix and suffix felts of the ID.
113    ///
114    /// See the documentation of the [`AccountId`] for more details on the generation.
115    ///
116    /// # Errors
117    ///
118    /// Returns an error if any of the ID constraints are not met. See the [constraints
119    /// documentation](AccountId#constraints) for details.
120    pub fn new(
121        seed: Word,
122        version: AccountIdVersion,
123        code_commitment: Digest,
124        storage_commitment: Digest,
125    ) -> Result<Self, AccountIdError> {
126        match version {
127            AccountIdVersion::Version0 => {
128                AccountIdV0::new(seed, code_commitment, storage_commitment).map(Self::V0)
129            },
130        }
131    }
132
133    /// Creates an [`AccountId`] from the given felts where the felt at index 0 is the prefix
134    /// and the felt at index 1 is the suffix.
135    ///
136    /// # Warning
137    ///
138    /// Validity of the ID must be ensured by the caller. An invalid ID may lead to panics.
139    ///
140    /// # Panics
141    ///
142    /// Panics if the prefix does not contain a known account ID version.
143    ///
144    /// If debug_assertions are enabled (e.g. in debug mode), this function panics if any of the ID
145    /// constraints are not met. See the [constraints documentation](AccountId#constraints) for
146    /// details.
147    pub fn new_unchecked(elements: [Felt; 2]) -> Self {
148        // The prefix contains the metadata.
149        // If we add more versions in the future, we may need to generalize this.
150        match v0::extract_version(elements[0].as_int())
151            .expect("prefix should contain a valid account ID version")
152        {
153            AccountIdVersion::Version0 => Self::V0(AccountIdV0::new_unchecked(elements)),
154        }
155    }
156
157    /// Constructs an [`AccountId`] for testing purposes with the given account type, storage
158    /// mode.
159    ///
160    /// This function does the following:
161    /// - Split the given bytes into a `prefix = bytes[0..8]` and `suffix = bytes[8..]` part to be
162    ///   used for the prefix and suffix felts, respectively.
163    /// - The least significant byte of the prefix is set to the given version, type and storage
164    ///   mode.
165    /// - The 32nd most significant bit in the prefix is cleared to ensure it is a valid felt. The
166    ///   32nd is chosen as it is the lowest bit that we can clear and still ensure felt validity.
167    ///   This leaves the upper 31 bits to be set by the input `bytes` which makes it simpler to
168    ///   create test values which more often need specific values for the most significant end of
169    ///   the ID.
170    /// - In the suffix the most significant bit and the lower 8 bits are cleared.
171    #[cfg(any(feature = "testing", test))]
172    pub fn dummy(
173        bytes: [u8; 15],
174        version: AccountIdVersion,
175        account_type: AccountType,
176        storage_mode: AccountStorageMode,
177    ) -> AccountId {
178        match version {
179            AccountIdVersion::Version0 => {
180                Self::V0(AccountIdV0::dummy(bytes, account_type, storage_mode))
181            },
182        }
183    }
184
185    /// Grinds an account seed until its hash matches the given `account_type`, `storage_mode` and
186    /// `version` and returns it as a [`Word`]. The input to the hash function next to the seed are
187    /// the `code_commitment` and `storage_commitment`.
188    ///
189    /// The grinding process is started from the given `init_seed` which should be a random seed
190    /// generated from a cryptographically secure source.
191    pub fn compute_account_seed(
192        init_seed: [u8; 32],
193        account_type: AccountType,
194        storage_mode: AccountStorageMode,
195        version: AccountIdVersion,
196        code_commitment: Digest,
197        storage_commitment: Digest,
198    ) -> Result<Word, AccountError> {
199        match version {
200            AccountIdVersion::Version0 => AccountIdV0::compute_account_seed(
201                init_seed,
202                account_type,
203                storage_mode,
204                version,
205                code_commitment,
206                storage_commitment,
207            ),
208        }
209    }
210
211    // PUBLIC ACCESSORS
212    // --------------------------------------------------------------------------------------------
213
214    /// Returns the type of this account ID.
215    pub const fn account_type(&self) -> AccountType {
216        match self {
217            AccountId::V0(account_id) => account_id.account_type(),
218        }
219    }
220
221    /// Returns `true` if an account with this ID is a faucet which can issue assets.
222    pub fn is_faucet(&self) -> bool {
223        self.account_type().is_faucet()
224    }
225
226    /// Returns `true` if an account with this ID is a regular account.
227    pub fn is_regular_account(&self) -> bool {
228        self.account_type().is_regular_account()
229    }
230
231    /// Returns the storage mode of this account ID.
232    pub fn storage_mode(&self) -> AccountStorageMode {
233        match self {
234            AccountId::V0(account_id) => account_id.storage_mode(),
235        }
236    }
237
238    /// Returns `true` if the full state of the account is on chain, i.e. if the modes are
239    /// [`AccountStorageMode::Public`] or [`AccountStorageMode::Network`], `false` otherwise.
240    pub fn is_onchain(&self) -> bool {
241        self.storage_mode().is_onchain()
242    }
243
244    /// Returns `true` if the storage mode is [`AccountStorageMode::Public`], `false` otherwise.
245    pub fn is_public(&self) -> bool {
246        self.storage_mode().is_public()
247    }
248
249    /// Returns `true` if the storage mode is [`AccountStorageMode::Network`], `false` otherwise.
250    pub fn is_network(&self) -> bool {
251        self.storage_mode().is_network()
252    }
253
254    /// Returns `true` if the storage mode is [`AccountStorageMode::Private`], `false` otherwise.
255    pub fn is_private(&self) -> bool {
256        self.storage_mode().is_private()
257    }
258
259    /// Returns the version of this account ID.
260    pub fn version(&self) -> AccountIdVersion {
261        match self {
262            AccountId::V0(_) => AccountIdVersion::Version0,
263        }
264    }
265
266    /// Creates an [`AccountId`] from a hex string. Assumes the string starts with "0x" and
267    /// that the hexadecimal characters are big-endian encoded.
268    pub fn from_hex(hex_str: &str) -> Result<Self, AccountIdError> {
269        hex_to_bytes(hex_str)
270            .map_err(AccountIdError::AccountIdHexParseError)
271            .and_then(AccountId::try_from)
272    }
273
274    /// Returns a big-endian, hex-encoded string of length 32, including the `0x` prefix. This means
275    /// it encodes 15 bytes.
276    pub fn to_hex(self) -> String {
277        match self {
278            AccountId::V0(account_id) => account_id.to_hex(),
279        }
280    }
281
282    /// Encodes the [`AccountId`] into a [bech32](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) string.
283    ///
284    /// # Encoding
285    ///
286    /// The encoding of an account ID into bech32 is done as follows:
287    /// - Convert the account ID into its `[u8; 15]` data format.
288    /// - Insert the address type [`AddressType::AccountId`] byte at index 0, shifting all other
289    ///   elements to the right.
290    /// - Choose an HRP, defined as a [`NetworkId`], for example [`NetworkId::Mainnet`] whose string
291    ///   representation is `mm`.
292    /// - Encode the resulting HRP together with the data into a bech32 string using the
293    ///   [`bech32::Bech32m`] checksum algorithm.
294    ///
295    /// This is an example of an account ID in hex and bech32 representations:
296    ///
297    /// ```text
298    /// hex:    0x140fa04a1e61fc100000126ef8f1d6
299    /// bech32: mm1qq2qlgz2reslcyqqqqfxa7836chrjcvk
300    /// ```
301    ///
302    /// ## Rationale
303    ///
304    /// Having the address type at the very beginning is so that it can be decoded to detect the
305    /// type of the address without having to decode the entire data. Moreover, choosing the
306    /// address type as a multiple of 8 means the first character of the bech32 string after the
307    /// `1` separator will be different for every address type. This makes the type of the address
308    /// conveniently human-readable.
309    ///
310    /// The only allowed checksum algorithm is [`Bech32m`](bech32::Bech32m) due to being the best
311    /// available checksum algorithm with no known weaknesses (unlike [`Bech32`](bech32::Bech32)).
312    /// No checksum is also not allowed since the intended use of bech32 is to have error
313    /// detection capabilities.
314    pub fn to_bech32(&self, network_id: NetworkId) -> String {
315        match self {
316            AccountId::V0(account_id_v0) => account_id_v0.to_bech32(network_id),
317        }
318    }
319
320    /// Decodes a [bech32](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) string into an [`AccountId`].
321    ///
322    /// See [`AccountId::to_bech32`] for details on the format. The procedure for decoding the
323    /// bech32 data into the ID consists of the inverse operations of encoding.
324    pub fn from_bech32(bech32_string: &str) -> Result<(NetworkId, Self), AccountIdError> {
325        AccountIdV0::from_bech32(bech32_string)
326            .map(|(network_id, account_id)| (network_id, AccountId::V0(account_id)))
327    }
328
329    /// Returns the [`AccountIdPrefix`] of this ID.
330    ///
331    /// The prefix of an account ID is guaranteed to be unique.
332    pub fn prefix(&self) -> AccountIdPrefix {
333        match self {
334            AccountId::V0(account_id) => AccountIdPrefix::V0(account_id.prefix()),
335        }
336    }
337
338    /// Returns the suffix of this ID as a [`Felt`].
339    pub const fn suffix(&self) -> Felt {
340        match self {
341            AccountId::V0(account_id) => account_id.suffix(),
342        }
343    }
344}
345
346// CONVERSIONS FROM ACCOUNT ID
347// ================================================================================================
348
349impl From<AccountId> for [Felt; 2] {
350    fn from(id: AccountId) -> Self {
351        match id {
352            AccountId::V0(account_id) => account_id.into(),
353        }
354    }
355}
356
357impl From<AccountId> for [u8; 15] {
358    fn from(id: AccountId) -> Self {
359        match id {
360            AccountId::V0(account_id) => account_id.into(),
361        }
362    }
363}
364
365impl From<AccountId> for u128 {
366    fn from(id: AccountId) -> Self {
367        match id {
368            AccountId::V0(account_id) => account_id.into(),
369        }
370    }
371}
372
373// CONVERSIONS TO ACCOUNT ID
374// ================================================================================================
375
376impl From<AccountIdV0> for AccountId {
377    fn from(id: AccountIdV0) -> Self {
378        Self::V0(id)
379    }
380}
381
382impl TryFrom<[Felt; 2]> for AccountId {
383    type Error = AccountIdError;
384
385    /// Returns an [`AccountId`] instantiated with the provided field elements where `elements[0]`
386    /// is taken as the prefix and `elements[1]` is taken as the suffix.
387    ///
388    /// # Errors
389    ///
390    /// Returns an error if any of the ID constraints are not met. See the [constraints
391    /// documentation](AccountId#constraints) for details.
392    fn try_from(elements: [Felt; 2]) -> Result<Self, Self::Error> {
393        // The prefix contains the metadata.
394        // If we add more versions in the future, we may need to generalize this.
395        match v0::extract_version(elements[0].as_int())? {
396            AccountIdVersion::Version0 => AccountIdV0::try_from(elements).map(Self::V0),
397        }
398    }
399}
400
401impl TryFrom<[u8; 15]> for AccountId {
402    type Error = AccountIdError;
403
404    /// Tries to convert a byte array in big-endian order to an [`AccountId`].
405    ///
406    /// # Errors
407    ///
408    /// Returns an error if any of the ID constraints are not met. See the [constraints
409    /// documentation](AccountId#constraints) for details.
410    fn try_from(bytes: [u8; 15]) -> Result<Self, Self::Error> {
411        // The least significant byte of the ID prefix contains the metadata.
412        let metadata_byte = bytes[7];
413        // We only have one supported version for now, so we use the extractor from that version.
414        // If we add more versions in the future, we may need to generalize this.
415        let version = v0::extract_version(metadata_byte as u64)?;
416
417        match version {
418            AccountIdVersion::Version0 => AccountIdV0::try_from(bytes).map(Self::V0),
419        }
420    }
421}
422
423impl TryFrom<u128> for AccountId {
424    type Error = AccountIdError;
425
426    /// Tries to convert a u128 into an [`AccountId`].
427    ///
428    /// # Errors
429    ///
430    /// Returns an error if any of the ID constraints are not met. See the [constraints
431    /// documentation](AccountId#constraints) for details.
432    fn try_from(int: u128) -> Result<Self, Self::Error> {
433        let mut bytes: [u8; 15] = [0; 15];
434        bytes.copy_from_slice(&int.to_be_bytes()[0..15]);
435
436        Self::try_from(bytes)
437    }
438}
439
440// COMMON TRAIT IMPLS
441// ================================================================================================
442
443impl PartialOrd for AccountId {
444    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
445        Some(self.cmp(other))
446    }
447}
448
449impl Ord for AccountId {
450    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
451        u128::from(*self).cmp(&u128::from(*other))
452    }
453}
454
455impl fmt::Display for AccountId {
456    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
457        write!(f, "{}", self.to_hex())
458    }
459}
460
461// SERIALIZATION
462// ================================================================================================
463
464impl Serializable for AccountId {
465    fn write_into<W: miden_crypto::utils::ByteWriter>(&self, target: &mut W) {
466        match self {
467            AccountId::V0(account_id) => {
468                account_id.write_into(target);
469            },
470        }
471    }
472
473    fn get_size_hint(&self) -> usize {
474        match self {
475            AccountId::V0(account_id) => account_id.get_size_hint(),
476        }
477    }
478}
479
480impl Deserializable for AccountId {
481    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
482        <[u8; 15]>::read_from(source)?
483            .try_into()
484            .map_err(|err: AccountIdError| DeserializationError::InvalidValue(err.to_string()))
485    }
486}
487
488// TESTS
489// ================================================================================================
490
491#[cfg(test)]
492mod tests {
493    use assert_matches::assert_matches;
494    use bech32::{Bech32, Bech32m, Hrp, NoChecksum};
495
496    use super::*;
497    use crate::{
498        account::account_id::{
499            address_type::AddressType,
500            v0::{extract_storage_mode, extract_type, extract_version},
501        },
502        errors::Bech32Error,
503        testing::account_id::{
504            ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET, ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
505            ACCOUNT_ID_PRIVATE_SENDER, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
506            ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
507            ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
508        },
509    };
510
511    #[test]
512    fn test_account_id_wrapper_conversion_roundtrip() {
513        for (idx, account_id) in [
514            ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
515            ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
516            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
517            ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
518            ACCOUNT_ID_PRIVATE_SENDER,
519            ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET,
520        ]
521        .into_iter()
522        .enumerate()
523        {
524            let wrapper = AccountId::try_from(account_id).unwrap();
525            assert_eq!(
526                wrapper,
527                AccountId::read_from_bytes(&wrapper.to_bytes()).unwrap(),
528                "failed in {idx}"
529            );
530        }
531    }
532
533    #[test]
534    fn bech32_encode_decode_roundtrip() {
535        // We use this to check that encoding does not panic even when using the longest possible
536        // HRP.
537        let longest_possible_hrp =
538            "01234567890123456789012345678901234567890123456789012345678901234567890123456789012";
539        assert_eq!(longest_possible_hrp.len(), 83);
540
541        for network_id in [
542            NetworkId::Mainnet,
543            NetworkId::Custom(Hrp::parse("custom").unwrap()),
544            NetworkId::Custom(Hrp::parse(longest_possible_hrp).unwrap()),
545        ] {
546            for (idx, account_id) in [
547                ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
548                ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
549                ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
550                ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
551                ACCOUNT_ID_PRIVATE_SENDER,
552            ]
553            .into_iter()
554            .enumerate()
555            {
556                let account_id = AccountId::try_from(account_id).unwrap();
557
558                let bech32_string = account_id.to_bech32(network_id);
559                let (decoded_network_id, decoded_account_id) =
560                    AccountId::from_bech32(&bech32_string).unwrap();
561
562                assert_eq!(network_id, decoded_network_id, "network id failed in {idx}");
563                assert_eq!(account_id, decoded_account_id, "account id failed in {idx}");
564
565                let (_, data) = bech32::decode(&bech32_string).unwrap();
566
567                // Raw bech32 data should contain the address type as the first byte.
568                assert_eq!(data[0], AddressType::AccountId as u8);
569
570                // Raw bech32 data should contain the metadata byte at index 8.
571                assert_eq!(extract_version(data[8] as u64).unwrap(), account_id.version());
572                assert_eq!(extract_type(data[8] as u64), account_id.account_type());
573                assert_eq!(
574                    extract_storage_mode(data[8] as u64).unwrap(),
575                    account_id.storage_mode()
576                );
577            }
578        }
579    }
580
581    #[test]
582    fn bech32_invalid_checksum() {
583        let network_id = NetworkId::Mainnet;
584        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
585
586        let bech32_string = account_id.to_bech32(network_id);
587        let mut invalid_bech32_1 = bech32_string.clone();
588        invalid_bech32_1.remove(0);
589        let mut invalid_bech32_2 = bech32_string.clone();
590        invalid_bech32_2.remove(7);
591
592        let error = AccountId::from_bech32(&invalid_bech32_1).unwrap_err();
593        assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
594
595        let error = AccountId::from_bech32(&invalid_bech32_2).unwrap_err();
596        assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
597    }
598
599    #[test]
600    fn bech32_invalid_address_type() {
601        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
602        let mut id_bytes = account_id.to_bytes();
603
604        // Set invalid address type.
605        id_bytes.insert(0, 16);
606
607        let invalid_bech32 =
608            bech32::encode::<Bech32m>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
609
610        let error = AccountId::from_bech32(&invalid_bech32).unwrap_err();
611        assert_matches!(
612            error,
613            AccountIdError::Bech32DecodeError(Bech32Error::UnknownAddressType(16))
614        );
615    }
616
617    #[test]
618    fn bech32_invalid_other_checksum() {
619        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
620        let mut id_bytes = account_id.to_bytes();
621        id_bytes.insert(0, AddressType::AccountId as u8);
622
623        // Use Bech32 instead of Bech32m which is disallowed.
624        let invalid_bech32_regular =
625            bech32::encode::<Bech32>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
626        let error = AccountId::from_bech32(&invalid_bech32_regular).unwrap_err();
627        assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
628
629        // Use no checksum instead of Bech32m which is disallowed.
630        let invalid_bech32_no_checksum =
631            bech32::encode::<NoChecksum>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
632        let error = AccountId::from_bech32(&invalid_bech32_no_checksum).unwrap_err();
633        assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
634    }
635
636    #[test]
637    fn bech32_invalid_length() {
638        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
639        let mut id_bytes = account_id.to_bytes();
640        id_bytes.insert(0, AddressType::AccountId as u8);
641        // Add one byte to make the length invalid.
642        id_bytes.push(5);
643
644        let invalid_bech32 =
645            bech32::encode::<Bech32m>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
646
647        let error = AccountId::from_bech32(&invalid_bech32).unwrap_err();
648        assert_matches!(
649            error,
650            AccountIdError::Bech32DecodeError(Bech32Error::InvalidDataLength { .. })
651        );
652    }
653}