1use alloc::vec::Vec;
2
3use miden_protocol::account::{AccountId, RoleSymbol};
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;
24
25const RBAC_ACTION_SCRIPT_PATH: &str = "::miden::standards::notes::rbac_action::main";
30
31static RBAC_ACTION_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
33 let standards_lib = StandardsLib::default();
34 let path = Path::new(RBAC_ACTION_SCRIPT_PATH);
35 NoteScript::from_library_reference(standards_lib.as_ref(), path)
36 .expect("Standards library contains RBAC_ACTION note script procedure")
37});
38
39#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum RbacAction {
52 GrantRole { role: RoleSymbol, account: AccountId },
54 RevokeRole { role: RoleSymbol, account: AccountId },
57 SetRoleAdmin {
61 role: RoleSymbol,
62 admin_role: Option<RoleSymbol>,
63 },
64 RenounceRole { role: RoleSymbol },
66}
67
68impl RbacAction {
69 const SELECTOR_GRANT_ROLE: u8 = 0;
74 const SELECTOR_REVOKE_ROLE: u8 = 1;
75 const SELECTOR_SET_ROLE_ADMIN: u8 = 2;
76 const SELECTOR_RENOUNCE_ROLE: u8 = 3;
77
78 fn to_storage_values(&self) -> Vec<Felt> {
80 match self {
81 RbacAction::GrantRole { role, account } => {
82 vec![
83 Felt::from(Self::SELECTOR_GRANT_ROLE),
84 role.as_element(),
85 account.suffix(),
86 account.prefix().as_felt(),
87 ]
88 },
89 RbacAction::RevokeRole { role, account } => {
90 vec![
91 Felt::from(Self::SELECTOR_REVOKE_ROLE),
92 role.as_element(),
93 account.suffix(),
94 account.prefix().as_felt(),
95 ]
96 },
97 RbacAction::SetRoleAdmin { role, admin_role } => {
98 let admin_role = admin_role.as_ref().map_or(Felt::ZERO, RoleSymbol::as_element);
101 vec![Felt::from(Self::SELECTOR_SET_ROLE_ADMIN), role.as_element(), admin_role]
102 },
103 RbacAction::RenounceRole { role } => {
104 vec![Felt::from(Self::SELECTOR_RENOUNCE_ROLE), role.as_element()]
105 },
106 }
107 }
108}
109
110impl From<RbacAction> for NoteStorage {
111 fn from(action: RbacAction) -> Self {
112 NoteStorage::new(action.to_storage_values())
113 .expect("number of storage items should not exceed max storage items")
114 }
115}
116
117#[derive(Debug, Clone)]
137pub struct RbacActionNote {
138 sender: AccountId,
139 account: AccountId,
140 action: RbacAction,
141 serial_number: Word,
142 attachments: NoteAttachments,
143}
144
145#[bon::bon]
146impl RbacActionNote {
147 #[builder]
154 pub fn new(
155 #[builder(field)] attachments: Vec<NoteAttachment>,
156 sender: AccountId,
157 account: AccountId,
158 action: RbacAction,
159 serial_number: Word,
160 ) -> Result<Self, NoteError> {
161 let attachments = NoteAttachments::new(attachments)?;
162
163 Ok(Self {
164 sender,
165 account,
166 action,
167 serial_number,
168 attachments,
169 })
170 }
171}
172
173impl RbacActionNote {
174 pub const MAX_NUM_STORAGE_ITEMS: usize = 4;
182
183 pub fn script() -> NoteScript {
188 RBAC_ACTION_SCRIPT.clone()
189 }
190
191 pub fn script_root() -> NoteScriptRoot {
193 RBAC_ACTION_SCRIPT.root()
194 }
195
196 pub fn sender(&self) -> AccountId {
198 self.sender
199 }
200
201 pub fn account(&self) -> AccountId {
203 self.account
204 }
205
206 pub fn action(&self) -> &RbacAction {
208 &self.action
209 }
210
211 pub fn serial_number(&self) -> Word {
213 self.serial_number
214 }
215
216 pub fn attachments(&self) -> &NoteAttachments {
218 &self.attachments
219 }
220}
221
222impl<S: rbac_action_note_builder::State> RbacActionNoteBuilder<S> {
226 pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
228 self.attachments.push(attachment.into());
229 self
230 }
231
232 pub fn attachments(
234 mut self,
235 attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
236 ) -> Self {
237 self.attachments.extend(attachments.into_iter().map(Into::into));
238 self
239 }
240}
241
242impl<S: rbac_action_note_builder::State> RbacActionNoteBuilder<S>
243where
244 S::SerialNumber: rbac_action_note_builder::IsUnset,
245{
246 pub fn generate_serial_number(
248 self,
249 rng: &mut impl FeltRng,
250 ) -> RbacActionNoteBuilder<rbac_action_note_builder::SetSerialNumber<S>> {
251 self.serial_number(rng.draw_word())
252 }
253}
254
255impl From<RbacActionNote> for Note {
259 fn from(note: RbacActionNote) -> Self {
260 let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
263 .with_tag(NoteTag::with_account_target(note.account));
264 let recipient = NoteRecipient::new(
265 note.serial_number,
266 RbacActionNote::script(),
267 NoteStorage::from(note.action),
268 );
269
270 Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
271 }
272}
273
274#[cfg(test)]
278mod tests {
279 use miden_protocol::account::AccountType;
280 use miden_protocol::crypto::rand::RandomCoin;
281
282 use super::*;
283
284 fn account_id(seed: u8) -> AccountId {
285 AccountId::builder()
286 .account_type(AccountType::Public)
287 .build_with_seed([seed; 32])
288 }
289
290 fn role(name: &str) -> RoleSymbol {
291 RoleSymbol::new(name).expect("role symbol should be valid")
292 }
293
294 #[test]
296 fn builder_builds_rbac_action_note() {
297 let mut rng = RandomCoin::new(Word::empty());
298 let managed = account_id(1);
299 let admin = account_id(2);
300 let grantee = account_id(3);
301
302 let note = RbacActionNote::builder()
303 .sender(admin)
304 .account(managed)
305 .action(RbacAction::GrantRole { role: role("MINTER"), account: grantee })
306 .generate_serial_number(&mut rng)
307 .build()
308 .unwrap();
309
310 assert_eq!(note.sender(), admin);
311 assert_eq!(note.account(), managed);
312
313 let note = Note::from(note);
314 assert_eq!(note.metadata().note_type(), NoteType::Public);
315 assert_eq!(note.metadata().tag(), NoteTag::with_account_target(managed));
316 assert_eq!(note.assets().num_assets(), 0);
317 }
318
319 #[test]
321 fn grant_role_storage_layout() {
322 let grantee = account_id(3);
323 let minter = role("MINTER");
324 let storage =
325 NoteStorage::from(RbacAction::GrantRole { role: minter.clone(), account: grantee });
326
327 assert_eq!(
328 storage.items(),
329 &[
330 Felt::from(RbacAction::SELECTOR_GRANT_ROLE),
331 minter.as_element(),
332 grantee.suffix(),
333 grantee.prefix().as_felt(),
334 ]
335 );
336 }
337
338 #[test]
340 fn set_role_admin_default_storage_layout() {
341 let minter = role("MINTER");
342 let storage =
343 NoteStorage::from(RbacAction::SetRoleAdmin { role: minter.clone(), admin_role: None });
344
345 assert_eq!(
346 storage.items(),
347 &[Felt::from(RbacAction::SELECTOR_SET_ROLE_ADMIN), minter.as_element(), Felt::ZERO]
348 );
349 }
350
351 #[test]
353 fn set_role_admin_delegated_storage_layout() {
354 let minter = role("MINTER");
355 let admin = role("MINT_ADMIN");
356 let storage = NoteStorage::from(RbacAction::SetRoleAdmin {
357 role: minter.clone(),
358 admin_role: Some(admin.clone()),
359 });
360
361 assert_eq!(
362 storage.items(),
363 &[
364 Felt::from(RbacAction::SELECTOR_SET_ROLE_ADMIN),
365 minter.as_element(),
366 admin.as_element(),
367 ]
368 );
369 }
370
371 #[test]
373 fn renounce_role_storage_layout() {
374 let minter = role("MINTER");
375 let storage = NoteStorage::from(RbacAction::RenounceRole { role: minter.clone() });
376
377 assert_eq!(
378 storage.items(),
379 &[Felt::from(RbacAction::SELECTOR_RENOUNCE_ROLE), minter.as_element()]
380 );
381 }
382}