miden_standards/account/fees/
fee_policy_manager.rs1use alloc::collections::BTreeMap;
4use alloc::vec::Vec;
5
6use miden_protocol::Word;
7use miden_protocol::account::component::{SchemaType, StorageSlotSchema};
8use miden_protocol::account::{
9 AccountComponent,
10 AccountId,
11 AccountProcedureRoot,
12 StorageMap,
13 StorageMapKey,
14 StorageSlot,
15 StorageSlotName,
16};
17use miden_protocol::asset::AssetId;
18use miden_protocol::utils::sync::LazyLock;
19
20use super::policies::FeePolicy;
21
22static ACTIVE_FEE_POLICY_PROC_ROOT_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
26 StorageSlotName::new("miden::standards::auth::network_account::active_fee_policy_proc_root")
27 .expect("storage slot name should be valid")
28});
29
30static ALLOWED_FEE_POLICY_PROC_ROOTS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
31 StorageSlotName::new("miden::standards::auth::network_account::allowed_fee_policy_proc_roots")
32 .expect("storage slot name should be valid")
33});
34
35static FEE_ASSET_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
36 StorageSlotName::new("miden::standards::auth::network_account::fee_asset_id")
37 .expect("storage slot name should be valid")
38});
39
40#[derive(Debug, Clone)]
57pub struct FeePolicyManager {
58 fee_asset_id: AssetId,
59 active_fee_policy_root: AccountProcedureRoot,
60 policies: BTreeMap<AccountProcedureRoot, Vec<AccountComponent>>,
61}
62
63#[bon::bon]
64impl FeePolicyManager {
65 #[builder]
72 pub fn new(
73 #[builder(field)] allowed_fee_policies: BTreeMap<AccountProcedureRoot, FeePolicy>,
74 fee_faucet_id: AccountId,
75 active_fee_policy: FeePolicy,
76 ) -> Self {
77 let fee_asset_id = AssetId::new_fungible(fee_faucet_id);
78 let active_fee_policy_root = active_fee_policy.root();
79
80 let mut policies: BTreeMap<AccountProcedureRoot, Vec<AccountComponent>> = BTreeMap::new();
81 policies.insert(active_fee_policy_root, active_fee_policy.into_iter().collect());
82 for (root, policy) in allowed_fee_policies {
83 policies.entry(root).or_insert_with(|| policy.into_iter().collect());
84 }
85
86 Self {
87 fee_asset_id,
88 active_fee_policy_root,
89 policies,
90 }
91 }
92}
93
94impl<S: fee_policy_manager_builder::State> FeePolicyManagerBuilder<S> {
95 pub fn allowed_fee_policy(mut self, policy: FeePolicy) -> Self {
99 self.allowed_fee_policies.insert(policy.root(), policy);
100 self
101 }
102}
103
104impl FeePolicyManager {
105 pub fn fee_asset_id(&self) -> AssetId {
110 self.fee_asset_id
111 }
112
113 pub fn active_fee_policy(&self) -> AccountProcedureRoot {
115 self.active_fee_policy_root
116 }
117
118 pub fn allowed_fee_policies(&self) -> Vec<AccountProcedureRoot> {
120 self.policies.keys().copied().collect()
121 }
122
123 pub fn into_fee_policy_components(self) -> impl Iterator<Item = AccountComponent> {
125 self.policies.into_values().flat_map(|components| components.into_iter())
126 }
127
128 pub fn active_fee_policy_slot() -> &'static StorageSlotName {
133 &ACTIVE_FEE_POLICY_PROC_ROOT_SLOT_NAME
134 }
135
136 pub fn allowed_fee_policies_slot() -> &'static StorageSlotName {
138 &ALLOWED_FEE_POLICY_PROC_ROOTS_SLOT_NAME
139 }
140
141 pub fn fee_asset_id_slot() -> &'static StorageSlotName {
143 &FEE_ASSET_ID_SLOT_NAME
144 }
145
146 pub(crate) fn slot_schemas() -> [(StorageSlotName, StorageSlotSchema); 3] {
152 [
153 (
154 ACTIVE_FEE_POLICY_PROC_ROOT_SLOT_NAME.clone(),
155 StorageSlotSchema::value(
156 "Active fee policy procedure root",
157 SchemaType::native_word(),
158 ),
159 ),
160 (
161 ALLOWED_FEE_POLICY_PROC_ROOTS_SLOT_NAME.clone(),
162 StorageSlotSchema::map(
163 "Allowed fee policy procedure roots",
164 SchemaType::native_word(),
165 SchemaType::native_word(),
166 ),
167 ),
168 (
169 FEE_ASSET_ID_SLOT_NAME.clone(),
170 StorageSlotSchema::value(
171 "ID of the asset fees are charged in",
172 SchemaType::native_word(),
173 ),
174 ),
175 ]
176 }
177
178 pub fn to_storage_slots(&self) -> [StorageSlot; 3] {
186 let allowed_flag = Word::from([1u32, 0, 0, 0]);
187 let allowed_entries: Vec<_> = self
188 .allowed_fee_policies()
189 .into_iter()
190 .map(|root| (StorageMapKey::new(root.as_word()), allowed_flag))
191 .collect();
192 let allowed_map = StorageMap::with_entries(allowed_entries)
193 .expect("allowed policy roots should have unique keys");
194
195 [
196 StorageSlot::with_value(
197 ACTIVE_FEE_POLICY_PROC_ROOT_SLOT_NAME.clone(),
198 self.active_fee_policy().as_word(),
199 ),
200 StorageSlot::with_map(ALLOWED_FEE_POLICY_PROC_ROOTS_SLOT_NAME.clone(), allowed_map),
201 StorageSlot::with_value(FEE_ASSET_ID_SLOT_NAME.clone(), self.fee_asset_id().to_word()),
202 ]
203 }
204}
205
206#[cfg(test)]
210mod tests {
211 use miden_protocol::account::AccountId;
212 use miden_protocol::account::component::AccountComponentMetadata;
213 use miden_protocol::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
214
215 use super::*;
216 use crate::account::auth::AuthNetworkAccount;
217 use crate::account::fees::BasicConstantFeePolicy;
218 use crate::code_builder::CodeBuilder;
219
220 fn fee_faucet_id() -> AccountId {
221 AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)
222 .expect("testing account ID should be valid")
223 }
224
225 fn custom_fee_policy() -> FeePolicy {
228 const NAME: &str = "test::fees::custom_policy";
229 let masm_source = "
230 @account_procedure
231 pub proc compute_note_fee
232 dropw dropw dropw dropw
233 end
234 ";
235 let code = CodeBuilder::default()
236 .compile_component_code(NAME, masm_source)
237 .expect("custom fee policy should compile");
238 let root = code
239 .get_procedure_root_by_path(format!("{NAME}::compute_note_fee").as_str())
240 .expect("custom fee policy should export compute_note_fee");
241 let component = AccountComponent::new(code, vec![], AccountComponentMetadata::mock(NAME))
242 .expect("custom fee policy component should be valid");
243 FeePolicy::custom(root, [component])
244 .expect("custom fee policy root should be in the component")
245 }
246
247 #[test]
251 fn manager_expands_into_policy_components_only() {
252 let fee_policy_manager = FeePolicyManager::builder()
253 .fee_faucet_id(fee_faucet_id())
254 .active_fee_policy(BasicConstantFeePolicy::new().into())
255 .allowed_fee_policy(custom_fee_policy())
256 .build();
257
258 let allowed_roots = fee_policy_manager.allowed_fee_policies();
259 let components: Vec<AccountComponent> =
260 fee_policy_manager.into_fee_policy_components().collect();
261
262 for root in allowed_roots {
263 assert!(
264 components.iter().any(|component| component.has_procedure(root)),
265 "every registered policy root should be exported by a yielded component"
266 );
267 }
268 assert!(
269 !components
270 .iter()
271 .any(|component| component.has_procedure(AuthNetworkAccount::get_fee_policy_root())),
272 "the fee-policy procedures are exported by the auth component, not by the manager"
273 );
274 }
275}