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