mod r#type;
use alloc::string::ToString;
pub use r#type::AddressType;
mod routing_parameters;
use alloc::borrow::ToOwned;
pub use routing_parameters::RoutingParameters;
mod interface;
mod network_id;
use alloc::string::String;
pub use interface::AddressInterface;
use miden_processor::DeserializationError;
pub use network_id::{CustomNetworkId, NetworkId};
use crate::AddressError;
use crate::account::AccountStorageMode;
use crate::crypto::ies::SealingKey;
use crate::note::NoteTag;
use crate::utils::serde::{ByteWriter, Deserializable, Serializable};
mod address_id;
pub use address_id::AddressId;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Address {
id: AddressId,
routing_params: Option<RoutingParameters>,
}
impl Address {
pub const SEPARATOR: char = '_';
pub fn new(id: impl Into<AddressId>) -> Self {
Self { id: id.into(), routing_params: None }
}
pub fn with_routing_parameters(
mut self,
routing_params: RoutingParameters,
) -> Result<Self, AddressError> {
if let Some(tag_len) = routing_params.note_tag_len() {
match self.id {
AddressId::AccountId(account_id) => {
if account_id.storage_mode() == AccountStorageMode::Network
&& tag_len != NoteTag::DEFAULT_NETWORK_TAG_LENGTH
{
return Err(AddressError::CustomTagLengthNotAllowedForNetworkAccounts(
tag_len,
));
}
},
}
}
self.routing_params = Some(routing_params);
Ok(self)
}
pub fn id(&self) -> AddressId {
self.id
}
pub fn interface(&self) -> Option<AddressInterface> {
self.routing_params.as_ref().map(RoutingParameters::interface)
}
pub fn note_tag_len(&self) -> u8 {
self.routing_params
.as_ref()
.and_then(RoutingParameters::note_tag_len)
.unwrap_or(self.id.default_note_tag_len())
}
pub fn to_note_tag(&self) -> NoteTag {
let note_tag_len = self.note_tag_len();
match self.id {
AddressId::AccountId(id) => {
match id.storage_mode() {
AccountStorageMode::Network => NoteTag::from_network_account_id(id),
AccountStorageMode::Private | AccountStorageMode::Public => {
NoteTag::from_local_account_id(id, note_tag_len)
.expect("address should validate that tag len does not exceed MAX_LOCAL_TAG_LENGTH bits")
}
}
},
}
}
pub fn encryption_key(&self) -> Option<&SealingKey> {
self.routing_params.as_ref().and_then(RoutingParameters::encryption_key)
}
pub fn encode(&self, network_id: NetworkId) -> String {
let mut encoded = match self.id {
AddressId::AccountId(id) => id.to_bech32(network_id),
};
if let Some(routing_params) = &self.routing_params {
encoded.push(Self::SEPARATOR);
encoded.push_str(&routing_params.encode_to_string());
}
encoded
}
pub fn decode(address_str: &str) -> Result<(NetworkId, Self), AddressError> {
if address_str.ends_with(Self::SEPARATOR) {
return Err(AddressError::TrailingSeparator);
}
let mut split = address_str.split(Self::SEPARATOR);
let encoded_identifier = split
.next()
.ok_or_else(|| AddressError::decode_error("identifier missing in address string"))?;
let (network_id, identifier) = AddressId::decode(encoded_identifier)?;
let mut address = Address::new(identifier);
if let Some(encoded_routing_params) = split.next() {
let routing_params = RoutingParameters::decode(encoded_routing_params.to_owned())?;
address = address.with_routing_parameters(routing_params)?;
}
Ok((network_id, address))
}
}
impl Serializable for Address {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
self.id.write_into(target);
self.routing_params.write_into(target);
}
}
impl Deserializable for Address {
fn read_from<R: miden_core::utils::ByteReader>(
source: &mut R,
) -> Result<Self, DeserializationError> {
let identifier: AddressId = source.read()?;
let routing_params: Option<RoutingParameters> = source.read()?;
let mut address = Self::new(identifier);
if let Some(routing_params) = routing_params {
address = address
.with_routing_parameters(routing_params)
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
}
Ok(address)
}
}
#[cfg(test)]
mod tests {
use alloc::boxed::Box;
use alloc::str::FromStr;
use assert_matches::assert_matches;
use bech32::{Bech32, Bech32m, NoChecksum};
use super::*;
use crate::AccountIdError;
use crate::account::{AccountId, AccountType};
use crate::address::CustomNetworkId;
use crate::errors::Bech32Error;
use crate::testing::account_id::{ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, AccountIdBuilder};
#[test]
fn address_encode_decode_roundtrip() -> anyhow::Result<()> {
let longest_possible_hrp =
"01234567890123456789012345678901234567890123456789012345678901234567890123456789012";
assert_eq!(longest_possible_hrp.len(), 83);
let rng = &mut rand::rng();
for network_id in [
NetworkId::Mainnet,
NetworkId::Custom(Box::new(CustomNetworkId::from_str("custom").unwrap())),
NetworkId::Custom(Box::new(CustomNetworkId::from_str(longest_possible_hrp).unwrap())),
] {
for (idx, account_id) in [
AccountIdBuilder::new()
.account_type(AccountType::FungibleFaucet)
.build_with_rng(rng),
AccountIdBuilder::new()
.account_type(AccountType::NonFungibleFaucet)
.build_with_rng(rng),
AccountIdBuilder::new()
.account_type(AccountType::RegularAccountImmutableCode)
.build_with_rng(rng),
AccountIdBuilder::new()
.account_type(AccountType::RegularAccountUpdatableCode)
.build_with_rng(rng),
]
.into_iter()
.enumerate()
{
let mut address = Address::new(account_id);
let bech32_string = address.encode(network_id.clone());
assert!(
!bech32_string.contains(Address::SEPARATOR),
"separator should not be present in address without routing params"
);
let (decoded_network_id, decoded_address) = Address::decode(&bech32_string)?;
assert_eq!(network_id, decoded_network_id, "network id failed in {idx}");
assert_eq!(address, decoded_address, "address failed in {idx}");
let AddressId::AccountId(decoded_account_id) = address.id();
assert_eq!(account_id, decoded_account_id);
address = address.with_routing_parameters(
RoutingParameters::new(AddressInterface::BasicWallet)
.with_note_tag_len(NoteTag::DEFAULT_NETWORK_TAG_LENGTH)?,
)?;
let bech32_string = address.encode(network_id.clone());
assert!(
bech32_string.contains(Address::SEPARATOR),
"separator should be present in address without routing params"
);
let (decoded_network_id, decoded_address) = Address::decode(&bech32_string)?;
assert_eq!(network_id, decoded_network_id, "network id failed in {idx}");
assert_eq!(address, decoded_address, "address failed in {idx}");
let AddressId::AccountId(decoded_account_id) = address.id();
assert_eq!(account_id, decoded_account_id);
}
}
Ok(())
}
#[test]
fn address_decoding_fails_on_trailing_separator() -> anyhow::Result<()> {
let id = AccountIdBuilder::new()
.account_type(AccountType::FungibleFaucet)
.build_with_rng(&mut rand::rng());
let address = Address::new(id);
let mut encoded_address = address.encode(NetworkId::Devnet);
encoded_address.push(Address::SEPARATOR);
let err = Address::decode(&encoded_address).unwrap_err();
assert_matches!(err, AddressError::TrailingSeparator);
Ok(())
}
#[test]
fn bech32_invalid_checksum() -> anyhow::Result<()> {
let network_id = NetworkId::Mainnet;
let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?;
let address = Address::new(account_id).with_routing_parameters(
RoutingParameters::new(AddressInterface::BasicWallet).with_note_tag_len(14)?,
)?;
let bech32_string = address.encode(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 = Address::decode(&invalid_bech32_1).unwrap_err();
assert_matches!(error, AddressError::Bech32DecodeError(Bech32Error::DecodeError(_)));
let error = Address::decode(&invalid_bech32_2).unwrap_err();
assert_matches!(error, AddressError::Bech32DecodeError(Bech32Error::DecodeError(_)));
Ok(())
}
#[test]
fn bech32_unknown_address_type() {
let invalid_bech32_address =
bech32::encode::<Bech32m>(NetworkId::Mainnet.into_hrp(), &[250]).unwrap();
let error = Address::decode(&invalid_bech32_address).unwrap_err();
assert_matches!(
error,
AddressError::Bech32DecodeError(Bech32Error::UnknownAddressType(250))
);
}
#[test]
fn bech32_invalid_other_checksum() {
let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
let address_id_bytes = AddressId::from(account_id).to_bytes();
let invalid_bech32_regular =
bech32::encode::<Bech32>(NetworkId::Mainnet.into_hrp(), &address_id_bytes).unwrap();
let error = Address::decode(&invalid_bech32_regular).unwrap_err();
assert_matches!(error, AddressError::Bech32DecodeError(Bech32Error::DecodeError(_)));
let invalid_bech32_no_checksum =
bech32::encode::<NoChecksum>(NetworkId::Mainnet.into_hrp(), &address_id_bytes).unwrap();
let error = Address::decode(&invalid_bech32_no_checksum).unwrap_err();
assert_matches!(error, AddressError::Bech32DecodeError(Bech32Error::DecodeError(_)));
}
#[test]
fn bech32_invalid_length() {
let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
let mut address_id_bytes = AddressId::from(account_id).to_bytes();
address_id_bytes.push(5);
let invalid_bech32 =
bech32::encode::<Bech32m>(NetworkId::Mainnet.into_hrp(), &address_id_bytes).unwrap();
let error = Address::decode(&invalid_bech32).unwrap_err();
assert_matches!(
error,
AddressError::AccountIdDecodeError(AccountIdError::Bech32DecodeError(
Bech32Error::InvalidDataLength { .. }
))
);
}
#[test]
fn address_serialization() -> anyhow::Result<()> {
let rng = &mut rand::rng();
for account_type in [
AccountType::FungibleFaucet,
AccountType::NonFungibleFaucet,
AccountType::RegularAccountImmutableCode,
AccountType::RegularAccountUpdatableCode,
]
.into_iter()
{
let account_id = AccountIdBuilder::new().account_type(account_type).build_with_rng(rng);
let address = Address::new(account_id).with_routing_parameters(
RoutingParameters::new(AddressInterface::BasicWallet)
.with_note_tag_len(NoteTag::DEFAULT_NETWORK_TAG_LENGTH)?,
)?;
let serialized = address.to_bytes();
let deserialized = Address::read_from_bytes(&serialized)?;
assert_eq!(address, deserialized);
}
Ok(())
}
#[test]
fn address_with_encryption_key() -> anyhow::Result<()> {
use crate::crypto::dsa::eddsa_25519::SecretKey;
use crate::crypto::ies::{SealingKey, UnsealingKey};
let rng = &mut rand::rng();
let account_id = AccountIdBuilder::new()
.account_type(AccountType::FungibleFaucet)
.build_with_rng(rng);
let secret_key = SecretKey::with_rng(rng);
let public_key = secret_key.public_key();
let sealing_key = SealingKey::X25519XChaCha20Poly1305(public_key.clone());
let unsealing_key = UnsealingKey::X25519XChaCha20Poly1305(secret_key.clone());
let address = Address::new(account_id).with_routing_parameters(
RoutingParameters::new(AddressInterface::BasicWallet)
.with_encryption_key(sealing_key.clone()),
)?;
let retrieved_key =
address.encryption_key().expect("encryption key should be present").clone();
assert_eq!(retrieved_key, sealing_key);
let plaintext = b"hello world";
let sealed_message =
retrieved_key.seal_bytes(rng, plaintext).expect("sealing should succeed");
let decrypted =
unsealing_key.unseal_bytes(sealed_message).expect("unsealing should succeed");
assert_eq!(decrypted.as_slice(), plaintext);
Ok(())
}
#[test]
fn address_encryption_key_encode_decode() -> anyhow::Result<()> {
use crate::crypto::dsa::eddsa_25519::SecretKey;
let rng = &mut rand::rng();
let account_id = AccountIdBuilder::new()
.account_type(AccountType::RegularAccountImmutableCode)
.storage_mode(AccountStorageMode::Public)
.build_with_rng(rng);
let secret_key = SecretKey::with_rng(rng);
let public_key = secret_key.public_key();
let sealing_key = SealingKey::X25519XChaCha20Poly1305(public_key);
let address = Address::new(account_id).with_routing_parameters(
RoutingParameters::new(AddressInterface::BasicWallet)
.with_encryption_key(sealing_key.clone()),
)?;
let encoded = address.encode(NetworkId::Mainnet);
let (decoded_network, decoded_address) = Address::decode(&encoded)?;
assert_eq!(decoded_network, NetworkId::Mainnet);
assert_eq!(address, decoded_address);
let decoded_key = decoded_address
.encryption_key()
.expect("encryption key should be present")
.clone();
assert_eq!(decoded_key, sealing_key);
Ok(())
}
}