1use alloc::vec::Vec;
2
3use miden_protocol::account::{AccountId, AccountProcedureRoot};
4use miden_protocol::assembly::Path;
5use miden_protocol::crypto::rand::FeltRng;
6use miden_protocol::errors::NoteError;
7use miden_protocol::note::{
8 Note,
9 NoteAssets,
10 NoteAttachment,
11 NoteAttachments,
12 NoteRecipient,
13 NoteScript,
14 NoteScriptRoot,
15 NoteStorage,
16 NoteTag,
17 NoteType,
18 PartialNoteMetadata,
19};
20use miden_protocol::transaction::TransactionScriptRoot;
21use miden_protocol::utils::sync::LazyLock;
22use miden_protocol::{Felt, Word};
23
24use crate::StandardsLib;
25use crate::note::NetworkAccountTarget;
26use crate::note::costs::{NETWORK_ACCOUNT_CONFIG_CONSUMPTION_CYCLES, NoteConsumptionCost};
27
28const NETWORK_ACCOUNT_CONFIG_SCRIPT_PATH: &str =
33 "::miden::standards::notes::network_account_config::main";
34
35static NETWORK_ACCOUNT_CONFIG_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
37 let standards_lib = StandardsLib::default();
38 let path = Path::new(NETWORK_ACCOUNT_CONFIG_SCRIPT_PATH);
39 NoteScript::from_package_reference(standards_lib.as_ref(), path)
40 .expect("Standards library contains NETWORK_ACCOUNT_CONFIG note script procedure")
41});
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum NetworkAccountConfig {
60 AddAllowedNoteScript { script_root: NoteScriptRoot },
62 RemoveAllowedNoteScript { script_root: NoteScriptRoot },
64 AddAllowedTxScript { script_root: TransactionScriptRoot },
66 RemoveAllowedTxScript { script_root: TransactionScriptRoot },
68 AddAllowedFeePolicy { policy_root: AccountProcedureRoot },
70 RemoveAllowedFeePolicy { policy_root: AccountProcedureRoot },
72}
73
74impl NetworkAccountConfig {
75 const SELECTOR_ADD_ALLOWED_NOTE_SCRIPT: u8 = 0;
81 const SELECTOR_REMOVE_ALLOWED_NOTE_SCRIPT: u8 = 1;
82 const SELECTOR_ADD_ALLOWED_TX_SCRIPT: u8 = 2;
83 const SELECTOR_REMOVE_ALLOWED_TX_SCRIPT: u8 = 3;
84 const SELECTOR_ADD_ALLOWED_FEE_POLICY: u8 = 4;
85 const SELECTOR_REMOVE_ALLOWED_FEE_POLICY: u8 = 5;
86
87 fn parts(self) -> (u8, Word) {
89 match self {
90 NetworkAccountConfig::AddAllowedNoteScript { script_root } => {
91 (Self::SELECTOR_ADD_ALLOWED_NOTE_SCRIPT, script_root.as_word())
92 },
93 NetworkAccountConfig::RemoveAllowedNoteScript { script_root } => {
94 (Self::SELECTOR_REMOVE_ALLOWED_NOTE_SCRIPT, script_root.as_word())
95 },
96 NetworkAccountConfig::AddAllowedTxScript { script_root } => {
97 (Self::SELECTOR_ADD_ALLOWED_TX_SCRIPT, script_root.as_word())
98 },
99 NetworkAccountConfig::RemoveAllowedTxScript { script_root } => {
100 (Self::SELECTOR_REMOVE_ALLOWED_TX_SCRIPT, script_root.as_word())
101 },
102 NetworkAccountConfig::AddAllowedFeePolicy { policy_root } => {
103 (Self::SELECTOR_ADD_ALLOWED_FEE_POLICY, policy_root.as_word())
104 },
105 NetworkAccountConfig::RemoveAllowedFeePolicy { policy_root } => {
106 (Self::SELECTOR_REMOVE_ALLOWED_FEE_POLICY, policy_root.as_word())
107 },
108 }
109 }
110
111 fn to_storage_values(self) -> Vec<Felt> {
113 let (selector, script_root) = self.parts();
114 let mut values = Vec::with_capacity(NetworkAccountConfigNote::NUM_STORAGE_ITEMS);
115 values.extend_from_slice(script_root.as_elements());
116 values.push(Felt::from(selector));
117 values
118 }
119}
120
121impl From<NetworkAccountConfig> for NoteStorage {
122 fn from(config: NetworkAccountConfig) -> Self {
123 NoteStorage::new(config.to_storage_values())
124 .expect("number of storage items should not exceed max storage items")
125 }
126}
127
128#[derive(Debug, Clone)]
147pub struct NetworkAccountConfigNote {
148 sender: AccountId,
149 target: AccountId,
150 config: NetworkAccountConfig,
151 serial_number: Word,
152 attachments: NoteAttachments,
153}
154
155#[bon::bon]
156impl NetworkAccountConfigNote {
157 #[builder]
170 pub fn new(
171 #[builder(field)] mut attachments: Vec<NoteAttachment>,
172 sender: AccountId,
173 target: AccountId,
174 config: NetworkAccountConfig,
175 serial_number: Word,
176 ) -> Result<Self, NoteError> {
177 NetworkAccountTarget::ensure_presence(&mut attachments, target).map_err(|err| {
179 NoteError::other_with_source("failed to bind the note to its target account", err)
180 })?;
181
182 let attachments = NoteAttachments::new(attachments)?;
183
184 Ok(Self {
185 sender,
186 target,
187 config,
188 serial_number,
189 attachments,
190 })
191 }
192}
193
194impl NetworkAccountConfigNote {
195 pub const NUM_STORAGE_ITEMS: usize = 5;
201
202 pub fn script() -> NoteScript {
207 NETWORK_ACCOUNT_CONFIG_SCRIPT.clone()
208 }
209
210 pub fn script_root() -> NoteScriptRoot {
212 NETWORK_ACCOUNT_CONFIG_SCRIPT.root()
213 }
214
215 pub fn sender(&self) -> AccountId {
217 self.sender
218 }
219
220 pub fn account(&self) -> AccountId {
222 self.target
223 }
224
225 pub fn config(&self) -> NetworkAccountConfig {
227 self.config
228 }
229
230 pub fn serial_number(&self) -> Word {
232 self.serial_number
233 }
234
235 pub fn attachments(&self) -> &NoteAttachments {
237 &self.attachments
238 }
239}
240
241impl<S: network_account_config_note_builder::State> NetworkAccountConfigNoteBuilder<S> {
245 pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
247 self.attachments.push(attachment.into());
248 self
249 }
250
251 pub fn attachments(
253 mut self,
254 attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
255 ) -> Self {
256 self.attachments.extend(attachments.into_iter().map(Into::into));
257 self
258 }
259}
260
261impl<S: network_account_config_note_builder::State> NetworkAccountConfigNoteBuilder<S>
262where
263 S::SerialNumber: network_account_config_note_builder::IsUnset,
264{
265 pub fn generate_serial_number(
267 self,
268 rng: &mut impl FeltRng,
269 ) -> NetworkAccountConfigNoteBuilder<network_account_config_note_builder::SetSerialNumber<S>>
270 {
271 self.serial_number(rng.draw_word())
272 }
273}
274
275impl From<NetworkAccountConfigNote> for Note {
279 fn from(note: NetworkAccountConfigNote) -> Self {
280 let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
283 .with_tag(NoteTag::with_account_target(note.target));
284 let recipient = NoteRecipient::new(
285 note.serial_number,
286 NetworkAccountConfigNote::script(),
287 NoteStorage::from(note.config),
288 );
289
290 Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
291 }
292}
293
294impl NoteConsumptionCost for NetworkAccountConfigNote {
298 fn consumption_cycles() -> u32 {
299 NETWORK_ACCOUNT_CONFIG_CONSUMPTION_CYCLES
300 }
301}
302
303#[cfg(test)]
307mod tests {
308 use alloc::vec::Vec;
309
310 use miden_protocol::account::AccountType;
311 use miden_protocol::crypto::rand::RandomCoin;
312
313 use super::*;
314
315 fn account_id(seed: u8) -> AccountId {
316 AccountId::builder()
317 .account_type(AccountType::Public)
318 .build_with_seed([seed; 32])
319 }
320
321 fn note_root(seed: u32) -> NoteScriptRoot {
322 NoteScriptRoot::from_array([seed, seed + 1, seed + 2, seed + 3])
323 }
324
325 fn tx_root(seed: u32) -> TransactionScriptRoot {
326 TransactionScriptRoot::from_raw(Word::from([seed, seed + 1, seed + 2, seed + 3]))
327 }
328
329 fn policy_root(seed: u32) -> AccountProcedureRoot {
330 AccountProcedureRoot::from_raw(Word::from([seed, seed + 1, seed + 2, seed + 3]))
331 }
332
333 #[test]
335 fn builder_builds_allowlist_config_note() {
336 let mut rng = RandomCoin::new(Word::empty());
337 let account = account_id(1);
338 let sender = account_id(2);
339
340 let note = NetworkAccountConfigNote::builder()
341 .sender(sender)
342 .target(account)
343 .config(NetworkAccountConfig::AddAllowedNoteScript { script_root: note_root(10) })
344 .generate_serial_number(&mut rng)
345 .build()
346 .unwrap();
347
348 assert_eq!(note.sender(), sender);
349 assert_eq!(note.account(), account);
350
351 let note = Note::from(note);
352 assert_eq!(note.metadata().note_type(), NoteType::Public);
353 assert_eq!(note.metadata().tag(), NoteTag::with_account_target(account));
354 assert_eq!(note.assets().num_assets(), 0);
355
356 let target = NetworkAccountTarget::try_from(note.attachments())
358 .expect("note must carry a network account target attachment");
359 assert_eq!(target.target_id(), account);
360 }
361
362 #[test]
364 fn storage_layout() {
365 let note_root = note_root(10);
366 let tx_root = tx_root(20);
367 let policy_root = policy_root(30);
368
369 let cases = [
370 (
371 NetworkAccountConfig::AddAllowedNoteScript { script_root: note_root },
372 NetworkAccountConfig::SELECTOR_ADD_ALLOWED_NOTE_SCRIPT,
373 note_root.as_word(),
374 ),
375 (
376 NetworkAccountConfig::RemoveAllowedNoteScript { script_root: note_root },
377 NetworkAccountConfig::SELECTOR_REMOVE_ALLOWED_NOTE_SCRIPT,
378 note_root.as_word(),
379 ),
380 (
381 NetworkAccountConfig::AddAllowedTxScript { script_root: tx_root },
382 NetworkAccountConfig::SELECTOR_ADD_ALLOWED_TX_SCRIPT,
383 tx_root.as_word(),
384 ),
385 (
386 NetworkAccountConfig::RemoveAllowedTxScript { script_root: tx_root },
387 NetworkAccountConfig::SELECTOR_REMOVE_ALLOWED_TX_SCRIPT,
388 tx_root.as_word(),
389 ),
390 (
391 NetworkAccountConfig::AddAllowedFeePolicy { policy_root },
392 NetworkAccountConfig::SELECTOR_ADD_ALLOWED_FEE_POLICY,
393 policy_root.as_word(),
394 ),
395 (
396 NetworkAccountConfig::RemoveAllowedFeePolicy { policy_root },
397 NetworkAccountConfig::SELECTOR_REMOVE_ALLOWED_FEE_POLICY,
398 policy_root.as_word(),
399 ),
400 ];
401
402 for (action, selector, root_word) in cases {
403 let storage = NoteStorage::from(action);
404 let mut expected = Vec::from(root_word.as_elements());
405 expected.push(Felt::from(selector));
406 assert_eq!(storage.items(), expected.as_slice());
407 assert_eq!(storage.items().len(), NetworkAccountConfigNote::NUM_STORAGE_ITEMS);
408 }
409 }
410
411 #[test]
414 fn script_root_is_registered_standard_note() {
415 use crate::note::StandardNote;
416
417 let standard = StandardNote::from_script_root(NetworkAccountConfigNote::script_root())
418 .expect("config note script root should be a registered standard note");
419 assert_eq!(standard.name(), "NETWORK_ACCOUNT_CONFIG");
420 }
421}