miden_standards/note/config/
owner_config.rs1use 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, NumStorageItems};
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 VARIANT_TRANSFER_OWNERSHIP: u8 = 0;
71 const VARIANT_ACCEPT_OWNERSHIP: u8 = 1;
72 const VARIANT_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::VARIANT_TRANSFER_OWNERSHIP), suffix, prefix]
85 },
86 OwnerConfig::AcceptOwnership => {
87 vec![Felt::from(Self::VARIANT_ACCEPT_OWNERSHIP)]
88 },
89 OwnerConfig::RenounceOwnership => {
90 vec![Felt::from(Self::VARIANT_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)]
131pub struct OwnerConfigNote {
132 sender: AccountId,
133 target: AccountId,
134 config: OwnerConfig,
135 serial_number: Word,
136 attachments: NoteAttachments,
137}
138
139#[bon::bon]
140impl OwnerConfigNote {
141 #[builder]
155 pub fn new(
156 #[builder(field)] mut attachments: Vec<NoteAttachment>,
157 sender: AccountId,
158 target: AccountId,
159 config: OwnerConfig,
160 serial_number: Word,
161 ) -> Result<Self, NoteError> {
162 NetworkAccountTarget::ensure_presence(&mut attachments, target).map_err(|err| {
165 NoteError::other_with_source(
166 "failed to bind the OwnerConfig note to its target account",
167 err,
168 )
169 })?;
170 let attachments = NoteAttachments::new(attachments)?;
171
172 Ok(Self {
173 sender,
174 target,
175 config,
176 serial_number,
177 attachments,
178 })
179 }
180}
181
182impl OwnerConfigNote {
183 pub const NUM_STORAGE_ITEMS: NumStorageItems =
192 NumStorageItems::AnyOf(&[NumStorageItems::Exact(1), NumStorageItems::Exact(3)]);
193
194 pub fn script() -> NoteScript {
199 OWNER_CONFIG_SCRIPT.clone()
200 }
201
202 pub fn script_root() -> NoteScriptRoot {
204 OWNER_CONFIG_SCRIPT.root()
205 }
206
207 pub fn sender(&self) -> AccountId {
209 self.sender
210 }
211
212 pub fn target(&self) -> AccountId {
214 self.target
215 }
216
217 pub fn config(&self) -> OwnerConfig {
219 self.config
220 }
221
222 pub fn serial_number(&self) -> Word {
224 self.serial_number
225 }
226
227 pub fn attachments(&self) -> &NoteAttachments {
230 &self.attachments
231 }
232}
233
234impl<S: owner_config_note_builder::State> OwnerConfigNoteBuilder<S> {
238 pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
240 self.attachments.push(attachment.into());
241 self
242 }
243
244 pub fn attachments(
246 mut self,
247 attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
248 ) -> Self {
249 self.attachments.extend(attachments.into_iter().map(Into::into));
250 self
251 }
252}
253
254impl<S: owner_config_note_builder::State> OwnerConfigNoteBuilder<S>
255where
256 S::SerialNumber: owner_config_note_builder::IsUnset,
257{
258 pub fn generate_serial_number(
260 self,
261 rng: &mut impl FeltRng,
262 ) -> OwnerConfigNoteBuilder<owner_config_note_builder::SetSerialNumber<S>> {
263 self.serial_number(rng.draw_word())
264 }
265}
266
267impl From<OwnerConfigNote> for Note {
271 fn from(note: OwnerConfigNote) -> Self {
272 let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
275 .with_tag(NoteTag::with_account_target(note.target));
276 let recipient = NoteRecipient::new(
277 note.serial_number,
278 OwnerConfigNote::script(),
279 NoteStorage::from(note.config),
280 );
281 Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
282 }
283}
284
285impl From<OwnerConfigNote> for AccountTargetNetworkNote {
286 fn from(note: OwnerConfigNote) -> Self {
287 AccountTargetNetworkNote::new(Note::from(note))
288 .expect("OwnerConfig note is public and carries a network account target attachment")
289 }
290}
291
292impl NoteConsumptionCost for OwnerConfigNote {
296 fn consumption_cycles() -> u32 {
297 OWNER_CONFIG_CONSUMPTION_CYCLES
298 }
299}
300
301#[cfg(test)]
305mod tests {
306 use assert_matches::assert_matches;
307 use miden_protocol::account::AccountType;
308 use miden_protocol::crypto::rand::RandomCoin;
309 use miden_protocol::note::NoteAttachmentScheme;
310
311 use super::*;
312 use crate::note::{NetworkAccountTargetError, NetworkNoteExt, NoteExecutionHint};
313
314 fn account_id(seed: u8) -> AccountId {
315 typed_account_id(seed, AccountType::Public)
316 }
317
318 fn typed_account_id(seed: u8, account_type: AccountType) -> AccountId {
319 AccountId::builder().account_type(account_type).build_with_seed([seed; 32])
320 }
321
322 #[test]
324 fn builder_builds_owner_config_note() {
325 let mut rng = RandomCoin::new(Word::empty());
326 let managed = account_id(1);
327 let owner = account_id(2);
328 let new_owner = account_id(3);
329
330 let note = OwnerConfigNote::builder()
331 .sender(owner)
332 .target(managed)
333 .config(OwnerConfig::TransferOwnership { new_owner: Some(new_owner) })
334 .generate_serial_number(&mut rng)
335 .build()
336 .unwrap();
337
338 assert_eq!(note.sender(), owner);
339 assert_eq!(note.target(), managed);
340
341 let note = Note::from(note);
342 assert_eq!(note.metadata().note_type(), NoteType::Public);
343 assert_eq!(note.metadata().tag(), NoteTag::with_account_target(managed));
344 assert_eq!(note.assets().num_assets(), 0);
345 }
346
347 #[test]
350 fn builder_attaches_network_target() {
351 let mut rng = RandomCoin::new(Word::empty());
352 let managed = account_id(1);
353
354 let note = OwnerConfigNote::builder()
355 .sender(account_id(2))
356 .target(managed)
357 .config(OwnerConfig::AcceptOwnership)
358 .generate_serial_number(&mut rng)
359 .build()
360 .unwrap();
361
362 assert_eq!(note.attachments().num_attachments(), 1);
363
364 let network_note = AccountTargetNetworkNote::from(note);
365 assert_eq!(network_note.target_account_id(), managed);
366 assert_eq!(network_note.execution_hint(), NoteExecutionHint::Always);
367 assert!(network_note.as_note().is_network_note());
368 }
369
370 #[test]
372 fn builder_keeps_caller_attachments() {
373 let mut rng = RandomCoin::new(Word::empty());
374 let managed = account_id(1);
375 let custom_scheme = NoteAttachmentScheme::new(64).unwrap();
376 let custom = NoteAttachment::with_word(custom_scheme, Word::from([7u32, 0, 0, 0]));
377
378 let note = OwnerConfigNote::builder()
379 .attachment(custom.clone())
380 .sender(account_id(2))
381 .target(managed)
382 .config(OwnerConfig::AcceptOwnership)
383 .generate_serial_number(&mut rng)
384 .build()
385 .unwrap();
386
387 assert_eq!(note.attachments().num_attachments(), 2);
389 assert_eq!(note.attachments().get(0), Some(&custom));
390
391 let network_note = AccountTargetNetworkNote::from(note);
392 assert_eq!(network_note.target_account_id(), managed);
393 }
394
395 #[test]
398 fn builder_rejects_target_for_other_account() {
399 let mut rng = RandomCoin::new(Word::empty());
400 let rogue_target =
401 NetworkAccountTarget::new(account_id(3), NoteExecutionHint::None).unwrap();
402
403 let err = OwnerConfigNote::builder()
404 .attachment(rogue_target)
405 .sender(account_id(2))
406 .target(account_id(1))
407 .config(OwnerConfig::AcceptOwnership)
408 .generate_serial_number(&mut rng)
409 .build()
410 .unwrap_err();
411
412 assert_matches!(err, NoteError::Other { source, .. } => {
413 assert_matches!(
414 *source.unwrap().downcast().unwrap(),
415 NetworkAccountTargetError::TargetMismatch { .. }
416 )
417 });
418 }
419
420 #[test]
422 fn builder_rejects_non_public_account() {
423 let mut rng = RandomCoin::new(Word::empty());
424 let managed = typed_account_id(1, AccountType::Private);
425
426 let err = OwnerConfigNote::builder()
427 .sender(account_id(2))
428 .target(managed)
429 .config(OwnerConfig::AcceptOwnership)
430 .generate_serial_number(&mut rng)
431 .build()
432 .unwrap_err();
433
434 assert_matches!(err, NoteError::Other { source, .. } => {
435 assert_matches!(
436 *source.unwrap().downcast().unwrap(),
437 NetworkAccountTargetError::TargetNotPublic { .. }
438 )
439 });
440 }
441
442 #[test]
444 fn transfer_ownership_storage_layout() {
445 let new_owner = account_id(3);
446 let storage =
447 NoteStorage::from(OwnerConfig::TransferOwnership { new_owner: Some(new_owner) });
448
449 assert_eq!(
450 storage.items(),
451 &[
452 Felt::from(OwnerConfig::VARIANT_TRANSFER_OWNERSHIP),
453 new_owner.suffix(),
454 new_owner.prefix().as_felt(),
455 ]
456 );
457 }
458
459 #[test]
461 fn cancel_transfer_ownership_storage_layout() {
462 let storage = NoteStorage::from(OwnerConfig::TransferOwnership { new_owner: None });
463
464 assert_eq!(
465 storage.items(),
466 &[Felt::from(OwnerConfig::VARIANT_TRANSFER_OWNERSHIP), Felt::ZERO, Felt::ZERO]
467 );
468 }
469
470 #[test]
472 fn accept_and_renounce_storage_layout() {
473 let accept = NoteStorage::from(OwnerConfig::AcceptOwnership);
474 assert_eq!(accept.items(), &[Felt::from(OwnerConfig::VARIANT_ACCEPT_OWNERSHIP)]);
475
476 let renounce = NoteStorage::from(OwnerConfig::RenounceOwnership);
477 assert_eq!(renounce.items(), &[Felt::from(OwnerConfig::VARIANT_RENOUNCE_OWNERSHIP)]);
478 }
479}