use alloc::collections::BTreeMap;
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::AssetAmount;
use miden_protocol::note::NoteScriptRoot;
use miden_protocol::utils::sync::LazyLock;
use miden_protocol::{Felt, Word};
use crate::account::account_component_code;
use crate::procedure_root;
account_component_code!(
BASIC_CONSTANT_FEE_POLICY_CODE,
"miden-standards-fees-policies-basic-constant-fee.masp"
);
const BASIC_CONSTANT_FEE_LIBRARY_PATH: &str =
"miden::standards::components::fees::policies::basic_constant_fee";
procedure_root!(
BASIC_CONSTANT_FEE_POLICY_ROOT,
BASIC_CONSTANT_FEE_LIBRARY_PATH,
BasicConstantFeePolicy::PROC_NAME,
BasicConstantFeePolicy::code()
);
static FEE_SCHEDULE_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::fees::policies::basic_constant_fee::fee_schedule")
.expect("storage slot name should be valid")
});
const FEE_SCHEDULE_ENTRY_MARKER: Felt = Felt::ONE;
fn fee_schedule_entry(fee: AssetAmount) -> Word {
let mut entry = fee.to_word();
entry[3] = FEE_SCHEDULE_ENTRY_MARKER;
entry
}
#[derive(Debug, Clone, Default)]
pub struct BasicConstantFeePolicy {
fee_schedule: BTreeMap<NoteScriptRoot, AssetAmount>,
}
impl BasicConstantFeePolicy {
pub const NAME: &'static str = "miden::standards::fees::policies::basic_constant_fee";
pub(crate) const PROC_NAME: &str = "compute_note_fee";
pub const fn name() -> AccountComponentName {
AccountComponentName::from_static_str(Self::NAME)
}
pub fn new() -> Self {
Self { fee_schedule: BTreeMap::new() }
}
#[must_use]
pub fn with_fee(mut self, script_root: NoteScriptRoot, fee: AssetAmount) -> Self {
self.fee_schedule.insert(script_root, fee);
self
}
#[must_use]
pub fn with_fees(
mut self,
entries: impl IntoIterator<Item = (NoteScriptRoot, AssetAmount)>,
) -> Self {
for (script_root, fee) in entries {
self = self.with_fee(script_root, fee);
}
self
}
pub fn code() -> &'static AccountComponentCode {
&BASIC_CONSTANT_FEE_POLICY_CODE
}
pub fn root() -> AccountProcedureRoot {
*BASIC_CONSTANT_FEE_POLICY_ROOT
}
pub fn fee_schedule_slot_name() -> &'static StorageSlotName {
&FEE_SCHEDULE_SLOT_NAME
}
pub fn fee_schedule(&self) -> &BTreeMap<NoteScriptRoot, AssetAmount> {
&self.fee_schedule
}
pub fn component_metadata() -> AccountComponentMetadata {
let storage_schema = StorageSchema::new([(
Self::fee_schedule_slot_name().clone(),
StorageSlotSchema::map(
"Fee charged per note script root",
SchemaType::native_word(),
SchemaType::native_word(),
),
)])
.expect("storage schema should be valid");
AccountComponentMetadata::new(Self::NAME)
.with_description(
"`basic_constant_fee` fee policy charging a constant per-note-script fee",
)
.with_storage_schema(storage_schema)
}
}
impl From<BasicConstantFeePolicy> for AccountComponent {
fn from(policy: BasicConstantFeePolicy) -> Self {
let entries = policy
.fee_schedule
.into_iter()
.map(|(root, fee)| (StorageMapKey::new(root.as_word()), fee_schedule_entry(fee)));
let fee_schedule_map = StorageMap::with_entries(entries)
.expect("fee schedule entries should produce a valid storage map");
let fee_schedule_slot = StorageSlot::with_map(
BasicConstantFeePolicy::fee_schedule_slot_name().clone(),
fee_schedule_map,
);
AccountComponent::new(
BasicConstantFeePolicy::code().clone(),
vec![fee_schedule_slot],
BasicConstantFeePolicy::component_metadata(),
)
.expect(
"`basic_constant_fee` fee policy component should satisfy the requirements of a valid account component",
)
}
}
#[cfg(test)]
mod tests {
use miden_protocol::account::StorageSlotContent;
use super::*;
#[test]
fn storage_slots_contain_expected_entries() -> anyhow::Result<()> {
let script_root = NoteScriptRoot::from_array([1, 2, 3, 4]);
let fee = AssetAmount::new(500)?;
let free_script_root = NoteScriptRoot::from_array([5, 6, 7, 8]);
let policy = BasicConstantFeePolicy::new()
.with_fee(script_root, AssetAmount::new(100)?)
.with_fees([(script_root, fee), (free_script_root, AssetAmount::ZERO)]);
let component = AccountComponent::from(policy);
let slot = component
.storage_slots()
.iter()
.find(|slot| slot.name() == BasicConstantFeePolicy::fee_schedule_slot_name())
.expect("fee schedule slot should exist");
let StorageSlotContent::Map(map) = slot.content() else {
panic!("fee schedule slot must be a map");
};
assert_eq!(
map.get(&StorageMapKey::new(script_root.as_word())),
Word::new([Felt::new(500)?, Felt::ZERO, Felt::ZERO, Felt::ONE]),
"the fee entry should be stored as an asset value word with the set-marker"
);
assert_eq!(
map.get(&StorageMapKey::new(free_script_root.as_word())),
Word::new([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]),
"an explicit 0-fee entry should survive as a non-zero word"
);
Ok(())
}
}