miden_standards/note/config/
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)]
92pub struct ConstantFeePolicyConfigNote {
93 sender: AccountId,
94 target: AccountId,
95 note_script_root: NoteScriptRoot,
96 fee_asset: FungibleAsset,
97 serial_number: Word,
98 attachments: NoteAttachments,
99}
100
101#[bon::bon]
102impl ConstantFeePolicyConfigNote {
103 #[builder]
115 pub fn new(
116 #[builder(field)] mut attachments: Vec<NoteAttachment>,
117 sender: AccountId,
118 target: AccountId,
119 note_script_root: NoteScriptRoot,
120 fee_asset: FungibleAsset,
121 serial_number: Word,
122 ) -> Result<Self, NoteError> {
123 NetworkAccountTarget::ensure_presence(&mut attachments, target).map_err(|err| {
126 NoteError::other_with_source("failed to bind the note to its target account", err)
127 })?;
128
129 let attachments = NoteAttachments::new(attachments)?;
130
131 Ok(Self {
132 sender,
133 target,
134 note_script_root,
135 fee_asset,
136 serial_number,
137 attachments,
138 })
139 }
140}
141
142impl ConstantFeePolicyConfigNote {
143 pub const NUM_STORAGE_ITEMS: usize = 12;
151
152 pub fn script() -> NoteScript {
157 CONSTANT_FEE_POLICY_CONFIG_SCRIPT.clone()
158 }
159
160 pub fn script_root() -> NoteScriptRoot {
162 CONSTANT_FEE_POLICY_CONFIG_SCRIPT.root()
163 }
164
165 pub fn sender(&self) -> AccountId {
167 self.sender
168 }
169
170 pub fn target(&self) -> AccountId {
173 self.target
174 }
175
176 pub fn note_script_root(&self) -> NoteScriptRoot {
178 self.note_script_root
179 }
180
181 pub fn fee_asset(&self) -> FungibleAsset {
183 self.fee_asset
184 }
185
186 pub fn serial_number(&self) -> Word {
188 self.serial_number
189 }
190
191 pub fn attachments(&self) -> &NoteAttachments {
193 &self.attachments
194 }
195
196 fn to_storage_values(&self) -> Vec<Felt> {
202 let mut values = Vec::with_capacity(Self::NUM_STORAGE_ITEMS);
203 values.extend_from_slice(self.note_script_root.as_word().as_elements());
204 values.extend_from_slice(self.fee_asset.to_id_word().as_elements());
205 values.extend_from_slice(self.fee_asset.to_value_word().as_elements());
206 values
207 }
208}
209
210impl<S: constant_fee_policy_config_note_builder::State> ConstantFeePolicyConfigNoteBuilder<S> {
214 pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
216 self.attachments.push(attachment.into());
217 self
218 }
219
220 pub fn attachments(
222 mut self,
223 attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
224 ) -> Self {
225 self.attachments.extend(attachments.into_iter().map(Into::into));
226 self
227 }
228}
229
230impl<S: constant_fee_policy_config_note_builder::State> ConstantFeePolicyConfigNoteBuilder<S>
231where
232 S::SerialNumber: constant_fee_policy_config_note_builder::IsUnset,
233{
234 pub fn generate_serial_number(
236 self,
237 rng: &mut impl FeltRng,
238 ) -> ConstantFeePolicyConfigNoteBuilder<
239 constant_fee_policy_config_note_builder::SetSerialNumber<S>,
240 > {
241 self.serial_number(rng.draw_word())
242 }
243}
244
245impl From<ConstantFeePolicyConfigNote> for Note {
249 fn from(note: ConstantFeePolicyConfigNote) -> Self {
250 let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
253 .with_tag(NoteTag::with_account_target(note.target));
254 let storage = NoteStorage::new(note.to_storage_values())
255 .expect("number of storage items should not exceed max storage items");
256 let recipient =
257 NoteRecipient::new(note.serial_number, ConstantFeePolicyConfigNote::script(), storage);
258
259 Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
260 }
261}
262
263impl NoteConsumptionCost for ConstantFeePolicyConfigNote {
267 fn consumption_cycles() -> u32 {
268 CONSTANT_FEE_POLICY_CONFIG_CONSUMPTION_CYCLES
269 }
270}
271
272#[cfg(test)]
276mod tests {
277 use alloc::vec::Vec;
278
279 use assert_matches::assert_matches;
280 use miden_protocol::account::AccountType;
281 use miden_protocol::crypto::rand::RandomCoin;
282 use miden_protocol::note::NoteAttachmentScheme;
283 use miden_protocol::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
284
285 use super::*;
286 use crate::note::{NetworkAccountTargetError, NoteExecutionHint};
287
288 fn account_id(seed: u8) -> AccountId {
289 AccountId::builder()
290 .account_type(AccountType::Public)
291 .build_with_seed([seed; 32])
292 }
293
294 fn note_root(seed: u32) -> NoteScriptRoot {
295 NoteScriptRoot::from_array([seed, seed + 1, seed + 2, seed + 3])
296 }
297
298 fn fee_asset(amount: u64) -> FungibleAsset {
299 FungibleAsset::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(), amount).unwrap()
300 }
301
302 #[test]
304 fn builder_builds_constant_fee_policy_config_note() {
305 let mut rng = RandomCoin::new(Word::empty());
306 let account = account_id(1);
307 let sender = account_id(2);
308
309 let note = ConstantFeePolicyConfigNote::builder()
310 .sender(sender)
311 .target(account)
312 .note_script_root(note_root(10))
313 .fee_asset(fee_asset(500))
314 .generate_serial_number(&mut rng)
315 .build()
316 .unwrap();
317
318 assert_eq!(note.sender(), sender);
319 assert_eq!(note.target(), account);
320
321 let note = Note::from(note);
322 assert_eq!(note.metadata().note_type(), NoteType::Public);
323 assert_eq!(note.metadata().tag(), NoteTag::with_account_target(account));
324 assert_eq!(note.assets().num_assets(), 0);
325 }
326
327 #[test]
330 fn note_is_bound_to_target_account() {
331 let account = account_id(1);
332 let note = ConstantFeePolicyConfigNote::builder()
333 .sender(account_id(2))
334 .target(account)
335 .note_script_root(note_root(10))
336 .fee_asset(fee_asset(500))
337 .serial_number(Word::empty())
338 .build()
339 .unwrap();
340
341 let built = Note::from(note);
342 let target = NetworkAccountTarget::try_from(built.attachments())
343 .expect("note should carry a network account target attachment");
344 assert_eq!(target.target_id(), account);
345 }
346
347 #[test]
350 fn caller_supplied_target_for_other_account_is_rejected() {
351 let rogue_target =
352 NetworkAccountTarget::new(account_id(3), NoteExecutionHint::Always).unwrap();
353
354 let err = ConstantFeePolicyConfigNote::builder()
355 .sender(account_id(2))
356 .target(account_id(1))
357 .note_script_root(note_root(10))
358 .fee_asset(fee_asset(500))
359 .serial_number(Word::empty())
360 .attachment(rogue_target)
361 .build()
362 .unwrap_err();
363
364 assert_matches!(err, NoteError::Other { source, .. } => {
365 assert_matches!(
366 *source.unwrap().downcast().unwrap(),
367 NetworkAccountTargetError::TargetMismatch { .. }
368 )
369 });
370 }
371
372 #[test]
375 fn private_target_account_is_rejected() {
376 let private_account =
377 AccountId::builder().account_type(AccountType::Private).build_with_seed([9; 32]);
378
379 let err = ConstantFeePolicyConfigNote::builder()
380 .sender(account_id(2))
381 .target(private_account)
382 .note_script_root(note_root(10))
383 .fee_asset(fee_asset(500))
384 .serial_number(Word::empty())
385 .build()
386 .unwrap_err();
387
388 assert_matches!(err, NoteError::Other { source, .. } => {
389 assert_matches!(
390 *source.unwrap().downcast().unwrap(),
391 NetworkAccountTargetError::TargetNotPublic { .. }
392 )
393 });
394 }
395
396 #[test]
399 fn caller_attachments_beyond_limit_are_rejected() {
400 let mut builder = ConstantFeePolicyConfigNote::builder()
401 .sender(account_id(2))
402 .target(account_id(1))
403 .note_script_root(note_root(10))
404 .fee_asset(fee_asset(500))
405 .serial_number(Word::empty());
406 for scheme in 0..NoteAttachments::MAX_COUNT as u16 {
407 let extra = NoteAttachment::with_word(
408 NoteAttachmentScheme::new(64 + scheme).unwrap(),
409 Word::empty(),
410 );
411 builder = builder.attachment(extra);
412 }
413
414 assert!(matches!(builder.build(), Err(NoteError::TooManyAttachments(_))));
415 }
416
417 #[test]
419 fn storage_layout() {
420 let root = note_root(10);
421 let asset = fee_asset(777);
422
423 let note = ConstantFeePolicyConfigNote::builder()
424 .sender(account_id(2))
425 .target(account_id(1))
426 .note_script_root(root)
427 .fee_asset(asset)
428 .serial_number(Word::empty())
429 .build()
430 .unwrap();
431
432 let built = Note::from(note);
433 let mut expected = Vec::from(root.as_word().as_elements());
434 expected.extend_from_slice(asset.to_id_word().as_elements());
435 expected.extend_from_slice(asset.to_value_word().as_elements());
436 assert_eq!(built.storage().items(), expected.as_slice());
437 assert_eq!(built.storage().items().len(), ConstantFeePolicyConfigNote::NUM_STORAGE_ITEMS);
438 }
439
440 #[test]
443 fn script_root_is_registered_standard_note() {
444 use crate::note::StandardNote;
445
446 let standard = StandardNote::from_script_root(ConstantFeePolicyConfigNote::script_root())
447 .expect("config note script root should be a registered standard note");
448 assert_eq!(standard.name(), "CONSTANT_FEE_POLICY_CONFIG");
449 }
450}