use alloc::collections::{BTreeMap, BTreeSet};
use alloc::vec::Vec;
use miden_protocol::Word;
use miden_protocol::account::component::{
AccountComponentCode,
AccountComponentMetadata,
SchemaType,
StorageSchema,
StorageSlotSchema,
};
use miden_protocol::account::{
AccountComponent,
AccountComponentName,
AccountProcedureRoot,
StorageMap,
StorageMapKey,
StorageSlot,
StorageSlotName,
};
use miden_protocol::asset::AssetCallbacks;
use miden_protocol::utils::sync::LazyLock;
use thiserror::Error;
use super::PolicyRegistration;
use super::burn::BurnPolicyConfig;
use super::mint::MintPolicyConfig;
use super::transfer::TransferPolicy;
use crate::account::account_component_code;
use crate::procedure_root;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum TokenPolicyManagerError {
#[error("token policy manager: more than one active {kind} policy registered")]
DuplicateActivePolicy { kind: &'static str },
}
account_component_code!(POLICY_MANAGER_CODE, "faucets/policies/policy_manager.masl");
const POLICY_MANAGER_LIBRARY_PATH: &str =
"miden::standards::components::faucets::policies::policy_manager";
procedure_root!(
POLICY_MANAGER_SET_MINT_POLICY,
POLICY_MANAGER_LIBRARY_PATH,
TokenPolicyManager::SET_MINT_POLICY_PROC_NAME,
TokenPolicyManager::code()
);
procedure_root!(
POLICY_MANAGER_SET_BURN_POLICY,
POLICY_MANAGER_LIBRARY_PATH,
TokenPolicyManager::SET_BURN_POLICY_PROC_NAME,
TokenPolicyManager::code()
);
procedure_root!(
POLICY_MANAGER_SET_SEND_POLICY,
POLICY_MANAGER_LIBRARY_PATH,
TokenPolicyManager::SET_SEND_POLICY_PROC_NAME,
TokenPolicyManager::code()
);
procedure_root!(
POLICY_MANAGER_SET_RECEIVE_POLICY,
POLICY_MANAGER_LIBRARY_PATH,
TokenPolicyManager::SET_RECEIVE_POLICY_PROC_NAME,
TokenPolicyManager::code()
);
static ACTIVE_MINT_POLICY_PROC_ROOT_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new(
"miden::standards::faucets::policies::policy_manager::active_mint_policy_proc_root",
)
.expect("storage slot name should be valid")
});
static ACTIVE_BURN_POLICY_PROC_ROOT_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new(
"miden::standards::faucets::policies::policy_manager::active_burn_policy_proc_root",
)
.expect("storage slot name should be valid")
});
static ALLOWED_MINT_POLICY_PROC_ROOTS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new(
"miden::standards::faucets::policies::policy_manager::allowed_mint_policy_proc_roots",
)
.expect("storage slot name should be valid")
});
static ALLOWED_BURN_POLICY_PROC_ROOTS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new(
"miden::standards::faucets::policies::policy_manager::allowed_burn_policy_proc_roots",
)
.expect("storage slot name should be valid")
});
static ALLOWED_SEND_POLICY_PROC_ROOTS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new(
"miden::standards::faucets::policies::policy_manager::allowed_send_policy_proc_roots",
)
.expect("storage slot name should be valid")
});
static ALLOWED_RECEIVE_POLICY_PROC_ROOTS_SLOT_NAME: LazyLock<StorageSlotName> =
LazyLock::new(|| {
StorageSlotName::new(
"miden::standards::faucets::policies::policy_manager::allowed_receive_policy_proc_roots",
)
.expect("storage slot name should be valid")
});
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum PolicyKind {
Mint,
Burn,
Send,
Receive,
}
#[derive(Debug, Clone)]
struct PolicyConfig {
components: Vec<AccountComponent>,
kinds: BTreeSet<PolicyKind>,
}
#[derive(Debug, Clone)]
pub struct TokenPolicyManager {
active_mint_policy_root: AccountProcedureRoot,
active_burn_policy_root: AccountProcedureRoot,
active_send_policy_root: AccountProcedureRoot,
active_receive_policy_root: AccountProcedureRoot,
policies: BTreeMap<AccountProcedureRoot, PolicyConfig>,
}
impl TokenPolicyManager {
pub const NAME: &'static str = "miden::standards::faucets::policies::policy_manager";
pub const DESCRIPTION: &'static str = "Token policy manager for fungible faucets";
const SET_MINT_POLICY_PROC_NAME: &'static str = "set_mint_policy";
const SET_BURN_POLICY_PROC_NAME: &'static str = "set_burn_policy";
const SET_SEND_POLICY_PROC_NAME: &'static str = "set_send_policy";
const SET_RECEIVE_POLICY_PROC_NAME: &'static str = "set_receive_policy";
pub const fn name() -> AccountComponentName {
AccountComponentName::from_static_str(Self::NAME)
}
pub fn new() -> Self {
Self::default()
}
pub fn with_mint_policy(
mut self,
policy: MintPolicyConfig,
registration: PolicyRegistration,
) -> Result<Self, TokenPolicyManagerError> {
let root = AccountProcedureRoot::from_raw(policy.root());
if registration == PolicyRegistration::Active {
if !self.active_mint_policy_root.as_word().is_empty() {
return Err(TokenPolicyManagerError::DuplicateActivePolicy { kind: "mint" });
}
self.active_mint_policy_root = root;
}
self.insert_policy(root, policy.into_components(), PolicyKind::Mint);
Ok(self)
}
pub fn with_burn_policy(
mut self,
policy: BurnPolicyConfig,
registration: PolicyRegistration,
) -> Result<Self, TokenPolicyManagerError> {
let root = AccountProcedureRoot::from_raw(policy.root());
if registration == PolicyRegistration::Active {
if !self.active_burn_policy_root.as_word().is_empty() {
return Err(TokenPolicyManagerError::DuplicateActivePolicy { kind: "burn" });
}
self.active_burn_policy_root = root;
}
self.insert_policy(root, policy.into_components(), PolicyKind::Burn);
Ok(self)
}
pub fn with_send_policy(
mut self,
policy: TransferPolicy,
registration: PolicyRegistration,
) -> Result<Self, TokenPolicyManagerError> {
let root = policy.root();
if registration == PolicyRegistration::Active {
if !self.active_send_policy_root.as_word().is_empty() {
return Err(TokenPolicyManagerError::DuplicateActivePolicy { kind: "send" });
}
self.active_send_policy_root = root;
}
self.insert_policy(root, policy.into_components(), PolicyKind::Send);
Ok(self)
}
pub fn with_receive_policy(
mut self,
policy: TransferPolicy,
registration: PolicyRegistration,
) -> Result<Self, TokenPolicyManagerError> {
let root = policy.root();
if registration == PolicyRegistration::Active {
if !self.active_receive_policy_root.as_word().is_empty() {
return Err(TokenPolicyManagerError::DuplicateActivePolicy { kind: "receive" });
}
self.active_receive_policy_root = root;
}
self.insert_policy(root, policy.into_components(), PolicyKind::Receive);
Ok(self)
}
fn insert_policy(
&mut self,
root: AccountProcedureRoot,
components: Vec<AccountComponent>,
kind: PolicyKind,
) {
self.policies
.entry(root)
.and_modify(|cfg| {
cfg.kinds.insert(kind);
})
.or_insert_with(|| {
let mut kinds = BTreeSet::new();
kinds.insert(kind);
PolicyConfig { components, kinds }
});
}
pub fn active_mint_policy(&self) -> Option<AccountProcedureRoot> {
(!self.active_mint_policy_root.as_word().is_empty()).then_some(self.active_mint_policy_root)
}
pub fn active_burn_policy(&self) -> Option<AccountProcedureRoot> {
(!self.active_burn_policy_root.as_word().is_empty()).then_some(self.active_burn_policy_root)
}
pub fn active_send_policy(&self) -> Option<AccountProcedureRoot> {
(!self.active_send_policy_root.as_word().is_empty()).then_some(self.active_send_policy_root)
}
pub fn active_receive_policy(&self) -> Option<AccountProcedureRoot> {
(!self.active_receive_policy_root.as_word().is_empty())
.then_some(self.active_receive_policy_root)
}
pub fn allowed_mint_policies(&self) -> Vec<AccountProcedureRoot> {
self.roots_of_kind(PolicyKind::Mint)
}
pub fn allowed_burn_policies(&self) -> Vec<AccountProcedureRoot> {
self.roots_of_kind(PolicyKind::Burn)
}
pub fn allowed_send_policies(&self) -> Vec<AccountProcedureRoot> {
self.roots_of_kind(PolicyKind::Send)
}
pub fn allowed_receive_policies(&self) -> Vec<AccountProcedureRoot> {
self.roots_of_kind(PolicyKind::Receive)
}
fn roots_of_kind(&self, kind: PolicyKind) -> Vec<AccountProcedureRoot> {
self.policies
.iter()
.filter(|(_, cfg)| cfg.kinds.contains(&kind))
.map(|(root, _)| *root)
.collect()
}
pub fn set_mint_policy_root() -> AccountProcedureRoot {
*POLICY_MANAGER_SET_MINT_POLICY
}
pub fn set_burn_policy_root() -> AccountProcedureRoot {
*POLICY_MANAGER_SET_BURN_POLICY
}
pub fn set_send_policy_root() -> AccountProcedureRoot {
*POLICY_MANAGER_SET_SEND_POLICY
}
pub fn set_receive_policy_root() -> AccountProcedureRoot {
*POLICY_MANAGER_SET_RECEIVE_POLICY
}
pub fn active_mint_policy_slot() -> &'static StorageSlotName {
&ACTIVE_MINT_POLICY_PROC_ROOT_SLOT_NAME
}
pub fn active_burn_policy_slot() -> &'static StorageSlotName {
&ACTIVE_BURN_POLICY_PROC_ROOT_SLOT_NAME
}
pub fn allowed_mint_policies_slot() -> &'static StorageSlotName {
&ALLOWED_MINT_POLICY_PROC_ROOTS_SLOT_NAME
}
pub fn allowed_burn_policies_slot() -> &'static StorageSlotName {
&ALLOWED_BURN_POLICY_PROC_ROOTS_SLOT_NAME
}
pub fn allowed_send_policies_slot() -> &'static StorageSlotName {
&ALLOWED_SEND_POLICY_PROC_ROOTS_SLOT_NAME
}
pub fn allowed_receive_policies_slot() -> &'static StorageSlotName {
&ALLOWED_RECEIVE_POLICY_PROC_ROOTS_SLOT_NAME
}
pub fn code() -> &'static AccountComponentCode {
&POLICY_MANAGER_CODE
}
pub fn component_metadata() -> AccountComponentMetadata {
let storage_schema = StorageSchema::new(vec![
(
ACTIVE_MINT_POLICY_PROC_ROOT_SLOT_NAME.clone(),
StorageSlotSchema::value(
"Active mint policy procedure root",
SchemaType::native_word(),
),
),
(
ACTIVE_BURN_POLICY_PROC_ROOT_SLOT_NAME.clone(),
StorageSlotSchema::value(
"Active burn policy procedure root",
SchemaType::native_word(),
),
),
(
ALLOWED_MINT_POLICY_PROC_ROOTS_SLOT_NAME.clone(),
StorageSlotSchema::map(
"Allowed mint policy procedure roots",
SchemaType::native_word(),
SchemaType::native_word(),
),
),
(
ALLOWED_BURN_POLICY_PROC_ROOTS_SLOT_NAME.clone(),
StorageSlotSchema::map(
"Allowed burn policy procedure roots",
SchemaType::native_word(),
SchemaType::native_word(),
),
),
(
ALLOWED_SEND_POLICY_PROC_ROOTS_SLOT_NAME.clone(),
StorageSlotSchema::map(
"Allowed send policy procedure roots",
SchemaType::native_word(),
SchemaType::native_word(),
),
),
(
ALLOWED_RECEIVE_POLICY_PROC_ROOTS_SLOT_NAME.clone(),
StorageSlotSchema::map(
"Allowed receive policy procedure roots",
SchemaType::native_word(),
SchemaType::native_word(),
),
),
])
.expect("storage schema should be valid");
AccountComponentMetadata::new(Self::NAME)
.with_description(Self::DESCRIPTION)
.with_storage_schema(storage_schema)
}
fn manager_storage_slots(&self) -> Vec<StorageSlot> {
let mut slots = vec![
StorageSlot::with_value(
ACTIVE_MINT_POLICY_PROC_ROOT_SLOT_NAME.clone(),
self.active_mint_policy_root.as_word(),
),
StorageSlot::with_value(
ACTIVE_BURN_POLICY_PROC_ROOT_SLOT_NAME.clone(),
self.active_burn_policy_root.as_word(),
),
StorageSlot::with_map(
ALLOWED_MINT_POLICY_PROC_ROOTS_SLOT_NAME.clone(),
self.build_allowed_map(PolicyKind::Mint),
),
StorageSlot::with_map(
ALLOWED_BURN_POLICY_PROC_ROOTS_SLOT_NAME.clone(),
self.build_allowed_map(PolicyKind::Burn),
),
StorageSlot::with_map(
ALLOWED_SEND_POLICY_PROC_ROOTS_SLOT_NAME.clone(),
self.build_allowed_map(PolicyKind::Send),
),
StorageSlot::with_map(
ALLOWED_RECEIVE_POLICY_PROC_ROOTS_SLOT_NAME.clone(),
self.build_allowed_map(PolicyKind::Receive),
),
];
let has_transfer_policy = self.policies.iter().any(|(_, cfg)| {
cfg.kinds.contains(&PolicyKind::Send) || cfg.kinds.contains(&PolicyKind::Receive)
});
if has_transfer_policy {
let callback_slots = AssetCallbacks::new()
.on_before_asset_added_to_account(self.active_receive_policy_root.as_word())
.on_before_asset_added_to_note(self.active_send_policy_root.as_word())
.into_storage_slots();
slots.extend(callback_slots);
}
slots
}
fn build_allowed_map(&self, kind: PolicyKind) -> StorageMap {
let allowed_flag = Word::from([1u32, 0, 0, 0]);
let entries: Vec<_> = self
.policies
.iter()
.filter(|(_, cfg)| cfg.kinds.contains(&kind))
.map(|(root, _)| (StorageMapKey::new(root.as_word()), allowed_flag))
.collect();
StorageMap::with_entries(entries).expect("allowed policy roots should have unique keys")
}
fn to_manager_component(&self) -> AccountComponent {
let storage_slots = self.manager_storage_slots();
AccountComponent::new(
Self::code().clone(),
storage_slots,
Self::component_metadata(),
)
.expect(
"token policy manager component should satisfy the requirements of a valid account component",
)
}
}
impl Default for TokenPolicyManager {
fn default() -> Self {
Self {
active_mint_policy_root: AccountProcedureRoot::from_raw(Word::empty()),
active_burn_policy_root: AccountProcedureRoot::from_raw(Word::empty()),
active_send_policy_root: AccountProcedureRoot::from_raw(Word::empty()),
active_receive_policy_root: AccountProcedureRoot::from_raw(Word::empty()),
policies: BTreeMap::new(),
}
}
}
impl IntoIterator for TokenPolicyManager {
type Item = AccountComponent;
type IntoIter = alloc::vec::IntoIter<AccountComponent>;
fn into_iter(self) -> Self::IntoIter {
let manager_component = self.to_manager_component();
let mut components = vec![manager_component];
for (_, policy) in self.policies {
components.extend(policy.components);
}
components.into_iter()
}
}
#[cfg(test)]
mod tests {
use miden_protocol::asset::AssetCallbacks;
use super::*;
use crate::account::policies::transfer::TransferAllowAll;
fn find_slot<'a>(
component: &'a AccountComponent,
slot_name: &StorageSlotName,
) -> Option<&'a StorageSlot> {
component.storage_slots().iter().find(|slot| slot.name() == slot_name)
}
#[test]
fn allow_all_transfer_policy_registers_protocol_callback_slots() {
let manager = TokenPolicyManager::new()
.with_mint_policy(MintPolicyConfig::AllowAll, PolicyRegistration::Active)
.unwrap()
.with_burn_policy(BurnPolicyConfig::AllowAll, PolicyRegistration::Active)
.unwrap()
.with_send_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)
.unwrap()
.with_receive_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)
.unwrap();
let manager_component = manager.to_manager_component();
let allow_all_root = TransferAllowAll::root().as_word();
let on_account_slot =
find_slot(&manager_component, AssetCallbacks::on_before_asset_added_to_account_slot())
.expect(
"AllowAll receive policy must register the on_before_asset_added_to_account \
protocol callback slot",
);
let on_note_slot =
find_slot(&manager_component, AssetCallbacks::on_before_asset_added_to_note_slot())
.expect(
"AllowAll send policy must register the on_before_asset_added_to_note protocol \
callback slot",
);
assert_eq!(on_account_slot.value(), allow_all_root);
assert_eq!(on_note_slot.value(), allow_all_root);
}
#[test]
fn manager_without_transfer_policies_omits_protocol_callback_slots() {
let manager = TokenPolicyManager::new()
.with_mint_policy(MintPolicyConfig::AllowAll, PolicyRegistration::Active)
.unwrap()
.with_burn_policy(BurnPolicyConfig::AllowAll, PolicyRegistration::Active)
.unwrap();
let manager_component = manager.to_manager_component();
assert!(
find_slot(&manager_component, AssetCallbacks::on_before_asset_added_to_account_slot(),)
.is_none(),
"without a receive policy, the manager must leave the on_before_asset_added_to_account \
slot to a separate component",
);
assert!(
find_slot(&manager_component, AssetCallbacks::on_before_asset_added_to_note_slot())
.is_none(),
"without a send policy, the manager must leave the on_before_asset_added_to_note slot \
to a separate component",
);
}
}