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}