miden_objects/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::address::NetworkId;
27use crate::errors::AccountIdError;
28use crate::{AccountError, Word};
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 /// Decodes the data from the bech32 byte iterator into an [`AccountId`].
321 pub(crate) fn from_bech32_byte_iter(byte_iter: ByteIter<'_>) -> Result<Self, AccountIdError> {
322 AccountIdV0::from_bech32_byte_iter(byte_iter).map(AccountId::V0)
323 }
324
325 /// Returns the [`AccountIdPrefix`] of this ID.
326 ///
327 /// The prefix of an account ID is guaranteed to be unique.
328 pub fn prefix(&self) -> AccountIdPrefix {
329 match self {
330 AccountId::V0(account_id) => AccountIdPrefix::V0(account_id.prefix()),
331 }
332 }
333
334 /// Returns the suffix of this ID as a [`Felt`].
335 pub const fn suffix(&self) -> Felt {
336 match self {
337 AccountId::V0(account_id) => account_id.suffix(),
338 }
339 }
340}
341
342// CONVERSIONS FROM ACCOUNT ID
343// ================================================================================================
344
345impl From<AccountId> for [Felt; 2] {
346 fn from(id: AccountId) -> Self {
347 match id {
348 AccountId::V0(account_id) => account_id.into(),
349 }
350 }
351}
352
353impl From<AccountId> for [u8; 15] {
354 fn from(id: AccountId) -> Self {
355 match id {
356 AccountId::V0(account_id) => account_id.into(),
357 }
358 }
359}
360
361impl From<AccountId> for u128 {
362 fn from(id: AccountId) -> Self {
363 match id {
364 AccountId::V0(account_id) => account_id.into(),
365 }
366 }
367}
368
369// CONVERSIONS TO ACCOUNT ID
370// ================================================================================================
371
372impl From<AccountIdV0> for AccountId {
373 fn from(id: AccountIdV0) -> Self {
374 Self::V0(id)
375 }
376}
377
378impl TryFrom<[Felt; 2]> for AccountId {
379 type Error = AccountIdError;
380
381 /// Returns an [`AccountId`] instantiated with the provided field elements where `elements[0]`
382 /// is taken as the prefix and `elements[1]` is taken as the suffix.
383 ///
384 /// # Errors
385 ///
386 /// Returns an error if any of the ID constraints are not met. See the [constraints
387 /// documentation](AccountId#constraints) for details.
388 fn try_from(elements: [Felt; 2]) -> Result<Self, Self::Error> {
389 // The prefix contains the metadata.
390 // If we add more versions in the future, we may need to generalize this.
391 match v0::extract_version(elements[0].as_int())? {
392 AccountIdVersion::Version0 => AccountIdV0::try_from(elements).map(Self::V0),
393 }
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 = v0::extract_version(metadata_byte as u64)?;
412
413 match version {
414 AccountIdVersion::Version0 => AccountIdV0::try_from(bytes).map(Self::V0),
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: miden_core::utils::ByteWriter>(&self, target: &mut W) {
462 match self {
463 AccountId::V0(account_id) => {
464 account_id.write_into(target);
465 },
466 }
467 }
468
469 fn get_size_hint(&self) -> usize {
470 match self {
471 AccountId::V0(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::v0::{extract_storage_mode, extract_type, extract_version};
496 use crate::address::{AddressType, CustomNetworkId};
497 use crate::errors::Bech32Error;
498 use crate::testing::account_id::{
499 ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET,
500 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
501 ACCOUNT_ID_PRIVATE_SENDER,
502 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
503 ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
504 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
505 AccountIdBuilder,
506 };
507
508 #[test]
509 fn test_account_id_wrapper_conversion_roundtrip() {
510 for (idx, account_id) in [
511 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
512 ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
513 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
514 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
515 ACCOUNT_ID_PRIVATE_SENDER,
516 ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET,
517 ]
518 .into_iter()
519 .enumerate()
520 {
521 let wrapper = AccountId::try_from(account_id).unwrap();
522 assert_eq!(
523 wrapper,
524 AccountId::read_from_bytes(&wrapper.to_bytes()).unwrap(),
525 "failed in {idx}"
526 );
527 }
528 }
529
530 #[test]
531 fn bech32_encode_decode_roundtrip() -> anyhow::Result<()> {
532 // We use this to check that encoding does not panic even when using the longest possible
533 // HRP.
534 let longest_possible_hrp =
535 "01234567890123456789012345678901234567890123456789012345678901234567890123456789012";
536 assert_eq!(longest_possible_hrp.len(), 83);
537
538 let random_id = AccountIdBuilder::new().build_with_rng(&mut rand::rng());
539
540 for network_id in [
541 NetworkId::Mainnet,
542 NetworkId::Custom(Box::new("custom".parse::<CustomNetworkId>()?)),
543 NetworkId::Custom(Box::new(longest_possible_hrp.parse::<CustomNetworkId>()?)),
544 ] {
545 for account_id in [
546 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
547 ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
548 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
549 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
550 ACCOUNT_ID_PRIVATE_SENDER,
551 random_id.into(),
552 ]
553 .into_iter()
554 {
555 let account_id = AccountId::try_from(account_id).unwrap();
556
557 let bech32_string = account_id.to_bech32(network_id.clone());
558 let (decoded_network_id, decoded_account_id) =
559 AccountId::from_bech32(&bech32_string).unwrap();
560
561 assert_eq!(network_id, decoded_network_id, "network id failed for {account_id}",);
562 assert_eq!(account_id, decoded_account_id, "account id failed for {account_id}");
563
564 let (_, data) = bech32::decode(&bech32_string).unwrap();
565
566 // Raw bech32 data should contain the address type as the first byte.
567 assert_eq!(data[0], AddressType::AccountId as u8);
568
569 // Raw bech32 data should contain the metadata byte at index 8.
570 assert_eq!(extract_version(data[8] as u64).unwrap(), account_id.version());
571 assert_eq!(extract_type(data[8] as u64), account_id.account_type());
572 assert_eq!(
573 extract_storage_mode(data[8] as u64).unwrap(),
574 account_id.storage_mode()
575 );
576 }
577 }
578
579 Ok(())
580 }
581
582 #[test]
583 fn bech32_invalid_checksum() {
584 let network_id = NetworkId::Mainnet;
585 let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
586
587 let bech32_string = account_id.to_bech32(network_id);
588 let mut invalid_bech32_1 = bech32_string.clone();
589 invalid_bech32_1.remove(0);
590 let mut invalid_bech32_2 = bech32_string.clone();
591 invalid_bech32_2.remove(7);
592
593 let error = AccountId::from_bech32(&invalid_bech32_1).unwrap_err();
594 assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
595
596 let error = AccountId::from_bech32(&invalid_bech32_2).unwrap_err();
597 assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
598 }
599
600 #[test]
601 fn bech32_invalid_address_type() {
602 let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
603 let mut id_bytes = account_id.to_bytes();
604
605 // Set invalid address type.
606 id_bytes.insert(0, 16);
607
608 let invalid_bech32 =
609 bech32::encode::<Bech32m>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
610
611 let error = AccountId::from_bech32(&invalid_bech32).unwrap_err();
612 assert_matches!(
613 error,
614 AccountIdError::Bech32DecodeError(Bech32Error::UnknownAddressType(16))
615 );
616 }
617
618 #[test]
619 fn bech32_invalid_other_checksum() {
620 let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
621 let mut id_bytes = account_id.to_bytes();
622 id_bytes.insert(0, AddressType::AccountId as u8);
623
624 // Use Bech32 instead of Bech32m which is disallowed.
625 let invalid_bech32_regular =
626 bech32::encode::<Bech32>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
627 let error = AccountId::from_bech32(&invalid_bech32_regular).unwrap_err();
628 assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
629
630 // Use no checksum instead of Bech32m which is disallowed.
631 let invalid_bech32_no_checksum =
632 bech32::encode::<NoChecksum>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
633 let error = AccountId::from_bech32(&invalid_bech32_no_checksum).unwrap_err();
634 assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
635 }
636
637 #[test]
638 fn bech32_invalid_length() {
639 let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
640 let mut id_bytes = account_id.to_bytes();
641 id_bytes.insert(0, AddressType::AccountId as u8);
642 // Add one byte to make the length invalid.
643 id_bytes.push(5);
644
645 let invalid_bech32 =
646 bech32::encode::<Bech32m>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
647
648 let error = AccountId::from_bech32(&invalid_bech32).unwrap_err();
649 assert_matches!(
650 error,
651 AccountIdError::Bech32DecodeError(Bech32Error::InvalidDataLength { .. })
652 );
653 }
654}