Skip to main content

miden_protocol/account/
access.rs

1use alloc::fmt;
2use core::cmp::Ordering;
3
4use crate::Felt;
5use crate::errors::RoleSymbolError;
6use crate::utils::ShortCapitalString;
7
8/// Represents a role symbol for role-based access control.
9///
10/// Role symbols can consist of up to 12 uppercase Latin characters and underscores, e.g.
11/// "MINTER", "BURNER", "MINTER_ADMIN".
12///
13/// The label is stored internally as a validated short string (`A`–`Z` and `_`) and can be
14/// converted to a [`Felt`] encoding via [`as_element()`](Self::as_element).
15///
16/// [`Ord`] and [`PartialOrd`] order role symbols by their encoded [`Felt`] value — the value
17/// actually used as the on-chain role key — rather than lexicographically by their text. The two
18/// orderings diverge (for example `"AB"` encodes above `"B"`), so ordering by the encoded value
19/// keeps in-memory ordering consistent with the on-chain key.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct RoleSymbol(ShortCapitalString);
22
23impl RoleSymbol {
24    /// Alphabet used for role symbols (`A-Z` and `_`).
25    pub const ALPHABET: &'static str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ_";
26
27    /// The minimum integer value of an encoded [`RoleSymbol`].
28    ///
29    /// This value encodes the "A" role symbol.
30    pub const MIN_ENCODED_VALUE: u64 = 1;
31
32    /// The maximum integer value of an encoded [`RoleSymbol`].
33    ///
34    /// This value encodes the "____________" role symbol (12 underscores).
35    pub const MAX_ENCODED_VALUE: u64 = 4052555153018976252;
36
37    /// Constructs a new [`RoleSymbol`] from a string, panicking on invalid input.
38    ///
39    /// # Panics
40    ///
41    /// Panics if:
42    /// - The length of the provided string is less than 1 or greater than 12.
43    /// - The provided role symbol contains characters outside `A-Z` and `_`.
44    pub fn new_unchecked(role_symbol: &str) -> Self {
45        Self::new(role_symbol).expect("invalid role symbol")
46    }
47
48    /// Creates a new [`RoleSymbol`] from the provided role symbol string.
49    ///
50    /// # Errors
51    /// Returns an error if:
52    /// - The length of the provided string is less than 1 or greater than 12.
53    /// - The provided role symbol contains characters outside `A-Z` and `_`.
54    pub fn new(role_symbol: &str) -> Result<Self, RoleSymbolError> {
55        ShortCapitalString::from_ascii_uppercase_and_underscore(role_symbol)
56            .map(Self)
57            .map_err(Into::into)
58    }
59
60    /// Returns the [`Felt`] encoding of this role symbol.
61    pub fn as_element(&self) -> Felt {
62        self.0.as_element(Self::ALPHABET).expect("RoleSymbol alphabet is always valid")
63    }
64}
65
66impl Ord for RoleSymbol {
67    fn cmp(&self, other: &Self) -> Ordering {
68        self.as_element().as_canonical_u64().cmp(&other.as_element().as_canonical_u64())
69    }
70}
71
72impl PartialOrd for RoleSymbol {
73    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
74        Some(self.cmp(other))
75    }
76}
77
78impl fmt::Display for RoleSymbol {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        self.0.fmt(f)
81    }
82}
83
84impl From<RoleSymbol> for Felt {
85    fn from(role_symbol: RoleSymbol) -> Self {
86        role_symbol.as_element()
87    }
88}
89
90impl From<&RoleSymbol> for Felt {
91    fn from(role_symbol: &RoleSymbol) -> Self {
92        role_symbol.as_element()
93    }
94}
95
96impl TryFrom<&str> for RoleSymbol {
97    type Error = RoleSymbolError;
98
99    fn try_from(role_symbol: &str) -> Result<Self, Self::Error> {
100        Self::new(role_symbol)
101    }
102}
103
104impl TryFrom<Felt> for RoleSymbol {
105    type Error = RoleSymbolError;
106
107    /// Decodes a [`Felt`] representation of the role symbol into a [`RoleSymbol`].
108    fn try_from(felt: Felt) -> Result<Self, Self::Error> {
109        ShortCapitalString::try_from_encoded_felt(
110            felt,
111            Self::ALPHABET,
112            Self::MIN_ENCODED_VALUE,
113            Self::MAX_ENCODED_VALUE,
114        )
115        .map(Self)
116        .map_err(Into::into)
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use alloc::string::ToString;
123
124    use assert_matches::assert_matches;
125
126    use super::{Felt, RoleSymbol};
127    use crate::errors::RoleSymbolError;
128
129    #[test]
130    fn test_role_symbol_roundtrip_and_validation() {
131        let role_symbols = ["MINTER", "BURNER", "MINTER_ADMIN", "A", "A_B_C"];
132        for role_symbol in role_symbols {
133            let encoded: Felt = RoleSymbol::new(role_symbol).unwrap().into();
134            let decoded = RoleSymbol::try_from(encoded).unwrap();
135            assert_eq!(decoded.to_string(), role_symbol);
136        }
137
138        assert_matches!(RoleSymbol::new("").unwrap_err(), RoleSymbolError::InvalidLength(0));
139        assert_matches!(
140            RoleSymbol::new("ABCDEFGHIJKLM").unwrap_err(),
141            RoleSymbolError::InvalidLength(13)
142        );
143        assert_matches!(
144            RoleSymbol::new("MINTER-ADMIN").unwrap_err(),
145            RoleSymbolError::InvalidCharacter
146        );
147        assert_matches!(RoleSymbol::new("mINTER").unwrap_err(), RoleSymbolError::InvalidCharacter);
148    }
149
150    #[test]
151    fn test_role_symbol_ordering_matches_encoded_felt() {
152        let ab = RoleSymbol::new("AB").unwrap();
153        let b = RoleSymbol::new("B").unwrap();
154
155        // "AB" encodes above "B" (29 vs 28), the opposite of lexicographic order where
156        // "AB" < "B". RoleSymbol must order by the encoded Felt value, not the text.
157        assert!(ab.as_element().as_canonical_u64() > b.as_element().as_canonical_u64());
158        assert!(ab > b);
159        assert!(b < ab);
160    }
161}