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