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