use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use miden_protocol::Word;
use miden_protocol::account::component::{SchemaType, StorageSlotSchema};
use miden_protocol::account::{
AccountComponent,
AccountId,
AccountProcedureRoot,
StorageMap,
StorageMapKey,
StorageSlot,
StorageSlotName,
};
use miden_protocol::asset::AssetId;
use miden_protocol::utils::sync::LazyLock;
use super::policies::FeePolicy;
static ACTIVE_FEE_POLICY_PROC_ROOT_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::auth::network_account::active_fee_policy_proc_root")
.expect("storage slot name should be valid")
});
static ALLOWED_FEE_POLICY_PROC_ROOTS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::auth::network_account::allowed_fee_policy_proc_roots")
.expect("storage slot name should be valid")
});
static FEE_ASSET_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::auth::network_account::fee_asset_id")
.expect("storage slot name should be valid")
});
#[derive(Debug, Clone)]
pub struct FeePolicyManager {
fee_asset_id: AssetId,
active_fee_policy_root: AccountProcedureRoot,
policies: BTreeMap<AccountProcedureRoot, Vec<AccountComponent>>,
}
#[bon::bon]
impl FeePolicyManager {
#[builder]
pub fn new(
#[builder(field)] allowed_fee_policies: BTreeMap<AccountProcedureRoot, FeePolicy>,
fee_faucet_id: AccountId,
active_fee_policy: FeePolicy,
) -> Self {
let fee_asset_id = AssetId::new_fungible(fee_faucet_id);
let active_fee_policy_root = active_fee_policy.root();
let mut policies: BTreeMap<AccountProcedureRoot, Vec<AccountComponent>> = BTreeMap::new();
policies.insert(active_fee_policy_root, active_fee_policy.into_iter().collect());
for (root, policy) in allowed_fee_policies {
policies.entry(root).or_insert_with(|| policy.into_iter().collect());
}
Self {
fee_asset_id,
active_fee_policy_root,
policies,
}
}
}
impl<S: fee_policy_manager_builder::State> FeePolicyManagerBuilder<S> {
pub fn allowed_fee_policy(mut self, policy: FeePolicy) -> Self {
self.allowed_fee_policies.insert(policy.root(), policy);
self
}
}
impl FeePolicyManager {
pub fn fee_asset_id(&self) -> AssetId {
self.fee_asset_id
}
pub fn active_fee_policy(&self) -> AccountProcedureRoot {
self.active_fee_policy_root
}
pub fn allowed_fee_policies(&self) -> Vec<AccountProcedureRoot> {
self.policies.keys().copied().collect()
}
pub fn into_fee_policy_components(self) -> impl Iterator<Item = AccountComponent> {
self.policies.into_values().flat_map(|components| components.into_iter())
}
pub fn active_fee_policy_slot() -> &'static StorageSlotName {
&ACTIVE_FEE_POLICY_PROC_ROOT_SLOT_NAME
}
pub fn allowed_fee_policies_slot() -> &'static StorageSlotName {
&ALLOWED_FEE_POLICY_PROC_ROOTS_SLOT_NAME
}
pub fn fee_asset_id_slot() -> &'static StorageSlotName {
&FEE_ASSET_ID_SLOT_NAME
}
pub(crate) fn slot_schemas() -> [(StorageSlotName, StorageSlotSchema); 3] {
[
(
ACTIVE_FEE_POLICY_PROC_ROOT_SLOT_NAME.clone(),
StorageSlotSchema::value(
"Active fee policy procedure root",
SchemaType::native_word(),
),
),
(
ALLOWED_FEE_POLICY_PROC_ROOTS_SLOT_NAME.clone(),
StorageSlotSchema::map(
"Allowed fee policy procedure roots",
SchemaType::native_word(),
SchemaType::native_word(),
),
),
(
FEE_ASSET_ID_SLOT_NAME.clone(),
StorageSlotSchema::value(
"ID of the asset fees are charged in",
SchemaType::native_word(),
),
),
]
}
pub fn to_storage_slots(&self) -> [StorageSlot; 3] {
let allowed_flag = Word::from([1u32, 0, 0, 0]);
let allowed_entries: Vec<_> = self
.allowed_fee_policies()
.into_iter()
.map(|root| (StorageMapKey::new(root.as_word()), allowed_flag))
.collect();
let allowed_map = StorageMap::with_entries(allowed_entries)
.expect("allowed policy roots should have unique keys");
[
StorageSlot::with_value(
ACTIVE_FEE_POLICY_PROC_ROOT_SLOT_NAME.clone(),
self.active_fee_policy().as_word(),
),
StorageSlot::with_map(ALLOWED_FEE_POLICY_PROC_ROOTS_SLOT_NAME.clone(), allowed_map),
StorageSlot::with_value(FEE_ASSET_ID_SLOT_NAME.clone(), self.fee_asset_id().to_word()),
]
}
}
#[cfg(test)]
mod tests {
use miden_protocol::account::AccountId;
use miden_protocol::account::component::AccountComponentMetadata;
use miden_protocol::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
use super::*;
use crate::account::auth::AuthNetworkAccount;
use crate::account::fees::BasicConstantFeePolicy;
use crate::code_builder::CodeBuilder;
fn fee_faucet_id() -> AccountId {
AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)
.expect("testing account ID should be valid")
}
fn custom_fee_policy() -> FeePolicy {
const NAME: &str = "test::fees::custom_policy";
let masm_source = "
@account_procedure
pub proc compute_note_fee
dropw dropw dropw dropw
end
";
let code = CodeBuilder::default()
.compile_component_code(NAME, masm_source)
.expect("custom fee policy should compile");
let root = code
.get_procedure_root_by_path(format!("{NAME}::compute_note_fee").as_str())
.expect("custom fee policy should export compute_note_fee");
let component = AccountComponent::new(code, vec![], AccountComponentMetadata::mock(NAME))
.expect("custom fee policy component should be valid");
FeePolicy::custom(root, [component])
.expect("custom fee policy root should be in the component")
}
#[test]
fn manager_expands_into_policy_components_only() {
let fee_policy_manager = FeePolicyManager::builder()
.fee_faucet_id(fee_faucet_id())
.active_fee_policy(BasicConstantFeePolicy::new().into())
.allowed_fee_policy(custom_fee_policy())
.build();
let allowed_roots = fee_policy_manager.allowed_fee_policies();
let components: Vec<AccountComponent> =
fee_policy_manager.into_fee_policy_components().collect();
for root in allowed_roots {
assert!(
components.iter().any(|component| component.has_procedure(root)),
"every registered policy root should be exported by a yielded component"
);
}
assert!(
!components
.iter()
.any(|component| component.has_procedure(AuthNetworkAccount::get_fee_policy_root())),
"the fee-policy procedures are exported by the auth component, not by the manager"
);
}
}