Skip to main content

miden_protocol/account/account_id/
mod.rs

1pub(crate) mod v1;
2pub use v1::{AccountIdPrefixV1, AccountIdV1};
3
4mod id_prefix;
5pub use id_prefix::AccountIdPrefix;
6
7mod seed;
8
9mod account_type;
10pub use account_type::AccountType;
11
12mod asset_callback_flag;
13pub use asset_callback_flag::AssetCallbackFlag;
14
15mod id_version;
16use alloc::string::{String, ToString};
17use core::fmt;
18
19use bech32::primitives::decode::ByteIter;
20pub use id_version::AccountIdVersion;
21use miden_core::Felt;
22use miden_crypto::utils::hex_to_bytes;
23
24use crate::Word;
25use crate::address::NetworkId;
26use crate::errors::{AccountError, AccountIdError};
27use crate::utils::serde::{
28    ByteReader,
29    ByteWriter,
30    Deserializable,
31    DeserializationError,
32    Serializable,
33};
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 1.
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 (58 bits) | asset callback flag (1 bit) | account type (1 bit) | 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 account type, asset
55/// callback flag 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 account type, asset callback flag, version and the
63/// most significant bit in the suffix, generating an ID requires 7 bits of Proof-of-Work.
64///
65/// # Constraints
66///
67/// Constructors will return an error if:
68///
69/// - The prefix contains an account ID version that does not match any of the known values.
70/// - The most significant bit of the suffix is not zero.
71/// - The lower 8 bits of the suffix are not zero, although [`AccountId::new`] ensures this is the
72///   case rather than return an error.
73///
74/// # Design Rationale
75///
76/// The rationale behind the above layout is as follows.
77///
78/// - The prefix is the output of a hash function so it will be a valid field element without
79///   requiring additional constraints.
80/// - The version is placed at a static offset such that future ID versions which may change the
81///   number of account type bits will not cause the version to be at a different offset. This is
82///   important so that a parser can always reliably read the version and then parse the remainder
83///   of the ID depending on the version. Having only 4 bits for the version is a trade off between
84///   future proofing to allow introducing more versions and the version requiring Proof of Work as
85///   part of the ID generation.
86/// - The asset callback flag is placed in the account ID so it is immutable throughout the lifetime
87///   of the account and every asset issued by an account has the same flag.
88/// - The most significant bit of the suffix must be zero to ensure the value of the suffix is
89///   always a valid felt, even if the lower 8 bits are all set to `1`. The lower 8 bits of the
90///   suffix may be overwritten when the ID is embedded in other layouts such as the
91///   [`NoteMetadata`](crate::note::NoteMetadata). In that case, it can happen that all lower bits
92///   of the encoded suffix are one, so having the zero bit constraint is important for validity.
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94pub enum AccountId {
95    V1(AccountIdV1),
96}
97
98impl AccountId {
99    // CONSTANTS
100    // --------------------------------------------------------------------------------------------
101
102    /// The serialized size of an [`AccountId`] in bytes.
103    pub const SERIALIZED_SIZE: usize = 15;
104
105    // CONSTRUCTORS
106    // --------------------------------------------------------------------------------------------
107
108    /// Creates an [`AccountId`] by hashing the given `seed`, `code_commitment`,
109    /// `storage_commitment` and using the resulting first and second element of the hash as the
110    /// prefix and suffix felts of the ID.
111    ///
112    /// See the documentation of the [`AccountId`] for more details on the generation.
113    ///
114    /// # Errors
115    ///
116    /// Returns an error if any of the ID constraints are not met. See the [constraints
117    /// documentation](AccountId#constraints) for details.
118    pub fn new(
119        seed: Word,
120        version: AccountIdVersion,
121        code_commitment: Word,
122        storage_commitment: Word,
123    ) -> Result<Self, AccountIdError> {
124        match version {
125            AccountIdVersion::Version1 => {
126                AccountIdV1::new(seed, code_commitment, storage_commitment).map(Self::V1)
127            },
128        }
129    }
130
131    /// Creates an [`AccountId`] from the given felts where the felt at index 0 is the prefix
132    /// and the felt at index 1 is the suffix.
133    ///
134    /// # Warning
135    ///
136    /// Validity of the ID must be ensured by the caller. An invalid ID may lead to panics.
137    ///
138    /// # Panics
139    ///
140    /// Panics if the prefix does not contain a known account ID version.
141    ///
142    /// If debug_assertions are enabled (e.g. in debug mode), this function panics if any of the ID
143    /// constraints are not met. See the [constraints documentation](AccountId#constraints) for
144    /// details.
145    pub fn new_unchecked(elements: [Felt; 2]) -> Self {
146        // The prefix contains the metadata.
147        // If we add more versions in the future, we may need to generalize this.
148        match v1::extract_version(elements[0].as_canonical_u64())
149            .expect("prefix should contain a valid account ID version")
150        {
151            AccountIdVersion::Version1 => Self::V1(AccountIdV1::new_unchecked(elements)),
152        }
153    }
154
155    /// Decodes an [`AccountId`] from the provided suffix and prefix felts.
156    ///
157    /// # Errors
158    ///
159    /// Returns an error if any of the ID constraints are not met. See the [constraints
160    /// documentation](AccountId#constraints) for details.
161    pub fn try_from_elements(suffix: Felt, prefix: Felt) -> Result<Self, AccountIdError> {
162        // The prefix contains the metadata.
163        // If we add more versions in the future, we may need to generalize this.
164        match v1::extract_version(prefix.as_canonical_u64())? {
165            AccountIdVersion::Version1 => {
166                AccountIdV1::try_from_elements(suffix, prefix).map(Self::V1)
167            },
168        }
169    }
170
171    /// Constructs an [`AccountId`] for testing purposes with the given account type.
172    ///
173    /// This function does the following:
174    /// - Split the given bytes into a `prefix = bytes[0..8]` and `suffix = bytes[8..]` part to be
175    ///   used for the prefix and suffix felts, respectively.
176    /// - The least significant byte of the prefix is set to the given version and account type.
177    /// - The 32nd most significant bit in the prefix is cleared to ensure it is a valid felt. The
178    ///   32nd is chosen as it is the lowest bit that we can clear and still ensure felt validity.
179    ///   This leaves the upper 31 bits to be set by the input `bytes` which makes it simpler to
180    ///   create test values which more often need specific values for the most significant end of
181    ///   the ID.
182    /// - In the suffix the most significant bit and the lower 8 bits are cleared.
183    #[cfg(any(feature = "testing", test))]
184    pub fn dummy(
185        bytes: [u8; 15],
186        version: AccountIdVersion,
187        account_type: AccountType,
188        asset_callbacks: AssetCallbackFlag,
189    ) -> AccountId {
190        match version {
191            AccountIdVersion::Version1 => {
192                Self::V1(AccountIdV1::dummy(bytes, account_type, asset_callbacks))
193            },
194        }
195    }
196
197    /// Grinds an account seed until its hash matches the given `account_type`, `asset_callbacks`
198    /// and `version` and returns it as a [`Word`]. The input to the hash function next to the
199    /// seed are the `code_commitment` and `storage_commitment`.
200    ///
201    /// The grinding process is started from the given `init_seed` which should be a random seed
202    /// generated from a cryptographically secure source.
203    pub fn compute_account_seed(
204        init_seed: [u8; 32],
205        account_type: AccountType,
206        asset_callbacks: AssetCallbackFlag,
207        version: AccountIdVersion,
208        code_commitment: Word,
209        storage_commitment: Word,
210    ) -> Result<Word, AccountError> {
211        match version {
212            AccountIdVersion::Version1 => AccountIdV1::compute_account_seed(
213                init_seed,
214                account_type,
215                asset_callbacks,
216                version,
217                code_commitment,
218                storage_commitment,
219            ),
220        }
221    }
222
223    // PUBLIC ACCESSORS
224    // --------------------------------------------------------------------------------------------
225
226    /// Returns the account type of this account ID.
227    pub fn account_type(&self) -> AccountType {
228        match self {
229            AccountId::V1(account_id) => account_id.account_type(),
230        }
231    }
232
233    /// Returns the [`AssetCallbackFlag`] of this account ID.
234    pub fn asset_callback_flag(&self) -> AssetCallbackFlag {
235        match self {
236            AccountId::V1(account_id) => account_id.asset_callback_flag(),
237        }
238    }
239
240    /// Returns `true` if the account type is [`AccountType::Public`], `false` otherwise.
241    pub fn is_public(&self) -> bool {
242        self.account_type().is_public()
243    }
244
245    /// Returns `true` if the account type is [`AccountType::Private`], `false` otherwise.
246    pub fn is_private(&self) -> bool {
247        self.account_type().is_private()
248    }
249
250    /// Returns the version of this account ID.
251    pub fn version(&self) -> AccountIdVersion {
252        match self {
253            AccountId::V1(_) => AccountIdVersion::Version1,
254        }
255    }
256
257    /// Creates an [`AccountId`] from a hex string. Assumes the string starts with "0x" and
258    /// that the hexadecimal characters are big-endian encoded.
259    pub fn from_hex(hex_str: &str) -> Result<Self, AccountIdError> {
260        hex_to_bytes(hex_str)
261            .map_err(AccountIdError::AccountIdHexParseError)
262            .and_then(AccountId::try_from)
263    }
264
265    /// Returns a big-endian, hex-encoded string of length 32, including the `0x` prefix. This means
266    /// it encodes 15 bytes.
267    pub fn to_hex(self) -> String {
268        match self {
269            AccountId::V1(account_id) => account_id.to_hex(),
270        }
271    }
272
273    /// Encodes the [`AccountId`] into a [bech32](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki)
274    /// string.
275    ///
276    /// # Encoding
277    ///
278    /// The encoding of an account ID into bech32 is done as follows:
279    /// - Convert the account ID into its `[u8; 15]` data format.
280    /// - Insert the address type `AddressType::AccountId` byte at index 0, shifting all other
281    ///   elements to the right.
282    /// - Choose an HRP, defined as a [`NetworkId`], for example [`NetworkId::Mainnet`] whose string
283    ///   representation is `mm`.
284    /// - Encode the resulting HRP together with the data into a bech32 string using the
285    ///   [`bech32::Bech32m`] checksum algorithm.
286    ///
287    /// This is an example of an account ID in hex and bech32 representations:
288    ///
289    /// ```text
290    /// hex:    0x6d449e4034fadca075d1976fef7e38
291    /// bech32: mm1apk5f8jqxnadegr46xtklmm78qhdgkwc
292    /// ```
293    ///
294    /// ## Rationale
295    ///
296    /// Having the address type at the very beginning is so that it can be decoded to detect the
297    /// type of the address without having to decode the entire data. Moreover, choosing the
298    /// address type as a multiple of 8 means the first character of the bech32 string after the
299    /// `1` separator will be different for every address type. This makes the type of the address
300    /// conveniently human-readable.
301    pub fn to_bech32(&self, network_id: NetworkId) -> String {
302        match self {
303            AccountId::V1(account_id_v1) => account_id_v1.to_bech32(network_id),
304        }
305    }
306
307    /// Decodes a [bech32](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) string into an [`AccountId`].
308    ///
309    /// See [`AccountId::to_bech32`] for details on the format. The procedure for decoding the
310    /// bech32 data into the ID consists of the inverse operations of encoding.
311    pub fn from_bech32(bech32_string: &str) -> Result<(NetworkId, Self), AccountIdError> {
312        AccountIdV1::from_bech32(bech32_string)
313            .map(|(network_id, account_id)| (network_id, AccountId::V1(account_id)))
314    }
315
316    /// Parses a string into an [`AccountId`].
317    ///
318    /// This function supports parsing from both hex (`0x...`) and bech32 formats.
319    ///
320    /// # Returns
321    ///
322    /// Returns a tuple of the parsed [`AccountId`] and an optional [`NetworkId`].
323    /// - For hex strings: `NetworkId` is `None`.
324    /// - For bech32 strings: `NetworkId` is `Some(...)`.
325    ///
326    /// # Errors
327    ///
328    /// Returns an error if the string cannot be parsed as either hex or bech32 format.
329    pub fn parse(s: &str) -> Result<(Self, Option<NetworkId>), AccountIdError> {
330        if let Some(hex_digits) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
331            // Normalize to lowercase "0x" prefix for from_hex
332            let normalized = format!("0x{hex_digits}");
333            Self::from_hex(&normalized).map(|id| (id, None))
334        } else {
335            Self::from_bech32(s).map(|(network_id, id)| (id, Some(network_id)))
336        }
337    }
338
339    /// Decodes the data from the bech32 byte iterator into an [`AccountId`].
340    pub(crate) fn from_bech32_byte_iter(byte_iter: ByteIter<'_>) -> Result<Self, AccountIdError> {
341        AccountIdV1::from_bech32_byte_iter(byte_iter).map(AccountId::V1)
342    }
343
344    /// Returns the [`AccountIdPrefix`] of this ID.
345    ///
346    /// The prefix of an account ID is guaranteed to be unique.
347    pub fn prefix(&self) -> AccountIdPrefix {
348        match self {
349            AccountId::V1(account_id) => AccountIdPrefix::V1(account_id.prefix()),
350        }
351    }
352
353    /// Returns the suffix of this ID as a [`Felt`].
354    pub const fn suffix(&self) -> Felt {
355        match self {
356            AccountId::V1(account_id) => account_id.suffix(),
357        }
358    }
359}
360
361// CONVERSIONS FROM ACCOUNT ID
362// ================================================================================================
363
364impl From<AccountId> for [Felt; 2] {
365    fn from(id: AccountId) -> Self {
366        match id {
367            AccountId::V1(account_id) => account_id.into(),
368        }
369    }
370}
371
372impl From<AccountId> for [u8; 15] {
373    fn from(id: AccountId) -> Self {
374        match id {
375            AccountId::V1(account_id) => account_id.into(),
376        }
377    }
378}
379
380impl From<AccountId> for u128 {
381    fn from(id: AccountId) -> Self {
382        match id {
383            AccountId::V1(account_id) => account_id.into(),
384        }
385    }
386}
387
388// CONVERSIONS TO ACCOUNT ID
389// ================================================================================================
390
391impl From<AccountIdV1> for AccountId {
392    fn from(id: AccountIdV1) -> Self {
393        Self::V1(id)
394    }
395}
396
397impl TryFrom<[u8; 15]> for AccountId {
398    type Error = AccountIdError;
399
400    /// Tries to convert a byte array in big-endian order to an [`AccountId`].
401    ///
402    /// # Errors
403    ///
404    /// Returns an error if any of the ID constraints are not met. See the [constraints
405    /// documentation](AccountId#constraints) for details.
406    fn try_from(bytes: [u8; 15]) -> Result<Self, Self::Error> {
407        // The least significant byte of the ID prefix contains the metadata.
408        let metadata_byte = bytes[7];
409        // We only have one supported version for now, so we use the extractor from that version.
410        // If we add more versions in the future, we may need to generalize this.
411        let version = v1::extract_version(metadata_byte as u64)?;
412
413        match version {
414            AccountIdVersion::Version1 => AccountIdV1::try_from(bytes).map(Self::V1),
415        }
416    }
417}
418
419impl TryFrom<u128> for AccountId {
420    type Error = AccountIdError;
421
422    /// Tries to convert a u128 into an [`AccountId`].
423    ///
424    /// # Errors
425    ///
426    /// Returns an error if any of the ID constraints are not met. See the [constraints
427    /// documentation](AccountId#constraints) for details.
428    fn try_from(int: u128) -> Result<Self, Self::Error> {
429        let mut bytes: [u8; 15] = [0; 15];
430        bytes.copy_from_slice(&int.to_be_bytes()[0..15]);
431
432        Self::try_from(bytes)
433    }
434}
435
436// COMMON TRAIT IMPLS
437// ================================================================================================
438
439impl PartialOrd for AccountId {
440    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
441        Some(self.cmp(other))
442    }
443}
444
445impl Ord for AccountId {
446    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
447        u128::from(*self).cmp(&u128::from(*other))
448    }
449}
450
451impl fmt::Display for AccountId {
452    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
453        write!(f, "{}", self.to_hex())
454    }
455}
456
457// SERIALIZATION
458// ================================================================================================
459
460impl Serializable for AccountId {
461    fn write_into<W: ByteWriter>(&self, target: &mut W) {
462        match self {
463            AccountId::V1(account_id) => {
464                account_id.write_into(target);
465            },
466        }
467    }
468
469    fn get_size_hint(&self) -> usize {
470        match self {
471            AccountId::V1(account_id) => account_id.get_size_hint(),
472        }
473    }
474}
475
476impl Deserializable for AccountId {
477    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
478        <[u8; 15]>::read_from(source)?
479            .try_into()
480            .map_err(|err: AccountIdError| DeserializationError::InvalidValue(err.to_string()))
481    }
482}
483
484// TESTS
485// ================================================================================================
486
487#[cfg(test)]
488mod tests {
489    use alloc::boxed::Box;
490
491    use assert_matches::assert_matches;
492    use bech32::{Bech32, Bech32m, NoChecksum};
493
494    use super::*;
495    use crate::account::account_id::v1::{extract_account_type, extract_version};
496    use crate::address::{AddressType, CustomNetworkId};
497    use crate::errors::Bech32Error;
498    use crate::testing::account_id::{
499        ACCOUNT_ID_MAX_ZEROES,
500        ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
501        ACCOUNT_ID_PRIVATE_SENDER,
502        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
503        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_WITH_CALLBACKS,
504        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
505        ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
506        ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
507        AccountIdBuilder,
508    };
509
510    #[test]
511    fn test_account_id_wrapper_conversion_roundtrip() {
512        for (idx, account_id) in [
513            ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
514            ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
515            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
516            ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
517            ACCOUNT_ID_PRIVATE_SENDER,
518            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
519            ACCOUNT_ID_MAX_ZEROES,
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() -> anyhow::Result<()> {
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        let random_id = AccountIdBuilder::new().build_with_rng(&mut rand::rng());
542
543        for network_id in [
544            NetworkId::Mainnet,
545            NetworkId::Custom(Box::new("custom".parse::<CustomNetworkId>()?)),
546            NetworkId::Custom(Box::new(longest_possible_hrp.parse::<CustomNetworkId>()?)),
547        ] {
548            for account_id in [
549                ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
550                ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
551                ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
552                ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_WITH_CALLBACKS,
553                ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
554                ACCOUNT_ID_PRIVATE_SENDER,
555                random_id.into(),
556            ]
557            .into_iter()
558            {
559                let account_id = AccountId::try_from(account_id).unwrap();
560
561                let bech32_string = account_id.to_bech32(network_id.clone());
562                let (decoded_network_id, decoded_account_id) =
563                    AccountId::from_bech32(&bech32_string).unwrap();
564
565                assert_eq!(network_id, decoded_network_id, "network id failed for {account_id}",);
566                assert_eq!(account_id, decoded_account_id, "account id failed for {account_id}");
567                assert_eq!(
568                    account_id.asset_callback_flag(),
569                    decoded_account_id.asset_callback_flag(),
570                    "asset callback flag failed for {account_id}"
571                );
572
573                let (_, data) = bech32::decode(&bech32_string).unwrap();
574
575                // Raw bech32 data should contain the address type as the first byte.
576                assert_eq!(data[0], AddressType::AccountId as u8);
577
578                // Raw bech32 data should contain the metadata byte at index 8.
579                assert_eq!(extract_version(data[8] as u64).unwrap(), account_id.version());
580                assert_eq!(extract_account_type(data[8] as u64), account_id.account_type());
581            }
582        }
583
584        Ok(())
585    }
586
587    #[test]
588    fn bech32_invalid_checksum() {
589        let network_id = NetworkId::Mainnet;
590        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
591
592        let bech32_string = account_id.to_bech32(network_id);
593        let mut invalid_bech32_1 = bech32_string.clone();
594        invalid_bech32_1.remove(0);
595        let mut invalid_bech32_2 = bech32_string.clone();
596        invalid_bech32_2.remove(7);
597
598        let error = AccountId::from_bech32(&invalid_bech32_1).unwrap_err();
599        assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
600
601        let error = AccountId::from_bech32(&invalid_bech32_2).unwrap_err();
602        assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
603    }
604
605    #[test]
606    fn bech32_invalid_address_type() {
607        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
608        let mut id_bytes = account_id.to_bytes();
609
610        // Set invalid address type.
611        id_bytes.insert(0, 16);
612
613        let invalid_bech32 =
614            bech32::encode::<Bech32m>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
615
616        let error = AccountId::from_bech32(&invalid_bech32).unwrap_err();
617        assert_matches!(
618            error,
619            AccountIdError::Bech32DecodeError(Bech32Error::UnknownAddressType(16))
620        );
621    }
622
623    #[test]
624    fn bech32_invalid_other_checksum() {
625        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
626        let mut id_bytes = account_id.to_bytes();
627        id_bytes.insert(0, AddressType::AccountId as u8);
628
629        // Use Bech32 instead of Bech32m which is disallowed.
630        let invalid_bech32_regular =
631            bech32::encode::<Bech32>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
632        let error = AccountId::from_bech32(&invalid_bech32_regular).unwrap_err();
633        assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
634
635        // Use no checksum instead of Bech32m which is disallowed.
636        let invalid_bech32_no_checksum =
637            bech32::encode::<NoChecksum>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
638        let error = AccountId::from_bech32(&invalid_bech32_no_checksum).unwrap_err();
639        assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
640    }
641
642    #[test]
643    fn bech32_invalid_length() {
644        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
645        let mut id_bytes = account_id.to_bytes();
646        id_bytes.insert(0, AddressType::AccountId as u8);
647        // Add one byte to make the length invalid.
648        id_bytes.push(5);
649
650        let invalid_bech32 =
651            bech32::encode::<Bech32m>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
652
653        let error = AccountId::from_bech32(&invalid_bech32).unwrap_err();
654        assert_matches!(
655            error,
656            AccountIdError::Bech32DecodeError(Bech32Error::InvalidDataLength { .. })
657        );
658    }
659
660    #[test]
661    fn parse_hex_string() {
662        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
663        let hex_string = account_id.to_hex();
664
665        let (parsed_id, network_id) = AccountId::parse(&hex_string).unwrap();
666
667        assert_eq!(parsed_id, account_id);
668        assert!(network_id.is_none());
669    }
670
671    #[test]
672    fn parse_hex_string_uppercase() {
673        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
674        // Keep "0x" prefix lowercase, only uppercase the hex digits
675        let hex_string = account_id.to_hex();
676        let hex_string = format!("0x{}", hex_string[2..].to_uppercase());
677
678        let (parsed_id, network_id) = AccountId::parse(&hex_string).unwrap();
679
680        assert_eq!(parsed_id, account_id);
681        assert!(network_id.is_none());
682    }
683
684    #[test]
685    fn parse_hex_string_uppercase_prefix() {
686        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
687        // Use "0X" prefix (uppercase X) with uppercase hex digits
688        let hex_string = account_id.to_hex();
689        let hex_string = format!("0X{}", hex_string[2..].to_uppercase());
690
691        let (parsed_id, network_id) = AccountId::parse(&hex_string).unwrap();
692
693        assert_eq!(parsed_id, account_id);
694        assert!(network_id.is_none());
695    }
696
697    #[test]
698    fn parse_bech32_string() {
699        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
700        let bech32_string = account_id.to_bech32(NetworkId::Mainnet);
701
702        let (parsed_id, parsed_network_id) = AccountId::parse(&bech32_string).unwrap();
703
704        assert_eq!(parsed_id, account_id);
705        assert_eq!(parsed_network_id, Some(NetworkId::Mainnet));
706    }
707
708    #[test]
709    fn parse_invalid_string() {
710        let error = AccountId::parse("invalid_string").unwrap_err();
711        assert_matches!(error, AccountIdError::Bech32DecodeError(_));
712
713        let error = AccountId::parse("0xinvalid").unwrap_err();
714        assert_matches!(error, AccountIdError::AccountIdHexParseError(_));
715    }
716}