use alloc::vec::Vec;
use miden_protocol::account::{AccountId, RoleSymbol};
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;
const RBAC_ACTION_SCRIPT_PATH: &str = "::miden::standards::notes::rbac_action::main";
static RBAC_ACTION_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
let standards_lib = StandardsLib::default();
let path = Path::new(RBAC_ACTION_SCRIPT_PATH);
NoteScript::from_library_reference(standards_lib.as_ref(), path)
.expect("Standards library contains RBAC_ACTION note script procedure")
});
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RbacAction {
GrantRole { role: RoleSymbol, account: AccountId },
RevokeRole { role: RoleSymbol, account: AccountId },
SetRoleAdmin {
role: RoleSymbol,
admin_role: Option<RoleSymbol>,
},
RenounceRole { role: RoleSymbol },
}
impl RbacAction {
const SELECTOR_GRANT_ROLE: u8 = 0;
const SELECTOR_REVOKE_ROLE: u8 = 1;
const SELECTOR_SET_ROLE_ADMIN: u8 = 2;
const SELECTOR_RENOUNCE_ROLE: u8 = 3;
fn to_storage_values(&self) -> Vec<Felt> {
match self {
RbacAction::GrantRole { role, account } => {
vec![
Felt::from(Self::SELECTOR_GRANT_ROLE),
role.as_element(),
account.suffix(),
account.prefix().as_felt(),
]
},
RbacAction::RevokeRole { role, account } => {
vec![
Felt::from(Self::SELECTOR_REVOKE_ROLE),
role.as_element(),
account.suffix(),
account.prefix().as_felt(),
]
},
RbacAction::SetRoleAdmin { role, admin_role } => {
let admin_role = admin_role.as_ref().map_or(Felt::ZERO, RoleSymbol::as_element);
vec![Felt::from(Self::SELECTOR_SET_ROLE_ADMIN), role.as_element(), admin_role]
},
RbacAction::RenounceRole { role } => {
vec![Felt::from(Self::SELECTOR_RENOUNCE_ROLE), role.as_element()]
},
}
}
}
impl From<RbacAction> for NoteStorage {
fn from(action: RbacAction) -> Self {
NoteStorage::new(action.to_storage_values())
.expect("number of storage items should not exceed max storage items")
}
}
#[derive(Debug, Clone)]
pub struct RbacActionNote {
sender: AccountId,
account: AccountId,
action: RbacAction,
serial_number: Word,
attachments: NoteAttachments,
}
#[bon::bon]
impl RbacActionNote {
#[builder]
pub fn new(
#[builder(field)] attachments: Vec<NoteAttachment>,
sender: AccountId,
account: AccountId,
action: RbacAction,
serial_number: Word,
) -> Result<Self, NoteError> {
let attachments = NoteAttachments::new(attachments)?;
Ok(Self {
sender,
account,
action,
serial_number,
attachments,
})
}
}
impl RbacActionNote {
pub const MAX_NUM_STORAGE_ITEMS: usize = 4;
pub fn script() -> NoteScript {
RBAC_ACTION_SCRIPT.clone()
}
pub fn script_root() -> NoteScriptRoot {
RBAC_ACTION_SCRIPT.root()
}
pub fn sender(&self) -> AccountId {
self.sender
}
pub fn account(&self) -> AccountId {
self.account
}
pub fn action(&self) -> &RbacAction {
&self.action
}
pub fn serial_number(&self) -> Word {
self.serial_number
}
pub fn attachments(&self) -> &NoteAttachments {
&self.attachments
}
}
impl<S: rbac_action_note_builder::State> RbacActionNoteBuilder<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: rbac_action_note_builder::State> RbacActionNoteBuilder<S>
where
S::SerialNumber: rbac_action_note_builder::IsUnset,
{
pub fn generate_serial_number(
self,
rng: &mut impl FeltRng,
) -> RbacActionNoteBuilder<rbac_action_note_builder::SetSerialNumber<S>> {
self.serial_number(rng.draw_word())
}
}
impl From<RbacActionNote> for Note {
fn from(note: RbacActionNote) -> Self {
let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
.with_tag(NoteTag::with_account_target(note.account));
let recipient = NoteRecipient::new(
note.serial_number,
RbacActionNote::script(),
NoteStorage::from(note.action),
);
Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
}
}
#[cfg(test)]
mod tests {
use miden_protocol::account::AccountType;
use miden_protocol::crypto::rand::RandomCoin;
use super::*;
fn account_id(seed: u8) -> AccountId {
AccountId::builder()
.account_type(AccountType::Public)
.build_with_seed([seed; 32])
}
fn role(name: &str) -> RoleSymbol {
RoleSymbol::new(name).expect("role symbol should be valid")
}
#[test]
fn builder_builds_rbac_action_note() {
let mut rng = RandomCoin::new(Word::empty());
let managed = account_id(1);
let admin = account_id(2);
let grantee = account_id(3);
let note = RbacActionNote::builder()
.sender(admin)
.account(managed)
.action(RbacAction::GrantRole { role: role("MINTER"), account: grantee })
.generate_serial_number(&mut rng)
.build()
.unwrap();
assert_eq!(note.sender(), admin);
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 grant_role_storage_layout() {
let grantee = account_id(3);
let minter = role("MINTER");
let storage =
NoteStorage::from(RbacAction::GrantRole { role: minter.clone(), account: grantee });
assert_eq!(
storage.items(),
&[
Felt::from(RbacAction::SELECTOR_GRANT_ROLE),
minter.as_element(),
grantee.suffix(),
grantee.prefix().as_felt(),
]
);
}
#[test]
fn set_role_admin_default_storage_layout() {
let minter = role("MINTER");
let storage =
NoteStorage::from(RbacAction::SetRoleAdmin { role: minter.clone(), admin_role: None });
assert_eq!(
storage.items(),
&[Felt::from(RbacAction::SELECTOR_SET_ROLE_ADMIN), minter.as_element(), Felt::ZERO]
);
}
#[test]
fn set_role_admin_delegated_storage_layout() {
let minter = role("MINTER");
let admin = role("MINT_ADMIN");
let storage = NoteStorage::from(RbacAction::SetRoleAdmin {
role: minter.clone(),
admin_role: Some(admin.clone()),
});
assert_eq!(
storage.items(),
&[
Felt::from(RbacAction::SELECTOR_SET_ROLE_ADMIN),
minter.as_element(),
admin.as_element(),
]
);
}
#[test]
fn renounce_role_storage_layout() {
let minter = role("MINTER");
let storage = NoteStorage::from(RbacAction::RenounceRole { role: minter.clone() });
assert_eq!(
storage.items(),
&[Felt::from(RbacAction::SELECTOR_RENOUNCE_ROLE), minter.as_element()]
);
}
}