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