use alloc::vec::Vec;
use miden_protocol::account::AccountId;
use miden_protocol::assembly::Path;
use miden_protocol::crypto::rand::FeltRng;
use miden_protocol::errors::NoteError;
use miden_protocol::note::{
Note,
NoteAssets,
NoteAttachment,
NoteAttachments,
NoteRecipient,
NoteScript,
NoteScriptRoot,
NoteStorage,
NoteTag,
NoteType,
PartialNoteMetadata,
};
use miden_protocol::utils::sync::LazyLock;
use miden_protocol::{Felt, Word};
use crate::StandardsLib;
use crate::note::costs::{NoteConsumptionCost, OWNER_CONFIG_CONSUMPTION_CYCLES};
use crate::note::{AccountTargetNetworkNote, NetworkAccountTarget};
const OWNER_CONFIG_SCRIPT_PATH: &str = "::miden::standards::notes::owner_config::main";
static OWNER_CONFIG_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
let standards_lib = StandardsLib::default();
let path = Path::new(OWNER_CONFIG_SCRIPT_PATH);
NoteScript::from_package_reference(standards_lib.as_ref(), path)
.expect("Standards library contains OWNER_CONFIG note script procedure")
});
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OwnerConfig {
TransferOwnership { new_owner: Option<AccountId> },
AcceptOwnership,
RenounceOwnership,
}
impl OwnerConfig {
const SELECTOR_TRANSFER_OWNERSHIP: u8 = 0;
const SELECTOR_ACCEPT_OWNERSHIP: u8 = 1;
const SELECTOR_RENOUNCE_OWNERSHIP: u8 = 2;
fn to_storage_values(self) -> Vec<Felt> {
match self {
OwnerConfig::TransferOwnership { new_owner } => {
let (suffix, prefix) = match new_owner {
Some(id) => (id.suffix(), id.prefix().as_felt()),
None => (Felt::ZERO, Felt::ZERO),
};
vec![Felt::from(Self::SELECTOR_TRANSFER_OWNERSHIP), suffix, prefix]
},
OwnerConfig::AcceptOwnership => {
vec![Felt::from(Self::SELECTOR_ACCEPT_OWNERSHIP)]
},
OwnerConfig::RenounceOwnership => {
vec![Felt::from(Self::SELECTOR_RENOUNCE_OWNERSHIP)]
},
}
}
}
impl From<OwnerConfig> for NoteStorage {
fn from(config: OwnerConfig) -> Self {
NoteStorage::new(config.to_storage_values())
.expect("number of storage items should not exceed max storage items")
}
}
#[derive(Debug, Clone)]
pub struct OwnerConfigNote {
sender: AccountId,
target: AccountId,
config: OwnerConfig,
serial_number: Word,
attachments: NoteAttachments,
}
#[bon::bon]
impl OwnerConfigNote {
#[builder]
pub fn new(
#[builder(field)] mut attachments: Vec<NoteAttachment>,
sender: AccountId,
target: AccountId,
config: OwnerConfig,
serial_number: Word,
) -> Result<Self, NoteError> {
NetworkAccountTarget::ensure_presence(&mut attachments, target).map_err(|err| {
NoteError::other_with_source(
"failed to bind the OwnerConfig note to its target account",
err,
)
})?;
let attachments = NoteAttachments::new(attachments)?;
Ok(Self {
sender,
target,
config,
serial_number,
attachments,
})
}
}
impl OwnerConfigNote {
pub const MAX_NUM_STORAGE_ITEMS: usize = 3;
pub fn script() -> NoteScript {
OWNER_CONFIG_SCRIPT.clone()
}
pub fn script_root() -> NoteScriptRoot {
OWNER_CONFIG_SCRIPT.root()
}
pub fn sender(&self) -> AccountId {
self.sender
}
pub fn account(&self) -> AccountId {
self.target
}
pub fn config(&self) -> OwnerConfig {
self.config
}
pub fn serial_number(&self) -> Word {
self.serial_number
}
pub fn attachments(&self) -> &NoteAttachments {
&self.attachments
}
}
impl<S: owner_config_note_builder::State> OwnerConfigNoteBuilder<S> {
pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
self.attachments.push(attachment.into());
self
}
pub fn attachments(
mut self,
attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
) -> Self {
self.attachments.extend(attachments.into_iter().map(Into::into));
self
}
}
impl<S: owner_config_note_builder::State> OwnerConfigNoteBuilder<S>
where
S::SerialNumber: owner_config_note_builder::IsUnset,
{
pub fn generate_serial_number(
self,
rng: &mut impl FeltRng,
) -> OwnerConfigNoteBuilder<owner_config_note_builder::SetSerialNumber<S>> {
self.serial_number(rng.draw_word())
}
}
impl From<OwnerConfigNote> for Note {
fn from(note: OwnerConfigNote) -> Self {
let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
.with_tag(NoteTag::with_account_target(note.target));
let recipient = NoteRecipient::new(
note.serial_number,
OwnerConfigNote::script(),
NoteStorage::from(note.config),
);
Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
}
}
impl From<OwnerConfigNote> for AccountTargetNetworkNote {
fn from(note: OwnerConfigNote) -> Self {
AccountTargetNetworkNote::new(Note::from(note))
.expect("OwnerConfig note is public and carries a network account target attachment")
}
}
impl NoteConsumptionCost for OwnerConfigNote {
fn consumption_cycles() -> u32 {
OWNER_CONFIG_CONSUMPTION_CYCLES
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use miden_protocol::account::AccountType;
use miden_protocol::crypto::rand::RandomCoin;
use miden_protocol::note::NoteAttachmentScheme;
use super::*;
use crate::note::{NetworkAccountTargetError, NetworkNoteExt, NoteExecutionHint};
fn account_id(seed: u8) -> AccountId {
typed_account_id(seed, AccountType::Public)
}
fn typed_account_id(seed: u8, account_type: AccountType) -> AccountId {
AccountId::builder().account_type(account_type).build_with_seed([seed; 32])
}
#[test]
fn builder_builds_owner_config_note() {
let mut rng = RandomCoin::new(Word::empty());
let managed = account_id(1);
let owner = account_id(2);
let new_owner = account_id(3);
let note = OwnerConfigNote::builder()
.sender(owner)
.target(managed)
.config(OwnerConfig::TransferOwnership { new_owner: Some(new_owner) })
.generate_serial_number(&mut rng)
.build()
.unwrap();
assert_eq!(note.sender(), owner);
assert_eq!(note.account(), managed);
let note = Note::from(note);
assert_eq!(note.metadata().note_type(), NoteType::Public);
assert_eq!(note.metadata().tag(), NoteTag::with_account_target(managed));
assert_eq!(note.assets().num_assets(), 0);
}
#[test]
fn builder_attaches_network_target() {
let mut rng = RandomCoin::new(Word::empty());
let managed = account_id(1);
let note = OwnerConfigNote::builder()
.sender(account_id(2))
.target(managed)
.config(OwnerConfig::AcceptOwnership)
.generate_serial_number(&mut rng)
.build()
.unwrap();
assert_eq!(note.attachments().num_attachments(), 1);
let network_note = AccountTargetNetworkNote::from(note);
assert_eq!(network_note.target_account_id(), managed);
assert_eq!(network_note.execution_hint(), NoteExecutionHint::Always);
assert!(network_note.as_note().is_network_note());
}
#[test]
fn builder_keeps_caller_attachments() {
let mut rng = RandomCoin::new(Word::empty());
let managed = account_id(1);
let custom_scheme = NoteAttachmentScheme::new(64).unwrap();
let custom = NoteAttachment::with_word(custom_scheme, Word::from([7u32, 0, 0, 0]));
let note = OwnerConfigNote::builder()
.attachment(custom.clone())
.sender(account_id(2))
.target(managed)
.config(OwnerConfig::AcceptOwnership)
.generate_serial_number(&mut rng)
.build()
.unwrap();
assert_eq!(note.attachments().num_attachments(), 2);
assert_eq!(note.attachments().get(0), Some(&custom));
let network_note = AccountTargetNetworkNote::from(note);
assert_eq!(network_note.target_account_id(), managed);
}
#[test]
fn builder_rejects_target_for_other_account() {
let mut rng = RandomCoin::new(Word::empty());
let rogue_target =
NetworkAccountTarget::new(account_id(3), NoteExecutionHint::None).unwrap();
let err = OwnerConfigNote::builder()
.attachment(rogue_target)
.sender(account_id(2))
.target(account_id(1))
.config(OwnerConfig::AcceptOwnership)
.generate_serial_number(&mut rng)
.build()
.unwrap_err();
assert_matches!(err, NoteError::Other { source, .. } => {
assert_matches!(
*source.unwrap().downcast().unwrap(),
NetworkAccountTargetError::TargetMismatch { .. }
)
});
}
#[test]
fn builder_rejects_non_public_account() {
let mut rng = RandomCoin::new(Word::empty());
let managed = typed_account_id(1, AccountType::Private);
let err = OwnerConfigNote::builder()
.sender(account_id(2))
.target(managed)
.config(OwnerConfig::AcceptOwnership)
.generate_serial_number(&mut rng)
.build()
.unwrap_err();
assert_matches!(err, NoteError::Other { source, .. } => {
assert_matches!(
*source.unwrap().downcast().unwrap(),
NetworkAccountTargetError::TargetNotPublic { .. }
)
});
}
#[test]
fn transfer_ownership_storage_layout() {
let new_owner = account_id(3);
let storage =
NoteStorage::from(OwnerConfig::TransferOwnership { new_owner: Some(new_owner) });
assert_eq!(
storage.items(),
&[
Felt::from(OwnerConfig::SELECTOR_TRANSFER_OWNERSHIP),
new_owner.suffix(),
new_owner.prefix().as_felt(),
]
);
}
#[test]
fn cancel_transfer_ownership_storage_layout() {
let storage = NoteStorage::from(OwnerConfig::TransferOwnership { new_owner: None });
assert_eq!(
storage.items(),
&[Felt::from(OwnerConfig::SELECTOR_TRANSFER_OWNERSHIP), Felt::ZERO, Felt::ZERO]
);
}
#[test]
fn accept_and_renounce_storage_layout() {
let accept = NoteStorage::from(OwnerConfig::AcceptOwnership);
assert_eq!(accept.items(), &[Felt::from(OwnerConfig::SELECTOR_ACCEPT_OWNERSHIP)]);
let renounce = NoteStorage::from(OwnerConfig::RenounceOwnership);
assert_eq!(renounce.items(), &[Felt::from(OwnerConfig::SELECTOR_RENOUNCE_OWNERSHIP)]);
}
}