pub(crate) mod v0;
pub use v0::{AccountIdPrefixV0, AccountIdV0};
mod id_prefix;
pub use id_prefix::AccountIdPrefix;
mod seed;
mod account_type;
pub use account_type::AccountType;
mod storage_mode;
pub use storage_mode::AccountStorageMode;
mod id_version;
use alloc::string::{String, ToString};
use core::fmt;
use bech32::primitives::decode::ByteIter;
pub use id_version::AccountIdVersion;
use miden_core::Felt;
use miden_core::utils::{ByteReader, Deserializable, Serializable};
use miden_crypto::utils::hex_to_bytes;
use miden_processor::DeserializationError;
use crate::address::NetworkId;
use crate::errors::AccountIdError;
use crate::{AccountError, Word};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AccountId {
V0(AccountIdV0),
}
impl AccountId {
pub const SERIALIZED_SIZE: usize = 15;
pub fn new(
seed: Word,
version: AccountIdVersion,
code_commitment: Word,
storage_commitment: Word,
) -> Result<Self, AccountIdError> {
match version {
AccountIdVersion::Version0 => {
AccountIdV0::new(seed, code_commitment, storage_commitment).map(Self::V0)
},
}
}
pub fn new_unchecked(elements: [Felt; 2]) -> Self {
match v0::extract_version(elements[0].as_int())
.expect("prefix should contain a valid account ID version")
{
AccountIdVersion::Version0 => Self::V0(AccountIdV0::new_unchecked(elements)),
}
}
#[cfg(any(feature = "testing", test))]
pub fn dummy(
bytes: [u8; 15],
version: AccountIdVersion,
account_type: AccountType,
storage_mode: AccountStorageMode,
) -> AccountId {
match version {
AccountIdVersion::Version0 => {
Self::V0(AccountIdV0::dummy(bytes, account_type, storage_mode))
},
}
}
pub fn compute_account_seed(
init_seed: [u8; 32],
account_type: AccountType,
storage_mode: AccountStorageMode,
version: AccountIdVersion,
code_commitment: Word,
storage_commitment: Word,
) -> Result<Word, AccountError> {
match version {
AccountIdVersion::Version0 => AccountIdV0::compute_account_seed(
init_seed,
account_type,
storage_mode,
version,
code_commitment,
storage_commitment,
),
}
}
pub const fn account_type(&self) -> AccountType {
match self {
AccountId::V0(account_id) => account_id.account_type(),
}
}
pub fn is_faucet(&self) -> bool {
self.account_type().is_faucet()
}
pub fn is_regular_account(&self) -> bool {
self.account_type().is_regular_account()
}
pub fn storage_mode(&self) -> AccountStorageMode {
match self {
AccountId::V0(account_id) => account_id.storage_mode(),
}
}
pub fn has_public_state(&self) -> bool {
self.storage_mode().has_public_state()
}
pub fn is_public(&self) -> bool {
self.storage_mode().is_public()
}
pub fn is_network(&self) -> bool {
self.storage_mode().is_network()
}
pub fn is_private(&self) -> bool {
self.storage_mode().is_private()
}
pub fn version(&self) -> AccountIdVersion {
match self {
AccountId::V0(_) => AccountIdVersion::Version0,
}
}
pub fn from_hex(hex_str: &str) -> Result<Self, AccountIdError> {
hex_to_bytes(hex_str)
.map_err(AccountIdError::AccountIdHexParseError)
.and_then(AccountId::try_from)
}
pub fn to_hex(self) -> String {
match self {
AccountId::V0(account_id) => account_id.to_hex(),
}
}
pub fn to_bech32(&self, network_id: NetworkId) -> String {
match self {
AccountId::V0(account_id_v0) => account_id_v0.to_bech32(network_id),
}
}
pub fn from_bech32(bech32_string: &str) -> Result<(NetworkId, Self), AccountIdError> {
AccountIdV0::from_bech32(bech32_string)
.map(|(network_id, account_id)| (network_id, AccountId::V0(account_id)))
}
pub(crate) fn from_bech32_byte_iter(byte_iter: ByteIter<'_>) -> Result<Self, AccountIdError> {
AccountIdV0::from_bech32_byte_iter(byte_iter).map(AccountId::V0)
}
pub fn prefix(&self) -> AccountIdPrefix {
match self {
AccountId::V0(account_id) => AccountIdPrefix::V0(account_id.prefix()),
}
}
pub const fn suffix(&self) -> Felt {
match self {
AccountId::V0(account_id) => account_id.suffix(),
}
}
}
impl From<AccountId> for [Felt; 2] {
fn from(id: AccountId) -> Self {
match id {
AccountId::V0(account_id) => account_id.into(),
}
}
}
impl From<AccountId> for [u8; 15] {
fn from(id: AccountId) -> Self {
match id {
AccountId::V0(account_id) => account_id.into(),
}
}
}
impl From<AccountId> for u128 {
fn from(id: AccountId) -> Self {
match id {
AccountId::V0(account_id) => account_id.into(),
}
}
}
impl From<AccountIdV0> for AccountId {
fn from(id: AccountIdV0) -> Self {
Self::V0(id)
}
}
impl TryFrom<[Felt; 2]> for AccountId {
type Error = AccountIdError;
fn try_from(elements: [Felt; 2]) -> Result<Self, Self::Error> {
match v0::extract_version(elements[0].as_int())? {
AccountIdVersion::Version0 => AccountIdV0::try_from(elements).map(Self::V0),
}
}
}
impl TryFrom<[u8; 15]> for AccountId {
type Error = AccountIdError;
fn try_from(bytes: [u8; 15]) -> Result<Self, Self::Error> {
let metadata_byte = bytes[7];
let version = v0::extract_version(metadata_byte as u64)?;
match version {
AccountIdVersion::Version0 => AccountIdV0::try_from(bytes).map(Self::V0),
}
}
}
impl TryFrom<u128> for AccountId {
type Error = AccountIdError;
fn try_from(int: u128) -> Result<Self, Self::Error> {
let mut bytes: [u8; 15] = [0; 15];
bytes.copy_from_slice(&int.to_be_bytes()[0..15]);
Self::try_from(bytes)
}
}
impl PartialOrd for AccountId {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for AccountId {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
u128::from(*self).cmp(&u128::from(*other))
}
}
impl fmt::Display for AccountId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_hex())
}
}
impl Serializable for AccountId {
fn write_into<W: miden_core::utils::ByteWriter>(&self, target: &mut W) {
match self {
AccountId::V0(account_id) => {
account_id.write_into(target);
},
}
}
fn get_size_hint(&self) -> usize {
match self {
AccountId::V0(account_id) => account_id.get_size_hint(),
}
}
}
impl Deserializable for AccountId {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
<[u8; 15]>::read_from(source)?
.try_into()
.map_err(|err: AccountIdError| DeserializationError::InvalidValue(err.to_string()))
}
}
#[cfg(test)]
mod tests {
use alloc::boxed::Box;
use assert_matches::assert_matches;
use bech32::{Bech32, Bech32m, NoChecksum};
use super::*;
use crate::account::account_id::v0::{extract_storage_mode, extract_type, extract_version};
use crate::address::{AddressType, CustomNetworkId};
use crate::errors::Bech32Error;
use crate::testing::account_id::{
ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET,
ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
ACCOUNT_ID_PRIVATE_SENDER,
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
AccountIdBuilder,
};
#[test]
fn test_account_id_wrapper_conversion_roundtrip() {
for (idx, account_id) in [
ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
ACCOUNT_ID_PRIVATE_SENDER,
ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET,
]
.into_iter()
.enumerate()
{
let wrapper = AccountId::try_from(account_id).unwrap();
assert_eq!(
wrapper,
AccountId::read_from_bytes(&wrapper.to_bytes()).unwrap(),
"failed in {idx}"
);
}
}
#[test]
fn bech32_encode_decode_roundtrip() -> anyhow::Result<()> {
let longest_possible_hrp =
"01234567890123456789012345678901234567890123456789012345678901234567890123456789012";
assert_eq!(longest_possible_hrp.len(), 83);
let random_id = AccountIdBuilder::new().build_with_rng(&mut rand::rng());
for network_id in [
NetworkId::Mainnet,
NetworkId::Custom(Box::new("custom".parse::<CustomNetworkId>()?)),
NetworkId::Custom(Box::new(longest_possible_hrp.parse::<CustomNetworkId>()?)),
] {
for account_id in [
ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
ACCOUNT_ID_PRIVATE_SENDER,
random_id.into(),
]
.into_iter()
{
let account_id = AccountId::try_from(account_id).unwrap();
let bech32_string = account_id.to_bech32(network_id.clone());
let (decoded_network_id, decoded_account_id) =
AccountId::from_bech32(&bech32_string).unwrap();
assert_eq!(network_id, decoded_network_id, "network id failed for {account_id}",);
assert_eq!(account_id, decoded_account_id, "account id failed for {account_id}");
let (_, data) = bech32::decode(&bech32_string).unwrap();
assert_eq!(data[0], AddressType::AccountId as u8);
assert_eq!(extract_version(data[8] as u64).unwrap(), account_id.version());
assert_eq!(extract_type(data[8] as u64), account_id.account_type());
assert_eq!(
extract_storage_mode(data[8] as u64).unwrap(),
account_id.storage_mode()
);
}
}
Ok(())
}
#[test]
fn bech32_invalid_checksum() {
let network_id = NetworkId::Mainnet;
let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
let bech32_string = account_id.to_bech32(network_id);
let mut invalid_bech32_1 = bech32_string.clone();
invalid_bech32_1.remove(0);
let mut invalid_bech32_2 = bech32_string.clone();
invalid_bech32_2.remove(7);
let error = AccountId::from_bech32(&invalid_bech32_1).unwrap_err();
assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
let error = AccountId::from_bech32(&invalid_bech32_2).unwrap_err();
assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
}
#[test]
fn bech32_invalid_address_type() {
let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
let mut id_bytes = account_id.to_bytes();
id_bytes.insert(0, 16);
let invalid_bech32 =
bech32::encode::<Bech32m>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
let error = AccountId::from_bech32(&invalid_bech32).unwrap_err();
assert_matches!(
error,
AccountIdError::Bech32DecodeError(Bech32Error::UnknownAddressType(16))
);
}
#[test]
fn bech32_invalid_other_checksum() {
let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
let mut id_bytes = account_id.to_bytes();
id_bytes.insert(0, AddressType::AccountId as u8);
let invalid_bech32_regular =
bech32::encode::<Bech32>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
let error = AccountId::from_bech32(&invalid_bech32_regular).unwrap_err();
assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
let invalid_bech32_no_checksum =
bech32::encode::<NoChecksum>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
let error = AccountId::from_bech32(&invalid_bech32_no_checksum).unwrap_err();
assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
}
#[test]
fn bech32_invalid_length() {
let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
let mut id_bytes = account_id.to_bytes();
id_bytes.insert(0, AddressType::AccountId as u8);
id_bytes.push(5);
let invalid_bech32 =
bech32::encode::<Bech32m>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
let error = AccountId::from_bech32(&invalid_bech32).unwrap_err();
assert_matches!(
error,
AccountIdError::Bech32DecodeError(Bech32Error::InvalidDataLength { .. })
);
}
}