miden_standards/note/
constant_fee_policy_config.rs1use alloc::vec::Vec;
2
3use miden_protocol::account::AccountId;
4use miden_protocol::assembly::Path;
5use miden_protocol::asset::FungibleAsset;
6use miden_protocol::crypto::rand::FeltRng;
7use miden_protocol::errors::NoteError;
8use miden_protocol::note::{
9 Note,
10 NoteAssets,
11 NoteAttachment,
12 NoteAttachments,
13 NoteRecipient,
14 NoteScript,
15 NoteScriptRoot,
16 NoteStorage,
17 NoteTag,
18 NoteType,
19 PartialNoteMetadata,
20};
21use miden_protocol::utils::sync::LazyLock;
22use miden_protocol::{Felt, Word};
23
24use crate::StandardsLib;
25use crate::note::NetworkAccountTarget;
26use crate::note::costs::{CONSTANT_FEE_POLICY_CONFIG_CONSUMPTION_CYCLES, NoteConsumptionCost};
27
28const CONSTANT_FEE_POLICY_CONFIG_SCRIPT_PATH: &str =
33 "::miden::standards::notes::constant_fee_policy_config::main";
34
35static CONSTANT_FEE_POLICY_CONFIG_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
37 let standards_lib = StandardsLib::default();
38 let path = Path::new(CONSTANT_FEE_POLICY_CONFIG_SCRIPT_PATH);
39 NoteScript::from_package_reference(standards_lib.as_ref(), path)
40 .expect("Standards library contains CONSTANT_FEE_POLICY_CONFIG note script procedure")
41});
42
43#[derive(Debug, Clone)]
88pub struct ConstantFeePolicyConfigNote {
89 sender: AccountId,
90 target: AccountId,
91 note_script_root: NoteScriptRoot,
92 fee_asset: FungibleAsset,
93 serial_number: Word,
94 attachments: NoteAttachments,
95}
96
97#[bon::bon]
98impl ConstantFeePolicyConfigNote {
99 #[builder]
111 pub fn new(
112 #[builder(field)] mut attachments: Vec<NoteAttachment>,
113 sender: AccountId,
114 target: AccountId,
115 note_script_root: NoteScriptRoot,
116 fee_asset: FungibleAsset,
117 serial_number: Word,
118 ) -> Result<Self, NoteError> {
119 NetworkAccountTarget::ensure_presence(&mut attachments, target).map_err(|err| {
122 NoteError::other_with_source("failed to bind the note to its target account", err)
123 })?;
124
125 let attachments = NoteAttachments::new(attachments)?;
126
127 Ok(Self {
128 sender,
129 target,
130 note_script_root,
131 fee_asset,
132 serial_number,
133 attachments,
134 })
135 }
136}
137
138impl ConstantFeePolicyConfigNote {
139 pub const NUM_STORAGE_ITEMS: usize = 12;
147
148 pub fn script() -> NoteScript {
153 CONSTANT_FEE_POLICY_CONFIG_SCRIPT.clone()
154 }
155
156 pub fn script_root() -> NoteScriptRoot {
158 CONSTANT_FEE_POLICY_CONFIG_SCRIPT.root()
159 }
160
161 pub fn sender(&self) -> AccountId {
163 self.sender
164 }
165
166 pub fn account(&self) -> AccountId {
169 self.target
170 }
171
172 pub fn note_script_root(&self) -> NoteScriptRoot {
174 self.note_script_root
175 }
176
177 pub fn fee_asset(&self) -> FungibleAsset {
179 self.fee_asset
180 }
181
182 pub fn serial_number(&self) -> Word {
184 self.serial_number
185 }
186
187 pub fn attachments(&self) -> &NoteAttachments {
189 &self.attachments
190 }
191
192 fn to_storage_values(&self) -> Vec<Felt> {
198 let mut values = Vec::with_capacity(Self::NUM_STORAGE_ITEMS);
199 values.extend_from_slice(self.note_script_root.as_word().as_elements());
200 values.extend_from_slice(self.fee_asset.to_id_word().as_elements());
201 values.extend_from_slice(self.fee_asset.to_value_word().as_elements());
202 values
203 }
204}
205
206impl<S: constant_fee_policy_config_note_builder::State> ConstantFeePolicyConfigNoteBuilder<S> {
210 pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
212 self.attachments.push(attachment.into());
213 self
214 }
215
216 pub fn attachments(
218 mut self,
219 attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
220 ) -> Self {
221 self.attachments.extend(attachments.into_iter().map(Into::into));
222 self
223 }
224}
225
226impl<S: constant_fee_policy_config_note_builder::State> ConstantFeePolicyConfigNoteBuilder<S>
227where
228 S::SerialNumber: constant_fee_policy_config_note_builder::IsUnset,
229{
230 pub fn generate_serial_number(
232 self,
233 rng: &mut impl FeltRng,
234 ) -> ConstantFeePolicyConfigNoteBuilder<
235 constant_fee_policy_config_note_builder::SetSerialNumber<S>,
236 > {
237 self.serial_number(rng.draw_word())
238 }
239}
240
241impl From<ConstantFeePolicyConfigNote> for Note {
245 fn from(note: ConstantFeePolicyConfigNote) -> Self {
246 let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
249 .with_tag(NoteTag::with_account_target(note.target));
250 let storage = NoteStorage::new(note.to_storage_values())
251 .expect("number of storage items should not exceed max storage items");
252 let recipient =
253 NoteRecipient::new(note.serial_number, ConstantFeePolicyConfigNote::script(), storage);
254
255 Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
256 }
257}
258
259impl NoteConsumptionCost for ConstantFeePolicyConfigNote {
263 fn consumption_cycles() -> u32 {
264 CONSTANT_FEE_POLICY_CONFIG_CONSUMPTION_CYCLES
265 }
266}
267
268#[cfg(test)]
272mod tests {
273 use alloc::vec::Vec;
274
275 use assert_matches::assert_matches;
276 use miden_protocol::account::AccountType;
277 use miden_protocol::crypto::rand::RandomCoin;
278 use miden_protocol::note::NoteAttachmentScheme;
279 use miden_protocol::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
280
281 use super::*;
282 use crate::note::{NetworkAccountTargetError, NoteExecutionHint};
283
284 fn account_id(seed: u8) -> AccountId {
285 AccountId::builder()
286 .account_type(AccountType::Public)
287 .build_with_seed([seed; 32])
288 }
289
290 fn note_root(seed: u32) -> NoteScriptRoot {
291 NoteScriptRoot::from_array([seed, seed + 1, seed + 2, seed + 3])
292 }
293
294 fn fee_asset(amount: u64) -> FungibleAsset {
295 FungibleAsset::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(), amount).unwrap()
296 }
297
298 #[test]
300 fn builder_builds_constant_fee_policy_config_note() {
301 let mut rng = RandomCoin::new(Word::empty());
302 let account = account_id(1);
303 let sender = account_id(2);
304
305 let note = ConstantFeePolicyConfigNote::builder()
306 .sender(sender)
307 .target(account)
308 .note_script_root(note_root(10))
309 .fee_asset(fee_asset(500))
310 .generate_serial_number(&mut rng)
311 .build()
312 .unwrap();
313
314 assert_eq!(note.sender(), sender);
315 assert_eq!(note.account(), account);
316
317 let note = Note::from(note);
318 assert_eq!(note.metadata().note_type(), NoteType::Public);
319 assert_eq!(note.metadata().tag(), NoteTag::with_account_target(account));
320 assert_eq!(note.assets().num_assets(), 0);
321 }
322
323 #[test]
326 fn note_is_bound_to_target_account() {
327 let account = account_id(1);
328 let note = ConstantFeePolicyConfigNote::builder()
329 .sender(account_id(2))
330 .target(account)
331 .note_script_root(note_root(10))
332 .fee_asset(fee_asset(500))
333 .serial_number(Word::empty())
334 .build()
335 .unwrap();
336
337 let built = Note::from(note);
338 let target = NetworkAccountTarget::try_from(built.attachments())
339 .expect("note should carry a network account target attachment");
340 assert_eq!(target.target_id(), account);
341 }
342
343 #[test]
346 fn caller_supplied_target_for_other_account_is_rejected() {
347 let rogue_target =
348 NetworkAccountTarget::new(account_id(3), NoteExecutionHint::Always).unwrap();
349
350 let err = ConstantFeePolicyConfigNote::builder()
351 .sender(account_id(2))
352 .target(account_id(1))
353 .note_script_root(note_root(10))
354 .fee_asset(fee_asset(500))
355 .serial_number(Word::empty())
356 .attachment(rogue_target)
357 .build()
358 .unwrap_err();
359
360 assert_matches!(err, NoteError::Other { source, .. } => {
361 assert_matches!(
362 *source.unwrap().downcast().unwrap(),
363 NetworkAccountTargetError::TargetMismatch { .. }
364 )
365 });
366 }
367
368 #[test]
371 fn private_target_account_is_rejected() {
372 let private_account =
373 AccountId::builder().account_type(AccountType::Private).build_with_seed([9; 32]);
374
375 let err = ConstantFeePolicyConfigNote::builder()
376 .sender(account_id(2))
377 .target(private_account)
378 .note_script_root(note_root(10))
379 .fee_asset(fee_asset(500))
380 .serial_number(Word::empty())
381 .build()
382 .unwrap_err();
383
384 assert_matches!(err, NoteError::Other { source, .. } => {
385 assert_matches!(
386 *source.unwrap().downcast().unwrap(),
387 NetworkAccountTargetError::TargetNotPublic { .. }
388 )
389 });
390 }
391
392 #[test]
395 fn caller_attachments_beyond_limit_are_rejected() {
396 let mut builder = ConstantFeePolicyConfigNote::builder()
397 .sender(account_id(2))
398 .target(account_id(1))
399 .note_script_root(note_root(10))
400 .fee_asset(fee_asset(500))
401 .serial_number(Word::empty());
402 for scheme in 0..NoteAttachments::MAX_COUNT as u16 {
403 let extra = NoteAttachment::with_word(
404 NoteAttachmentScheme::new(64 + scheme).unwrap(),
405 Word::empty(),
406 );
407 builder = builder.attachment(extra);
408 }
409
410 assert!(matches!(builder.build(), Err(NoteError::TooManyAttachments(_))));
411 }
412
413 #[test]
415 fn storage_layout() {
416 let root = note_root(10);
417 let asset = fee_asset(777);
418
419 let note = ConstantFeePolicyConfigNote::builder()
420 .sender(account_id(2))
421 .target(account_id(1))
422 .note_script_root(root)
423 .fee_asset(asset)
424 .serial_number(Word::empty())
425 .build()
426 .unwrap();
427
428 let built = Note::from(note);
429 let mut expected = Vec::from(root.as_word().as_elements());
430 expected.extend_from_slice(asset.to_id_word().as_elements());
431 expected.extend_from_slice(asset.to_value_word().as_elements());
432 assert_eq!(built.storage().items(), expected.as_slice());
433 assert_eq!(built.storage().items().len(), ConstantFeePolicyConfigNote::NUM_STORAGE_ITEMS);
434 }
435
436 #[test]
439 fn script_root_is_registered_standard_note() {
440 use crate::note::StandardNote;
441
442 let standard = StandardNote::from_script_root(ConstantFeePolicyConfigNote::script_root())
443 .expect("config note script root should be a registered standard note");
444 assert_eq!(standard.name(), "CONSTANT_FEE_POLICY_CONFIG");
445 }
446}