miden_standards/account/fees/policies/basic_constant_fee.rs
1use alloc::collections::BTreeMap;
2
3use miden_protocol::account::component::{
4 AccountComponentCode,
5 AccountComponentMetadata,
6 SchemaType,
7 StorageSchema,
8 StorageSlotSchema,
9};
10use miden_protocol::account::{
11 AccountComponent,
12 AccountComponentName,
13 AccountProcedureRoot,
14 StorageMap,
15 StorageMapKey,
16 StorageSlot,
17 StorageSlotName,
18};
19use miden_protocol::asset::AssetAmount;
20use miden_protocol::note::NoteScriptRoot;
21use miden_protocol::utils::sync::LazyLock;
22use miden_protocol::{Felt, Word};
23
24use crate::account::account_component_code;
25use crate::procedure_root;
26
27// BASIC CONSTANT FEE POLICY
28// ================================================================================================
29
30account_component_code!(
31 BASIC_CONSTANT_FEE_POLICY_CODE,
32 "miden-standards-fees-policies-basic-constant-fee.masp"
33);
34
35// PROCEDURE ROOTS
36// ================================================================================================
37
38/// MASL library namespace used for procedure-root lookups. Distinct from
39/// [`BasicConstantFeePolicy::NAME`], which mirrors the standards-side MASM module path.
40const BASIC_CONSTANT_FEE_LIBRARY_PATH: &str =
41 "miden::standards::components::fees::policies::basic_constant_fee";
42
43procedure_root!(
44 BASIC_CONSTANT_FEE_POLICY_ROOT,
45 BASIC_CONSTANT_FEE_LIBRARY_PATH,
46 BasicConstantFeePolicy::PROC_NAME,
47 BasicConstantFeePolicy::code()
48);
49
50static FEE_SCHEDULE_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
51 StorageSlotName::new("miden::standards::fees::policies::basic_constant_fee::fee_schedule")
52 .expect("storage slot name should be valid")
53});
54
55/// Set-marker element of a fee schedule entry, distinguishing scheduled entries (including
56/// explicit 0 fees) from unset keys: storage maps prune zero-word values and return the zero
57/// word for unset keys, so a scheduled entry must be a non-zero word. The MASM
58/// `compute_note_fee` counterpart asserts this element equals 1 (unset keys read as the zero
59/// word, whose marker element is 0) and strips it before returning the fee asset value.
60const FEE_SCHEDULE_ENTRY_MARKER: Felt = Felt::ONE;
61
62/// Encodes a fee as a fee schedule map entry: the asset value word with the set-marker as the
63/// last element, i.e. `[fee_amount, 0, 0, 1]`.
64fn fee_schedule_entry(fee: AssetAmount) -> Word {
65 let mut entry = fee.to_word();
66 entry[3] = FEE_SCHEDULE_ENTRY_MARKER;
67 entry
68}
69
70/// The `basic_constant_fee` fee policy account component.
71///
72/// This is a simple baseline constant fee policy: the fee depends only on the note's script root.
73/// More sophisticated constant fee policies can be derived from it by swapping the lookup-key
74/// computation of its `compute_note_fee` procedure, which yields a policy with a new root. A
75/// derived policy must also rename its `fee_schedule` storage slot, since two components
76/// installed on the same account cannot share a slot name.
77///
78/// Register with a [`crate::account::fees::FeePolicyManager`], whose allowed fee-policies map then
79/// includes [`BasicConstantFeePolicy::root`]. When active, `estimate_note_fee` dispatches to this
80/// policy's `compute_note_fee` procedure, which returns the fee as a fee asset (asset ID and
81/// value words): the amount is looked up in the fee schedule under the note's script root
82/// (recovered from the note's recipient via the advice provider), and note scripts without a
83/// schedule entry abort fee estimation. To make a note script free, schedule an explicit 0 fee
84/// for it via [`BasicConstantFeePolicy::with_fee`]. The remaining note parameters, including the
85/// timeframe and priority, are ignored by this policy. The fee asset ID is read from the
86/// fee-policy storage, so the fee is always charged in the configured asset and the policy
87/// requires an [`AuthNetworkAccount`][crate::account::auth::AuthNetworkAccount] component on the
88/// same account.
89///
90/// ## Storage layout
91///
92/// - [`Self::fee_schedule_slot_name`] map slot: `NOTE_SCRIPT_ROOT => [fee_amount, 0, 0, 1]`, where
93/// the last element is a set-marker distinguishing scheduled entries from unset keys.
94#[derive(Debug, Clone, Default)]
95pub struct BasicConstantFeePolicy {
96 /// The fee charged per note script root.
97 fee_schedule: BTreeMap<NoteScriptRoot, AssetAmount>,
98}
99
100impl BasicConstantFeePolicy {
101 // CONSTANTS
102 // --------------------------------------------------------------------------------------------
103
104 /// The name of the component.
105 pub const NAME: &'static str = "miden::standards::fees::policies::basic_constant_fee";
106
107 pub(crate) const PROC_NAME: &str = "compute_note_fee";
108
109 /// Returns the canonical [`AccountComponentName`] of this component.
110 pub const fn name() -> AccountComponentName {
111 AccountComponentName::from_static_str(Self::NAME)
112 }
113
114 // CONSTRUCTORS
115 // --------------------------------------------------------------------------------------------
116
117 /// Creates a new `basic_constant_fee` fee policy with an empty fee schedule, charging fees in
118 /// the fungible asset its [`crate::account::fees::FeePolicyManager`] is configured with.
119 pub fn new() -> Self {
120 Self { fee_schedule: BTreeMap::new() }
121 }
122
123 /// Sets the fee for notes with the given script root, replacing any previous entry.
124 ///
125 /// The schedule stores bare amounts: the fee must be denominated in the fee asset of the
126 /// [`FeePolicyManager`](crate::account::fees::FeePolicyManager) this policy is registered
127 /// with, which is the asset the policy charges it in.
128 ///
129 /// Scheduling an explicit fee of 0 makes notes with this script root free; script roots
130 /// without a schedule entry abort fee estimation.
131 #[must_use]
132 pub fn with_fee(mut self, script_root: NoteScriptRoot, fee: AssetAmount) -> Self {
133 self.fee_schedule.insert(script_root, fee);
134 self
135 }
136
137 /// Extends the fee schedule with the given `(script_root, fee)` entries, replacing any
138 /// previous entries. See [`Self::with_fee`] for the fee denomination requirement.
139 #[must_use]
140 pub fn with_fees(
141 mut self,
142 entries: impl IntoIterator<Item = (NoteScriptRoot, AssetAmount)>,
143 ) -> Self {
144 for (script_root, fee) in entries {
145 self = self.with_fee(script_root, fee);
146 }
147 self
148 }
149
150 // PUBLIC ACCESSORS
151 // --------------------------------------------------------------------------------------------
152
153 /// Returns the [`AccountComponentCode`] of this component.
154 pub fn code() -> &'static AccountComponentCode {
155 &BASIC_CONSTANT_FEE_POLICY_CODE
156 }
157
158 /// Returns the procedure root of the `compute_note_fee` fee policy procedure.
159 pub fn root() -> AccountProcedureRoot {
160 *BASIC_CONSTANT_FEE_POLICY_ROOT
161 }
162
163 /// Returns the [`StorageSlotName`] of the slot holding the fee schedule map.
164 pub fn fee_schedule_slot_name() -> &'static StorageSlotName {
165 &FEE_SCHEDULE_SLOT_NAME
166 }
167
168 /// Returns the fee charged per note script root.
169 pub fn fee_schedule(&self) -> &BTreeMap<NoteScriptRoot, AssetAmount> {
170 &self.fee_schedule
171 }
172
173 /// Returns the [`AccountComponentMetadata`] for this component.
174 pub fn component_metadata() -> AccountComponentMetadata {
175 let storage_schema = StorageSchema::new([(
176 Self::fee_schedule_slot_name().clone(),
177 StorageSlotSchema::map(
178 "Fee charged per note script root",
179 SchemaType::native_word(),
180 SchemaType::native_word(),
181 ),
182 )])
183 .expect("storage schema should be valid");
184
185 AccountComponentMetadata::new(Self::NAME)
186 .with_description(
187 "`basic_constant_fee` fee policy charging a constant per-note-script fee",
188 )
189 .with_storage_schema(storage_schema)
190 }
191}
192
193impl From<BasicConstantFeePolicy> for AccountComponent {
194 fn from(policy: BasicConstantFeePolicy) -> Self {
195 let entries = policy
196 .fee_schedule
197 .into_iter()
198 .map(|(root, fee)| (StorageMapKey::new(root.as_word()), fee_schedule_entry(fee)));
199 let fee_schedule_map = StorageMap::with_entries(entries)
200 .expect("fee schedule entries should produce a valid storage map");
201 let fee_schedule_slot = StorageSlot::with_map(
202 BasicConstantFeePolicy::fee_schedule_slot_name().clone(),
203 fee_schedule_map,
204 );
205
206 AccountComponent::new(
207 BasicConstantFeePolicy::code().clone(),
208 vec![fee_schedule_slot],
209 BasicConstantFeePolicy::component_metadata(),
210 )
211 .expect(
212 "`basic_constant_fee` fee policy component should satisfy the requirements of a valid account component",
213 )
214 }
215}
216
217// TESTS
218// ================================================================================================
219
220#[cfg(test)]
221mod tests {
222 use miden_protocol::account::StorageSlotContent;
223
224 use super::*;
225
226 /// Check that the policy's storage slot contains the fee schedule entries.
227 #[test]
228 fn storage_slots_contain_expected_entries() -> anyhow::Result<()> {
229 let script_root = NoteScriptRoot::from_array([1, 2, 3, 4]);
230 let fee = AssetAmount::new(500)?;
231 let free_script_root = NoteScriptRoot::from_array([5, 6, 7, 8]);
232
233 // Seed an outdated entry and overwrite it via `with_fees`, so the storage assertions
234 // below also cover the batch method's extend-and-replace contract.
235 let policy = BasicConstantFeePolicy::new()
236 .with_fee(script_root, AssetAmount::new(100)?)
237 .with_fees([(script_root, fee), (free_script_root, AssetAmount::ZERO)]);
238
239 let component = AccountComponent::from(policy);
240 let slot = component
241 .storage_slots()
242 .iter()
243 .find(|slot| slot.name() == BasicConstantFeePolicy::fee_schedule_slot_name())
244 .expect("fee schedule slot should exist");
245
246 let StorageSlotContent::Map(map) = slot.content() else {
247 panic!("fee schedule slot must be a map");
248 };
249 assert_eq!(
250 map.get(&StorageMapKey::new(script_root.as_word())),
251 Word::new([Felt::new(500)?, Felt::ZERO, Felt::ZERO, Felt::ONE]),
252 "the fee entry should be stored as an asset value word with the set-marker"
253 );
254 assert_eq!(
255 map.get(&StorageMapKey::new(free_script_root.as_word())),
256 Word::new([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]),
257 "an explicit 0-fee entry should survive as a non-zero word"
258 );
259
260 Ok(())
261 }
262}