Skip to main content

miden_protocol/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 account_type;
10pub use account_type::AccountType;
11
12mod storage_mode;
13pub use storage_mode::AccountStorageMode;
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 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: Word,
124        storage_commitment: Word,
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_canonical_u64())
151            .expect("prefix should contain a valid account ID version")
152        {
153            AccountIdVersion::Version0 => Self::V0(AccountIdV0::new_unchecked(elements)),
154        }
155    }
156
157    /// Decodes an [`AccountId`] from the provided suffix and prefix felts.
158    ///
159    /// # Errors
160    ///
161    /// Returns an error if any of the ID constraints are not met. See the [constraints
162    /// documentation](AccountId#constraints) for details.
163    pub fn try_from_elements(suffix: Felt, prefix: Felt) -> Result<Self, AccountIdError> {
164        // The prefix contains the metadata.
165        // If we add more versions in the future, we may need to generalize this.
166        match v0::extract_version(prefix.as_canonical_u64())? {
167            AccountIdVersion::Version0 => {
168                AccountIdV0::try_from_elements(suffix, prefix).map(Self::V0)
169            },
170        }
171    }
172
173    /// Constructs an [`AccountId`] for testing purposes with the given account type, storage
174    /// mode.
175    ///
176    /// This function does the following:
177    /// - Split the given bytes into a `prefix = bytes[0..8]` and `suffix = bytes[8..]` part to be
178    ///   used for the prefix and suffix felts, respectively.
179    /// - The least significant byte of the prefix is set to the given version, type and storage
180    ///   mode.
181    /// - The 32nd most significant bit in the prefix is cleared to ensure it is a valid felt. The
182    ///   32nd is chosen as it is the lowest bit that we can clear and still ensure felt validity.
183    ///   This leaves the upper 31 bits to be set by the input `bytes` which makes it simpler to
184    ///   create test values which more often need specific values for the most significant end of
185    ///   the ID.
186    /// - In the suffix the most significant bit and the lower 8 bits are cleared.
187    #[cfg(any(feature = "testing", test))]
188    pub fn dummy(
189        bytes: [u8; 15],
190        version: AccountIdVersion,
191        account_type: AccountType,
192        storage_mode: AccountStorageMode,
193    ) -> AccountId {
194        match version {
195            AccountIdVersion::Version0 => {
196                Self::V0(AccountIdV0::dummy(bytes, account_type, storage_mode))
197            },
198        }
199    }
200
201    /// Grinds an account seed until its hash matches the given `account_type`, `storage_mode` and
202    /// `version` and returns it as a [`Word`]. The input to the hash function next to the seed are
203    /// the `code_commitment` and `storage_commitment`.
204    ///
205    /// The grinding process is started from the given `init_seed` which should be a random seed
206    /// generated from a cryptographically secure source.
207    pub fn compute_account_seed(
208        init_seed: [u8; 32],
209        account_type: AccountType,
210        storage_mode: AccountStorageMode,
211        version: AccountIdVersion,
212        code_commitment: Word,
213        storage_commitment: Word,
214    ) -> Result<Word, AccountError> {
215        match version {
216            AccountIdVersion::Version0 => AccountIdV0::compute_account_seed(
217                init_seed,
218                account_type,
219                storage_mode,
220                version,
221                code_commitment,
222                storage_commitment,
223            ),
224        }
225    }
226
227    // PUBLIC ACCESSORS
228    // --------------------------------------------------------------------------------------------
229
230    /// Returns the type of this account ID.
231    pub fn account_type(&self) -> AccountType {
232        match self {
233            AccountId::V0(account_id) => account_id.account_type(),
234        }
235    }
236
237    /// Returns `true` if an account with this ID is a faucet which can issue assets.
238    pub fn is_faucet(&self) -> bool {
239        self.account_type().is_faucet()
240    }
241
242    /// Returns `true` if an account with this ID is a regular account.
243    pub fn is_regular_account(&self) -> bool {
244        self.account_type().is_regular_account()
245    }
246
247    /// Returns the storage mode of this account ID.
248    pub fn storage_mode(&self) -> AccountStorageMode {
249        match self {
250            AccountId::V0(account_id) => account_id.storage_mode(),
251        }
252    }
253
254    /// Returns `true` if the full state of the account is public on chain, i.e. if the modes are
255    /// [`AccountStorageMode::Public`] or [`AccountStorageMode::Network`], `false` otherwise.
256    pub fn has_public_state(&self) -> bool {
257        self.storage_mode().has_public_state()
258    }
259
260    /// Returns `true` if the storage mode is [`AccountStorageMode::Public`], `false` otherwise.
261    pub fn is_public(&self) -> bool {
262        self.storage_mode().is_public()
263    }
264
265    /// Returns `true` if the storage mode is [`AccountStorageMode::Network`], `false` otherwise.
266    pub fn is_network(&self) -> bool {
267        self.storage_mode().is_network()
268    }
269
270    /// Returns `true` if the storage mode is [`AccountStorageMode::Private`], `false` otherwise.
271    pub fn is_private(&self) -> bool {
272        self.storage_mode().is_private()
273    }
274
275    /// Returns the version of this account ID.
276    pub fn version(&self) -> AccountIdVersion {
277        match self {
278            AccountId::V0(_) => AccountIdVersion::Version0,
279        }
280    }
281
282    /// Creates an [`AccountId`] from a hex string. Assumes the string starts with "0x" and
283    /// that the hexadecimal characters are big-endian encoded.
284    pub fn from_hex(hex_str: &str) -> Result<Self, AccountIdError> {
285        hex_to_bytes(hex_str)
286            .map_err(AccountIdError::AccountIdHexParseError)
287            .and_then(AccountId::try_from)
288    }
289
290    /// Returns a big-endian, hex-encoded string of length 32, including the `0x` prefix. This means
291    /// it encodes 15 bytes.
292    pub fn to_hex(self) -> String {
293        match self {
294            AccountId::V0(account_id) => account_id.to_hex(),
295        }
296    }
297
298    /// Encodes the [`AccountId`] into a [bech32](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki)
299    /// string.
300    ///
301    /// # Encoding
302    ///
303    /// The encoding of an account ID into bech32 is done as follows:
304    /// - Convert the account ID into its `[u8; 15]` data format.
305    /// - Insert the address type `AddressType::AccountId` byte at index 0, shifting all other
306    ///   elements to the right.
307    /// - Choose an HRP, defined as a [`NetworkId`], for example [`NetworkId::Mainnet`] whose string
308    ///   representation is `mm`.
309    /// - Encode the resulting HRP together with the data into a bech32 string using the
310    ///   [`bech32::Bech32m`] checksum algorithm.
311    ///
312    /// This is an example of an account ID in hex and bech32 representations:
313    ///
314    /// ```text
315    /// hex:    0x6d449e4034fadca075d1976fef7e38
316    /// bech32: mm1apk5f8jqxnadegr46xtklmm78qhdgkwc
317    /// ```
318    ///
319    /// ## Rationale
320    ///
321    /// Having the address type at the very beginning is so that it can be decoded to detect the
322    /// type of the address without having to decode the entire data. Moreover, choosing the
323    /// address type as a multiple of 8 means the first character of the bech32 string after the
324    /// `1` separator will be different for every address type. This makes the type of the address
325    /// conveniently human-readable.
326    pub fn to_bech32(&self, network_id: NetworkId) -> String {
327        match self {
328            AccountId::V0(account_id_v0) => account_id_v0.to_bech32(network_id),
329        }
330    }
331
332    /// Decodes a [bech32](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) string into an [`AccountId`].
333    ///
334    /// See [`AccountId::to_bech32`] for details on the format. The procedure for decoding the
335    /// bech32 data into the ID consists of the inverse operations of encoding.
336    pub fn from_bech32(bech32_string: &str) -> Result<(NetworkId, Self), AccountIdError> {
337        AccountIdV0::from_bech32(bech32_string)
338            .map(|(network_id, account_id)| (network_id, AccountId::V0(account_id)))
339    }
340
341    /// Parses a string into an [`AccountId`].
342    ///
343    /// This function supports parsing from both hex (`0x...`) and bech32 formats.
344    ///
345    /// # Returns
346    ///
347    /// Returns a tuple of the parsed [`AccountId`] and an optional [`NetworkId`].
348    /// - For hex strings: `NetworkId` is `None`.
349    /// - For bech32 strings: `NetworkId` is `Some(...)`.
350    ///
351    /// # Errors
352    ///
353    /// Returns an error if the string cannot be parsed as either hex or bech32 format.
354    pub fn parse(s: &str) -> Result<(Self, Option<NetworkId>), AccountIdError> {
355        if let Some(hex_digits) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
356            // Normalize to lowercase "0x" prefix for from_hex
357            let normalized = format!("0x{hex_digits}");
358            Self::from_hex(&normalized).map(|id| (id, None))
359        } else {
360            Self::from_bech32(s).map(|(network_id, id)| (id, Some(network_id)))
361        }
362    }
363
364    /// Decodes the data from the bech32 byte iterator into an [`AccountId`].
365    pub(crate) fn from_bech32_byte_iter(byte_iter: ByteIter<'_>) -> Result<Self, AccountIdError> {
366        AccountIdV0::from_bech32_byte_iter(byte_iter).map(AccountId::V0)
367    }
368
369    /// Returns the [`AccountIdPrefix`] of this ID.
370    ///
371    /// The prefix of an account ID is guaranteed to be unique.
372    pub fn prefix(&self) -> AccountIdPrefix {
373        match self {
374            AccountId::V0(account_id) => AccountIdPrefix::V0(account_id.prefix()),
375        }
376    }
377
378    /// Returns the suffix of this ID as a [`Felt`].
379    pub const fn suffix(&self) -> Felt {
380        match self {
381            AccountId::V0(account_id) => account_id.suffix(),
382        }
383    }
384}
385
386// CONVERSIONS FROM ACCOUNT ID
387// ================================================================================================
388
389impl From<AccountId> for [Felt; 2] {
390    fn from(id: AccountId) -> Self {
391        match id {
392            AccountId::V0(account_id) => account_id.into(),
393        }
394    }
395}
396
397impl From<AccountId> for [u8; 15] {
398    fn from(id: AccountId) -> Self {
399        match id {
400            AccountId::V0(account_id) => account_id.into(),
401        }
402    }
403}
404
405impl From<AccountId> for u128 {
406    fn from(id: AccountId) -> Self {
407        match id {
408            AccountId::V0(account_id) => account_id.into(),
409        }
410    }
411}
412
413// CONVERSIONS TO ACCOUNT ID
414// ================================================================================================
415
416impl From<AccountIdV0> for AccountId {
417    fn from(id: AccountIdV0) -> Self {
418        Self::V0(id)
419    }
420}
421
422impl TryFrom<[u8; 15]> for AccountId {
423    type Error = AccountIdError;
424
425    /// Tries to convert a byte array in big-endian order to an [`AccountId`].
426    ///
427    /// # Errors
428    ///
429    /// Returns an error if any of the ID constraints are not met. See the [constraints
430    /// documentation](AccountId#constraints) for details.
431    fn try_from(bytes: [u8; 15]) -> Result<Self, Self::Error> {
432        // The least significant byte of the ID prefix contains the metadata.
433        let metadata_byte = bytes[7];
434        // We only have one supported version for now, so we use the extractor from that version.
435        // If we add more versions in the future, we may need to generalize this.
436        let version = v0::extract_version(metadata_byte as u64)?;
437
438        match version {
439            AccountIdVersion::Version0 => AccountIdV0::try_from(bytes).map(Self::V0),
440        }
441    }
442}
443
444impl TryFrom<u128> for AccountId {
445    type Error = AccountIdError;
446
447    /// Tries to convert a u128 into an [`AccountId`].
448    ///
449    /// # Errors
450    ///
451    /// Returns an error if any of the ID constraints are not met. See the [constraints
452    /// documentation](AccountId#constraints) for details.
453    fn try_from(int: u128) -> Result<Self, Self::Error> {
454        let mut bytes: [u8; 15] = [0; 15];
455        bytes.copy_from_slice(&int.to_be_bytes()[0..15]);
456
457        Self::try_from(bytes)
458    }
459}
460
461// COMMON TRAIT IMPLS
462// ================================================================================================
463
464impl PartialOrd for AccountId {
465    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
466        Some(self.cmp(other))
467    }
468}
469
470impl Ord for AccountId {
471    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
472        u128::from(*self).cmp(&u128::from(*other))
473    }
474}
475
476impl fmt::Display for AccountId {
477    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
478        write!(f, "{}", self.to_hex())
479    }
480}
481
482// SERIALIZATION
483// ================================================================================================
484
485impl Serializable for AccountId {
486    fn write_into<W: ByteWriter>(&self, target: &mut W) {
487        match self {
488            AccountId::V0(account_id) => {
489                account_id.write_into(target);
490            },
491        }
492    }
493
494    fn get_size_hint(&self) -> usize {
495        match self {
496            AccountId::V0(account_id) => account_id.get_size_hint(),
497        }
498    }
499}
500
501impl Deserializable for AccountId {
502    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
503        <[u8; 15]>::read_from(source)?
504            .try_into()
505            .map_err(|err: AccountIdError| DeserializationError::InvalidValue(err.to_string()))
506    }
507}
508
509// TESTS
510// ================================================================================================
511
512#[cfg(test)]
513mod tests {
514    use alloc::boxed::Box;
515
516    use assert_matches::assert_matches;
517    use bech32::{Bech32, Bech32m, NoChecksum};
518
519    use super::*;
520    use crate::account::account_id::v0::{extract_storage_mode, extract_type, extract_version};
521    use crate::address::{AddressType, CustomNetworkId};
522    use crate::errors::Bech32Error;
523    use crate::testing::account_id::{
524        ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET,
525        ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
526        ACCOUNT_ID_PRIVATE_SENDER,
527        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
528        ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
529        ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
530        AccountIdBuilder,
531    };
532
533    #[test]
534    fn test_account_id_wrapper_conversion_roundtrip() {
535        for (idx, account_id) in [
536            ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
537            ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
538            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
539            ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
540            ACCOUNT_ID_PRIVATE_SENDER,
541            ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET,
542        ]
543        .into_iter()
544        .enumerate()
545        {
546            let wrapper = AccountId::try_from(account_id).unwrap();
547            assert_eq!(
548                wrapper,
549                AccountId::read_from_bytes(&wrapper.to_bytes()).unwrap(),
550                "failed in {idx}"
551            );
552        }
553    }
554
555    #[test]
556    fn bech32_encode_decode_roundtrip() -> anyhow::Result<()> {
557        // We use this to check that encoding does not panic even when using the longest possible
558        // HRP.
559        let longest_possible_hrp =
560            "01234567890123456789012345678901234567890123456789012345678901234567890123456789012";
561        assert_eq!(longest_possible_hrp.len(), 83);
562
563        let random_id = AccountIdBuilder::new().build_with_rng(&mut rand::rng());
564
565        for network_id in [
566            NetworkId::Mainnet,
567            NetworkId::Custom(Box::new("custom".parse::<CustomNetworkId>()?)),
568            NetworkId::Custom(Box::new(longest_possible_hrp.parse::<CustomNetworkId>()?)),
569        ] {
570            for account_id in [
571                ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
572                ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
573                ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
574                ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
575                ACCOUNT_ID_PRIVATE_SENDER,
576                random_id.into(),
577            ]
578            .into_iter()
579            {
580                let account_id = AccountId::try_from(account_id).unwrap();
581
582                let bech32_string = account_id.to_bech32(network_id.clone());
583                let (decoded_network_id, decoded_account_id) =
584                    AccountId::from_bech32(&bech32_string).unwrap();
585
586                assert_eq!(network_id, decoded_network_id, "network id failed for {account_id}",);
587                assert_eq!(account_id, decoded_account_id, "account id failed for {account_id}");
588
589                let (_, data) = bech32::decode(&bech32_string).unwrap();
590
591                // Raw bech32 data should contain the address type as the first byte.
592                assert_eq!(data[0], AddressType::AccountId as u8);
593
594                // Raw bech32 data should contain the metadata byte at index 8.
595                assert_eq!(extract_version(data[8] as u64).unwrap(), account_id.version());
596                assert_eq!(extract_type(data[8] as u64), account_id.account_type());
597                assert_eq!(
598                    extract_storage_mode(data[8] as u64).unwrap(),
599                    account_id.storage_mode()
600                );
601            }
602        }
603
604        Ok(())
605    }
606
607    #[test]
608    fn bech32_invalid_checksum() {
609        let network_id = NetworkId::Mainnet;
610        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
611
612        let bech32_string = account_id.to_bech32(network_id);
613        let mut invalid_bech32_1 = bech32_string.clone();
614        invalid_bech32_1.remove(0);
615        let mut invalid_bech32_2 = bech32_string.clone();
616        invalid_bech32_2.remove(7);
617
618        let error = AccountId::from_bech32(&invalid_bech32_1).unwrap_err();
619        assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
620
621        let error = AccountId::from_bech32(&invalid_bech32_2).unwrap_err();
622        assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
623    }
624
625    #[test]
626    fn bech32_invalid_address_type() {
627        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
628        let mut id_bytes = account_id.to_bytes();
629
630        // Set invalid address type.
631        id_bytes.insert(0, 16);
632
633        let invalid_bech32 =
634            bech32::encode::<Bech32m>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
635
636        let error = AccountId::from_bech32(&invalid_bech32).unwrap_err();
637        assert_matches!(
638            error,
639            AccountIdError::Bech32DecodeError(Bech32Error::UnknownAddressType(16))
640        );
641    }
642
643    #[test]
644    fn bech32_invalid_other_checksum() {
645        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
646        let mut id_bytes = account_id.to_bytes();
647        id_bytes.insert(0, AddressType::AccountId as u8);
648
649        // Use Bech32 instead of Bech32m which is disallowed.
650        let invalid_bech32_regular =
651            bech32::encode::<Bech32>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
652        let error = AccountId::from_bech32(&invalid_bech32_regular).unwrap_err();
653        assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
654
655        // Use no checksum instead of Bech32m which is disallowed.
656        let invalid_bech32_no_checksum =
657            bech32::encode::<NoChecksum>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
658        let error = AccountId::from_bech32(&invalid_bech32_no_checksum).unwrap_err();
659        assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
660    }
661
662    #[test]
663    fn bech32_invalid_length() {
664        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
665        let mut id_bytes = account_id.to_bytes();
666        id_bytes.insert(0, AddressType::AccountId as u8);
667        // Add one byte to make the length invalid.
668        id_bytes.push(5);
669
670        let invalid_bech32 =
671            bech32::encode::<Bech32m>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
672
673        let error = AccountId::from_bech32(&invalid_bech32).unwrap_err();
674        assert_matches!(
675            error,
676            AccountIdError::Bech32DecodeError(Bech32Error::InvalidDataLength { .. })
677        );
678    }
679
680    #[test]
681    fn parse_hex_string() {
682        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
683        let hex_string = account_id.to_hex();
684
685        let (parsed_id, network_id) = AccountId::parse(&hex_string).unwrap();
686
687        assert_eq!(parsed_id, account_id);
688        assert!(network_id.is_none());
689    }
690
691    #[test]
692    fn parse_hex_string_uppercase() {
693        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
694        // Keep "0x" prefix lowercase, only uppercase the hex digits
695        let hex_string = account_id.to_hex();
696        let hex_string = format!("0x{}", hex_string[2..].to_uppercase());
697
698        let (parsed_id, network_id) = AccountId::parse(&hex_string).unwrap();
699
700        assert_eq!(parsed_id, account_id);
701        assert!(network_id.is_none());
702    }
703
704    #[test]
705    fn parse_hex_string_uppercase_prefix() {
706        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
707        // Use "0X" prefix (uppercase X) with uppercase hex digits
708        let hex_string = account_id.to_hex();
709        let hex_string = format!("0X{}", hex_string[2..].to_uppercase());
710
711        let (parsed_id, network_id) = AccountId::parse(&hex_string).unwrap();
712
713        assert_eq!(parsed_id, account_id);
714        assert!(network_id.is_none());
715    }
716
717    #[test]
718    fn parse_bech32_string() {
719        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
720        let bech32_string = account_id.to_bech32(NetworkId::Mainnet);
721
722        let (parsed_id, parsed_network_id) = AccountId::parse(&bech32_string).unwrap();
723
724        assert_eq!(parsed_id, account_id);
725        assert_eq!(parsed_network_id, Some(NetworkId::Mainnet));
726    }
727
728    #[test]
729    fn parse_invalid_string() {
730        let error = AccountId::parse("invalid_string").unwrap_err();
731        assert_matches!(error, AccountIdError::Bech32DecodeError(_));
732
733        let error = AccountId::parse("0xinvalid").unwrap_err();
734        assert_matches!(error, AccountIdError::AccountIdHexParseError(_));
735    }
736}