1use alloc::vec::Vec;
2
3use miden_protocol::account::AccountId;
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::utils::sync::LazyLock;
21use miden_protocol::{Felt, Word};
22
23use crate::StandardsLib;
24use crate::note::costs::{NoteConsumptionCost, OWNER_CONFIG_CONSUMPTION_CYCLES};
25use crate::note::{AccountTargetNetworkNote, NetworkAccountTarget};
26
27const OWNER_CONFIG_SCRIPT_PATH: &str = "::miden::standards::notes::owner_config::main";
32
33static OWNER_CONFIG_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
35 let standards_lib = StandardsLib::default();
36 let path = Path::new(OWNER_CONFIG_SCRIPT_PATH);
37 NoteScript::from_package_reference(standards_lib.as_ref(), path)
38 .expect("Standards library contains OWNER_CONFIG note script procedure")
39});
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum OwnerConfig {
53 TransferOwnership { new_owner: Option<AccountId> },
57 AcceptOwnership,
59 RenounceOwnership,
62}
63
64impl OwnerConfig {
65 const SELECTOR_TRANSFER_OWNERSHIP: u8 = 0;
71 const SELECTOR_ACCEPT_OWNERSHIP: u8 = 1;
72 const SELECTOR_RENOUNCE_OWNERSHIP: u8 = 2;
73
74 fn to_storage_values(self) -> Vec<Felt> {
76 match self {
77 OwnerConfig::TransferOwnership { new_owner } => {
78 let (suffix, prefix) = match new_owner {
81 Some(id) => (id.suffix(), id.prefix().as_felt()),
82 None => (Felt::ZERO, Felt::ZERO),
83 };
84 vec![Felt::from(Self::SELECTOR_TRANSFER_OWNERSHIP), suffix, prefix]
85 },
86 OwnerConfig::AcceptOwnership => {
87 vec![Felt::from(Self::SELECTOR_ACCEPT_OWNERSHIP)]
88 },
89 OwnerConfig::RenounceOwnership => {
90 vec![Felt::from(Self::SELECTOR_RENOUNCE_OWNERSHIP)]
91 },
92 }
93 }
94}
95
96impl From<OwnerConfig> for NoteStorage {
97 fn from(config: OwnerConfig) -> Self {
98 NoteStorage::new(config.to_storage_values())
99 .expect("number of storage items should not exceed max storage items")
100 }
101}
102
103#[derive(Debug, Clone)]
128pub struct OwnerConfigNote {
129 sender: AccountId,
130 target: AccountId,
131 config: OwnerConfig,
132 serial_number: Word,
133 attachments: NoteAttachments,
134}
135
136#[bon::bon]
137impl OwnerConfigNote {
138 #[builder]
152 pub fn new(
153 #[builder(field)] mut attachments: Vec<NoteAttachment>,
154 sender: AccountId,
155 target: AccountId,
156 config: OwnerConfig,
157 serial_number: Word,
158 ) -> Result<Self, NoteError> {
159 NetworkAccountTarget::ensure_presence(&mut attachments, target).map_err(|err| {
162 NoteError::other_with_source(
163 "failed to bind the OwnerConfig note to its target account",
164 err,
165 )
166 })?;
167 let attachments = NoteAttachments::new(attachments)?;
168
169 Ok(Self {
170 sender,
171 target,
172 config,
173 serial_number,
174 attachments,
175 })
176 }
177}
178
179impl OwnerConfigNote {
180 pub const MAX_NUM_STORAGE_ITEMS: usize = 3;
188
189 pub fn script() -> NoteScript {
194 OWNER_CONFIG_SCRIPT.clone()
195 }
196
197 pub fn script_root() -> NoteScriptRoot {
199 OWNER_CONFIG_SCRIPT.root()
200 }
201
202 pub fn sender(&self) -> AccountId {
204 self.sender
205 }
206
207 pub fn account(&self) -> AccountId {
209 self.target
210 }
211
212 pub fn config(&self) -> OwnerConfig {
214 self.config
215 }
216
217 pub fn serial_number(&self) -> Word {
219 self.serial_number
220 }
221
222 pub fn attachments(&self) -> &NoteAttachments {
225 &self.attachments
226 }
227}
228
229impl<S: owner_config_note_builder::State> OwnerConfigNoteBuilder<S> {
233 pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
235 self.attachments.push(attachment.into());
236 self
237 }
238
239 pub fn attachments(
241 mut self,
242 attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
243 ) -> Self {
244 self.attachments.extend(attachments.into_iter().map(Into::into));
245 self
246 }
247}
248
249impl<S: owner_config_note_builder::State> OwnerConfigNoteBuilder<S>
250where
251 S::SerialNumber: owner_config_note_builder::IsUnset,
252{
253 pub fn generate_serial_number(
255 self,
256 rng: &mut impl FeltRng,
257 ) -> OwnerConfigNoteBuilder<owner_config_note_builder::SetSerialNumber<S>> {
258 self.serial_number(rng.draw_word())
259 }
260}
261
262impl From<OwnerConfigNote> for Note {
266 fn from(note: OwnerConfigNote) -> Self {
267 let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
270 .with_tag(NoteTag::with_account_target(note.target));
271 let recipient = NoteRecipient::new(
272 note.serial_number,
273 OwnerConfigNote::script(),
274 NoteStorage::from(note.config),
275 );
276 Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
277 }
278}
279
280impl From<OwnerConfigNote> for AccountTargetNetworkNote {
281 fn from(note: OwnerConfigNote) -> Self {
282 AccountTargetNetworkNote::new(Note::from(note))
283 .expect("OwnerConfig note is public and carries a network account target attachment")
284 }
285}
286
287impl NoteConsumptionCost for OwnerConfigNote {
291 fn consumption_cycles() -> u32 {
292 OWNER_CONFIG_CONSUMPTION_CYCLES
293 }
294}
295
296#[cfg(test)]
300mod tests {
301 use assert_matches::assert_matches;
302 use miden_protocol::account::AccountType;
303 use miden_protocol::crypto::rand::RandomCoin;
304 use miden_protocol::note::NoteAttachmentScheme;
305
306 use super::*;
307 use crate::note::{NetworkAccountTargetError, NetworkNoteExt, NoteExecutionHint};
308
309 fn account_id(seed: u8) -> AccountId {
310 typed_account_id(seed, AccountType::Public)
311 }
312
313 fn typed_account_id(seed: u8, account_type: AccountType) -> AccountId {
314 AccountId::builder().account_type(account_type).build_with_seed([seed; 32])
315 }
316
317 #[test]
319 fn builder_builds_owner_config_note() {
320 let mut rng = RandomCoin::new(Word::empty());
321 let managed = account_id(1);
322 let owner = account_id(2);
323 let new_owner = account_id(3);
324
325 let note = OwnerConfigNote::builder()
326 .sender(owner)
327 .target(managed)
328 .config(OwnerConfig::TransferOwnership { new_owner: Some(new_owner) })
329 .generate_serial_number(&mut rng)
330 .build()
331 .unwrap();
332
333 assert_eq!(note.sender(), owner);
334 assert_eq!(note.account(), managed);
335
336 let note = Note::from(note);
337 assert_eq!(note.metadata().note_type(), NoteType::Public);
338 assert_eq!(note.metadata().tag(), NoteTag::with_account_target(managed));
339 assert_eq!(note.assets().num_assets(), 0);
340 }
341
342 #[test]
345 fn builder_attaches_network_target() {
346 let mut rng = RandomCoin::new(Word::empty());
347 let managed = account_id(1);
348
349 let note = OwnerConfigNote::builder()
350 .sender(account_id(2))
351 .target(managed)
352 .config(OwnerConfig::AcceptOwnership)
353 .generate_serial_number(&mut rng)
354 .build()
355 .unwrap();
356
357 assert_eq!(note.attachments().num_attachments(), 1);
358
359 let network_note = AccountTargetNetworkNote::from(note);
360 assert_eq!(network_note.target_account_id(), managed);
361 assert_eq!(network_note.execution_hint(), NoteExecutionHint::Always);
362 assert!(network_note.as_note().is_network_note());
363 }
364
365 #[test]
367 fn builder_keeps_caller_attachments() {
368 let mut rng = RandomCoin::new(Word::empty());
369 let managed = account_id(1);
370 let custom_scheme = NoteAttachmentScheme::new(64).unwrap();
371 let custom = NoteAttachment::with_word(custom_scheme, Word::from([7u32, 0, 0, 0]));
372
373 let note = OwnerConfigNote::builder()
374 .attachment(custom.clone())
375 .sender(account_id(2))
376 .target(managed)
377 .config(OwnerConfig::AcceptOwnership)
378 .generate_serial_number(&mut rng)
379 .build()
380 .unwrap();
381
382 assert_eq!(note.attachments().num_attachments(), 2);
384 assert_eq!(note.attachments().get(0), Some(&custom));
385
386 let network_note = AccountTargetNetworkNote::from(note);
387 assert_eq!(network_note.target_account_id(), managed);
388 }
389
390 #[test]
393 fn builder_rejects_target_for_other_account() {
394 let mut rng = RandomCoin::new(Word::empty());
395 let rogue_target =
396 NetworkAccountTarget::new(account_id(3), NoteExecutionHint::None).unwrap();
397
398 let err = OwnerConfigNote::builder()
399 .attachment(rogue_target)
400 .sender(account_id(2))
401 .target(account_id(1))
402 .config(OwnerConfig::AcceptOwnership)
403 .generate_serial_number(&mut rng)
404 .build()
405 .unwrap_err();
406
407 assert_matches!(err, NoteError::Other { source, .. } => {
408 assert_matches!(
409 *source.unwrap().downcast().unwrap(),
410 NetworkAccountTargetError::TargetMismatch { .. }
411 )
412 });
413 }
414
415 #[test]
417 fn builder_rejects_non_public_account() {
418 let mut rng = RandomCoin::new(Word::empty());
419 let managed = typed_account_id(1, AccountType::Private);
420
421 let err = OwnerConfigNote::builder()
422 .sender(account_id(2))
423 .target(managed)
424 .config(OwnerConfig::AcceptOwnership)
425 .generate_serial_number(&mut rng)
426 .build()
427 .unwrap_err();
428
429 assert_matches!(err, NoteError::Other { source, .. } => {
430 assert_matches!(
431 *source.unwrap().downcast().unwrap(),
432 NetworkAccountTargetError::TargetNotPublic { .. }
433 )
434 });
435 }
436
437 #[test]
439 fn transfer_ownership_storage_layout() {
440 let new_owner = account_id(3);
441 let storage =
442 NoteStorage::from(OwnerConfig::TransferOwnership { new_owner: Some(new_owner) });
443
444 assert_eq!(
445 storage.items(),
446 &[
447 Felt::from(OwnerConfig::SELECTOR_TRANSFER_OWNERSHIP),
448 new_owner.suffix(),
449 new_owner.prefix().as_felt(),
450 ]
451 );
452 }
453
454 #[test]
456 fn cancel_transfer_ownership_storage_layout() {
457 let storage = NoteStorage::from(OwnerConfig::TransferOwnership { new_owner: None });
458
459 assert_eq!(
460 storage.items(),
461 &[Felt::from(OwnerConfig::SELECTOR_TRANSFER_OWNERSHIP), Felt::ZERO, Felt::ZERO]
462 );
463 }
464
465 #[test]
467 fn accept_and_renounce_storage_layout() {
468 let accept = NoteStorage::from(OwnerConfig::AcceptOwnership);
469 assert_eq!(accept.items(), &[Felt::from(OwnerConfig::SELECTOR_ACCEPT_OWNERSHIP)]);
470
471 let renounce = NoteStorage::from(OwnerConfig::RenounceOwnership);
472 assert_eq!(renounce.items(), &[Felt::from(OwnerConfig::SELECTOR_RENOUNCE_OWNERSHIP)]);
473 }
474}