Skip to main content

miden_standards/account/fees/
fee_policy_manager.rs

1//! Fee policy manager.
2
3use 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
22// STORAGE SLOT NAMES
23// ================================================================================================
24
25static 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// FEE POLICY MANAGER
41// ================================================================================================
42
43/// The fee policy configuration of a network account: the policy defining the fee estimation for
44/// notes, which policies it may be switched to, and the asset that fees are charged in.
45///
46/// The actual fee computation logic is defined by a fee policy (e.g.
47/// [`BasicConstantFeePolicy`](crate::account::fees::BasicConstantFeePolicy)).
48///
49/// The [`AuthNetworkAccount`](crate::account::auth::AuthNetworkAccount) component carries the
50/// manager and adds the components of every registered policy when installed, so they do not
51/// need to be installed separately. The [`FeePolicyManager`] is not an account component itself and
52/// only exists to configure the auth component it is contained in.
53///
54/// Construct via [`Self::builder`]. The builder requires the fee faucet and the active fee policy.
55/// Additional allowed policies for runtime switching may be registered.
56#[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 constructor for [`FeePolicyManager`].
66    ///
67    /// The `fee_faucet_id` setter is required and sets the faucet issuing the fungible asset
68    /// fees are charged in. The `active_fee_policy` setter is required and registers the policy
69    /// the manager dispatches to. Each `allowed_fee_policy` setter registers an additional
70    /// reserved alternative for runtime switching via the `set_fee_policy` procedure.
71    #[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    /// Registers a reserved fee policy in the `allowed_fee_policy_proc_roots` map. May be
96    /// activated at runtime via `set_fee_policy`. Allowed entries are deduplicated by procedure
97    /// root.
98    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    // ACCESSORS
106    // --------------------------------------------------------------------------------------------
107
108    /// Returns the [`AssetId`] of the fungible asset fees are charged in.
109    pub fn fee_asset_id(&self) -> AssetId {
110        self.fee_asset_id
111    }
112
113    /// Returns the active fee policy procedure root.
114    pub fn active_fee_policy(&self) -> AccountProcedureRoot {
115        self.active_fee_policy_root
116    }
117
118    /// Returns all allowed fee policy procedure roots (active + reserved).
119    pub fn allowed_fee_policies(&self) -> Vec<AccountProcedureRoot> {
120        self.policies.keys().copied().collect()
121    }
122
123    /// Yields the [`AccountComponent`]s contributed by every registered fee policy.
124    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    // STORAGE
129    // --------------------------------------------------------------------------------------------
130
131    /// Returns the storage slot holding the active fee policy procedure root.
132    pub fn active_fee_policy_slot() -> &'static StorageSlotName {
133        &ACTIVE_FEE_POLICY_PROC_ROOT_SLOT_NAME
134    }
135
136    /// Returns the storage slot holding the map of allowed fee policy procedure roots.
137    pub fn allowed_fee_policies_slot() -> &'static StorageSlotName {
138        &ALLOWED_FEE_POLICY_PROC_ROOTS_SLOT_NAME
139    }
140
141    /// Returns the storage slot holding the ID of the asset fees are charged in.
142    pub fn fee_asset_id_slot() -> &'static StorageSlotName {
143        &FEE_ASSET_ID_SLOT_NAME
144    }
145
146    /// Returns the schema entries for the three fee-policy storage slots.
147    ///
148    /// These slots are installed by the owning
149    /// [`AuthNetworkAccount`][crate::account::auth::AuthNetworkAccount] component, whose storage
150    /// schema includes them.
151    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    /// Builds the three fee-policy storage slots from this manager's configuration:
179    /// - the active-policy value.
180    /// - the allowed-policies map.
181    /// - fee-asset value slot.
182    ///
183    /// Exposed so tests and tooling can reproduce the fee-policy storage without building the full
184    /// auth component.
185    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// TESTS
207// ================================================================================================
208
209#[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    /// Builds a minimal user-defined fee policy, mirroring how a contract developer registers a
226    /// reserved policy for runtime switching.
227    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    /// The manager is not a component itself: it expands into the components of the registered
248    /// policies only, each of which exports its policy root, and none of which exports a
249    /// fee-policy procedure - those belong to `AuthNetworkAccount`.
250    #[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}