use alloc::collections::BTreeSet;
use alloc::vec;
use miden_protocol::account::component::{
AccountComponentCode,
AccountComponentMetadata,
StorageSchema,
StorageSlotSchema,
};
use miden_protocol::account::{
AccountComponent,
AccountComponentName,
AccountProcedureRoot,
StorageSlotName,
};
use miden_protocol::note::NoteScriptRoot;
use miden_protocol::transaction::TransactionScriptRoot;
use super::{
NetworkAccountNoteAllowlist,
NetworkAccountNoteAllowlistError,
NetworkAccountTxScriptAllowlist,
SponsorshipPolicy,
};
use crate::account::account_component_code;
use crate::account::fees::FeePolicyManager;
use crate::note::{FeeSponsorshipNote, NetworkAccountConfigNote};
use crate::procedure_root;
use crate::tx_script::ExpirationTransactionScript;
account_component_code!(NETWORK_ACCOUNT_AUTH_CODE, "miden-standards-auth-network-account.masp");
const NETWORK_ACCOUNT_AUTH_LIBRARY_PATH: &str =
"miden::standards::components::auth::network_account";
procedure_root!(
NETWORK_ACCOUNT_ADD_ALLOWED_NOTE_SCRIPT,
NETWORK_ACCOUNT_AUTH_LIBRARY_PATH,
AuthNetworkAccount::ADD_ALLOWED_NOTE_SCRIPT_PROC_NAME,
AuthNetworkAccount::code()
);
procedure_root!(
NETWORK_ACCOUNT_REMOVE_ALLOWED_NOTE_SCRIPT,
NETWORK_ACCOUNT_AUTH_LIBRARY_PATH,
AuthNetworkAccount::REMOVE_ALLOWED_NOTE_SCRIPT_PROC_NAME,
AuthNetworkAccount::code()
);
procedure_root!(
NETWORK_ACCOUNT_ADD_ALLOWED_TX_SCRIPT,
NETWORK_ACCOUNT_AUTH_LIBRARY_PATH,
AuthNetworkAccount::ADD_ALLOWED_TX_SCRIPT_PROC_NAME,
AuthNetworkAccount::code()
);
procedure_root!(
NETWORK_ACCOUNT_REMOVE_ALLOWED_TX_SCRIPT,
NETWORK_ACCOUNT_AUTH_LIBRARY_PATH,
AuthNetworkAccount::REMOVE_ALLOWED_TX_SCRIPT_PROC_NAME,
AuthNetworkAccount::code()
);
procedure_root!(
NETWORK_ACCOUNT_ESTIMATE_NOTE_FEE,
NETWORK_ACCOUNT_AUTH_LIBRARY_PATH,
AuthNetworkAccount::ESTIMATE_NOTE_FEE_PROC_NAME,
AuthNetworkAccount::code()
);
procedure_root!(
NETWORK_ACCOUNT_SET_FEE_POLICY,
NETWORK_ACCOUNT_AUTH_LIBRARY_PATH,
AuthNetworkAccount::SET_FEE_POLICY_PROC_NAME,
AuthNetworkAccount::code()
);
procedure_root!(
NETWORK_ACCOUNT_GET_FEE_POLICY,
NETWORK_ACCOUNT_AUTH_LIBRARY_PATH,
AuthNetworkAccount::GET_FEE_POLICY_PROC_NAME,
AuthNetworkAccount::code()
);
procedure_root!(
NETWORK_ACCOUNT_GET_FEE_ASSET_ID,
NETWORK_ACCOUNT_AUTH_LIBRARY_PATH,
AuthNetworkAccount::GET_FEE_ASSET_ID_PROC_NAME,
AuthNetworkAccount::code()
);
procedure_root!(
FEE_MANAGER_ADD_ALLOWED_FEE_POLICY,
NETWORK_ACCOUNT_AUTH_LIBRARY_PATH,
AuthNetworkAccount::ADD_ALLOWED_FEE_POLICY_PROC_NAME,
AuthNetworkAccount::code()
);
procedure_root!(
FEE_MANAGER_REMOVE_ALLOWED_FEE_POLICY,
NETWORK_ACCOUNT_AUTH_LIBRARY_PATH,
AuthNetworkAccount::REMOVE_ALLOWED_FEE_POLICY_PROC_NAME,
AuthNetworkAccount::code()
);
pub struct AuthNetworkAccount {
allowed_notes: NetworkAccountNoteAllowlist,
allowed_tx_scripts: NetworkAccountTxScriptAllowlist,
sponsorship_policy: SponsorshipPolicy,
policy_manager: FeePolicyManager,
}
impl AuthNetworkAccount {
pub const NAME: &'static str = "miden::standards::auth::network_account";
const ADD_ALLOWED_NOTE_SCRIPT_PROC_NAME: &'static str = "add_allowed_note_script";
const REMOVE_ALLOWED_NOTE_SCRIPT_PROC_NAME: &'static str = "remove_allowed_note_script";
const ADD_ALLOWED_TX_SCRIPT_PROC_NAME: &'static str = "add_allowed_tx_script";
const REMOVE_ALLOWED_TX_SCRIPT_PROC_NAME: &'static str = "remove_allowed_tx_script";
const ESTIMATE_NOTE_FEE_PROC_NAME: &'static str = "estimate_note_fee";
const SET_FEE_POLICY_PROC_NAME: &'static str = "set_fee_policy";
const GET_FEE_POLICY_PROC_NAME: &'static str = "get_fee_policy";
const GET_FEE_ASSET_ID_PROC_NAME: &'static str = "get_fee_asset_id";
const ADD_ALLOWED_FEE_POLICY_PROC_NAME: &'static str = "add_allowed_fee_policy";
const REMOVE_ALLOWED_FEE_POLICY_PROC_NAME: &'static str = "remove_allowed_fee_policy";
pub fn new(
mut allowed_notes: BTreeSet<NoteScriptRoot>,
fee_policy_manager: FeePolicyManager,
) -> Result<Self, NetworkAccountNoteAllowlistError> {
allowed_notes.extend(Self::default_allowed_note_scripts());
Ok(Self::custom(allowed_notes, fee_policy_manager)?
.with_allowed_tx_scripts([ExpirationTransactionScript::script_root()]))
}
pub fn default_allowed_note_scripts() -> [NoteScriptRoot; 2] {
[NetworkAccountConfigNote::script_root(), FeeSponsorshipNote::script_root()]
}
pub fn custom(
allowed_notes: BTreeSet<NoteScriptRoot>,
fee_policy_manager: FeePolicyManager,
) -> Result<Self, NetworkAccountNoteAllowlistError> {
Ok(Self {
allowed_notes: NetworkAccountNoteAllowlist::new(allowed_notes)?,
allowed_tx_scripts: NetworkAccountTxScriptAllowlist::default(),
sponsorship_policy: SponsorshipPolicy::default(),
policy_manager: fee_policy_manager,
})
}
pub fn with_sponsorship_policy(mut self, sponsorship_policy: SponsorshipPolicy) -> Self {
self.sponsorship_policy = sponsorship_policy;
self
}
pub fn with_allowed_tx_scripts(
mut self,
allowed_tx_script_roots: impl IntoIterator<Item = TransactionScriptRoot>,
) -> Self {
self.allowed_tx_scripts.extend_script_roots(allowed_tx_script_roots);
self
}
pub const fn name() -> AccountComponentName {
AccountComponentName::from_static_str(Self::NAME)
}
pub fn code() -> &'static AccountComponentCode {
&NETWORK_ACCOUNT_AUTH_CODE
}
pub fn allowed_notes(&self) -> &NetworkAccountNoteAllowlist {
&self.allowed_notes
}
pub fn allowed_tx_scripts(&self) -> &NetworkAccountTxScriptAllowlist {
&self.allowed_tx_scripts
}
pub fn sponsorship_policy(&self) -> SponsorshipPolicy {
self.sponsorship_policy
}
pub fn add_allowed_note_script_root() -> AccountProcedureRoot {
*NETWORK_ACCOUNT_ADD_ALLOWED_NOTE_SCRIPT
}
pub fn remove_allowed_note_script_root() -> AccountProcedureRoot {
*NETWORK_ACCOUNT_REMOVE_ALLOWED_NOTE_SCRIPT
}
pub fn add_allowed_tx_script_root() -> AccountProcedureRoot {
*NETWORK_ACCOUNT_ADD_ALLOWED_TX_SCRIPT
}
pub fn remove_allowed_tx_script_root() -> AccountProcedureRoot {
*NETWORK_ACCOUNT_REMOVE_ALLOWED_TX_SCRIPT
}
pub fn estimate_note_fee_root() -> AccountProcedureRoot {
*NETWORK_ACCOUNT_ESTIMATE_NOTE_FEE
}
pub fn set_fee_policy_root() -> AccountProcedureRoot {
*NETWORK_ACCOUNT_SET_FEE_POLICY
}
pub fn get_fee_policy_root() -> AccountProcedureRoot {
*NETWORK_ACCOUNT_GET_FEE_POLICY
}
pub fn get_fee_asset_id_root() -> AccountProcedureRoot {
*NETWORK_ACCOUNT_GET_FEE_ASSET_ID
}
pub fn add_allowed_fee_policy_root() -> AccountProcedureRoot {
*FEE_MANAGER_ADD_ALLOWED_FEE_POLICY
}
pub fn remove_allowed_fee_policy_root() -> AccountProcedureRoot {
*FEE_MANAGER_REMOVE_ALLOWED_FEE_POLICY
}
pub fn allowed_note_scripts_slot() -> &'static StorageSlotName {
NetworkAccountNoteAllowlist::slot_name()
}
pub fn allowed_note_scripts_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
NetworkAccountNoteAllowlist::slot_schema()
}
pub fn allowed_tx_scripts_slot() -> &'static StorageSlotName {
NetworkAccountTxScriptAllowlist::slot_name()
}
pub fn allowed_tx_scripts_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
NetworkAccountTxScriptAllowlist::slot_schema()
}
pub fn sponsorship_policy_slot() -> &'static StorageSlotName {
SponsorshipPolicy::slot_name()
}
pub fn sponsorship_policy_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
SponsorshipPolicy::slot_schema()
}
pub fn component_metadata() -> AccountComponentMetadata {
let mut slot_schemas = vec![
NetworkAccountNoteAllowlist::slot_schema(),
NetworkAccountTxScriptAllowlist::slot_schema(),
SponsorshipPolicy::slot_schema(),
];
slot_schemas.extend(FeePolicyManager::slot_schemas());
let storage_schema =
StorageSchema::new(slot_schemas).expect("storage schema should be valid");
AccountComponentMetadata::new(Self::NAME)
.with_description(
"Authentication component that restricts input notes and transaction scripts to \
fixed allowlists of script roots",
)
.with_storage_schema(storage_schema)
}
}
impl IntoIterator for AuthNetworkAccount {
type Item = AccountComponent;
type IntoIter = alloc::vec::IntoIter<AccountComponent>;
fn into_iter(self) -> Self::IntoIter {
let Self {
allowed_notes,
allowed_tx_scripts,
sponsorship_policy,
policy_manager,
} = self;
let fee_policy_slots = policy_manager.to_storage_slots();
let mut storage_slots = vec![
allowed_notes.into_storage_slot(),
allowed_tx_scripts.into_storage_slot(),
sponsorship_policy.into_storage_slot(),
];
storage_slots.extend(fee_policy_slots);
let auth_component =
AccountComponent::new(Self::code().clone(), storage_slots, Self::component_metadata())
.expect(
"AuthNetworkAccount component should satisfy the requirements of a valid \
account component",
);
let mut components = vec![auth_component];
components.extend(policy_manager.into_fee_policy_components());
components.into_iter()
}
}
#[cfg(test)]
mod tests {
use miden_protocol::account::{AccountBuilder, StorageSlotContent};
use miden_protocol::asset::FungibleAsset;
use super::*;
use crate::account::wallets::BasicWallet;
use crate::note::NetworkAccountConfigNote;
#[test]
fn auth_network_account_component_builds() {
let root_a = NoteScriptRoot::from_array([1, 2, 3, 4]);
let root_b = NoteScriptRoot::from_array([5, 6, 7, 8]);
let _account = AccountBuilder::new([0; 32])
.with_components(
AuthNetworkAccount::new(
BTreeSet::from_iter([root_a, root_b]),
FeePolicyManager::mock(FungibleAsset::mock_issuer()),
)
.expect("non-empty allowlist should construct"),
)
.with_component(BasicWallet)
.build()
.expect("account building with AuthNetworkAccount failed");
}
#[test]
fn auth_network_account_with_empty_input_allowlists_default_notes() {
let account = AccountBuilder::new([0; 32])
.with_components(
AuthNetworkAccount::new(
BTreeSet::new(),
FeePolicyManager::mock(FungibleAsset::mock_issuer()),
)
.expect("the default note roots make the allowlist non-empty"),
)
.with_component(BasicWallet)
.build()
.expect("account building with AuthNetworkAccount failed");
let allowlist = NetworkAccountNoteAllowlist::try_from(account.storage())
.expect("allowlist should be reconstructable from account storage");
assert_eq!(
allowlist.allowed_script_roots(),
&BTreeSet::from_iter([
NetworkAccountConfigNote::script_root(),
FeeSponsorshipNote::script_root(),
]),
"an empty input should yield an allowlist containing only the default note roots",
);
}
#[test]
fn auth_network_account_uses_standardized_allowlist_slot() {
let root_a = NoteScriptRoot::from_array([1, 2, 3, 4]);
let component: AccountComponent = AuthNetworkAccount::new(
BTreeSet::from_iter([root_a]),
FeePolicyManager::mock(FungibleAsset::mock_issuer()),
)
.expect("non-empty allowlist should construct")
.into_iter()
.next()
.expect("auth component is yielded first");
let storage_slots = component.storage_slots();
assert_eq!(storage_slots[0].name(), NetworkAccountNoteAllowlist::slot_name());
assert_eq!(storage_slots[1].name(), NetworkAccountTxScriptAllowlist::slot_name());
for name in [
NetworkAccountNoteAllowlist::slot_name(),
NetworkAccountTxScriptAllowlist::slot_name(),
] {
let slot = storage_slots
.iter()
.find(|slot| slot.name() == name)
.expect("allowlist slot must be present");
let StorageSlotContent::Map(_) = slot.content() else {
panic!("allowlist slots must be maps");
};
}
}
#[test]
fn auth_network_account_always_allowlists_config_note() {
let root_a = NoteScriptRoot::from_array([1, 2, 3, 4]);
let account = AccountBuilder::new([0; 32])
.with_components(
AuthNetworkAccount::new(
BTreeSet::from_iter([root_a]),
FeePolicyManager::mock(FungibleAsset::mock_issuer()),
)
.expect("config note root makes the allowlist non-empty"),
)
.with_component(BasicWallet)
.build()
.expect("account building with AuthNetworkAccount failed");
let allowlist = NetworkAccountNoteAllowlist::try_from(account.storage())
.expect("allowlist should be reconstructable from account storage");
assert!(
allowlist
.allowed_script_roots()
.contains(&NetworkAccountConfigNote::script_root()),
"new should always allowlist the config note root",
);
assert!(
allowlist.allowed_script_roots().contains(&root_a),
"new should preserve the provided allowlist entries",
);
}
}