diff --git a/CHANGELOG.md b/CHANGELOG.md index a84aa03c2d..dfb3d7b658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changes - [BREAKING] Renamed `ProvenBatch::new` to `new_unchecked` ([#2687](https://github.com/0xMiden/miden-base/issues/2687)). +- Added `ShortCapitalString` type and related `TokenSymbol` and `RoleSymbol` types. ([#2690](https://github.com/0xMiden/protocol/pull/2690)). ## 0.14.0 (2026-03-23) diff --git a/crates/miden-protocol/src/account/component/storage/type_registry.rs b/crates/miden-protocol/src/account/component/storage/type_registry.rs index 6db8c9716c..5e901475d2 100644 --- a/crates/miden-protocol/src/account/component/storage/type_registry.rs +++ b/crates/miden-protocol/src/account/component/storage/type_registry.rs @@ -9,7 +9,7 @@ use miden_core::{Felt, Word}; use thiserror::Error; use crate::account::auth::{AuthScheme, PublicKey}; -use crate::asset::TokenSymbol; +use crate::asset::{RoleSymbol, TokenSymbol}; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -32,6 +32,7 @@ pub static SCHEMA_TYPE_REGISTRY: LazyLock = LazyLock::new(|| registry.register_felt_type::(); registry.register_felt_type::(); registry.register_felt_type::(); + registry.register_felt_type::(); registry.register_felt_type::(); registry.register_word_type::(); registry.register_word_type::(); @@ -185,6 +186,11 @@ impl SchemaType { .expect("type is well formed") } + /// Returns the schema type for RBAC role symbols. + pub fn role_symbol() -> SchemaType { + SchemaType::new("miden::standards::access::role_symbol").expect("type is well formed") + } + /// Returns a reference to the inner string. pub fn as_str(&self) -> &str { &self.0 @@ -488,6 +494,29 @@ impl FeltType for TokenSymbol { } } +impl FeltType for RoleSymbol { + fn type_name() -> SchemaType { + SchemaType::role_symbol() + } + + fn parse_str(input: &str) -> Result { + let role_symbol = RoleSymbol::new(input).map_err(|err| { + SchemaTypeError::parse(input.to_string(), ::type_name(), err) + })?; + Ok(Felt::from(role_symbol)) + } + + fn display_felt(value: Felt) -> Result { + let role_symbol = RoleSymbol::try_from(value).map_err(|err| { + SchemaTypeError::ConversionError(format!( + "invalid role_symbol value `{}`: {err}", + value.as_canonical_u64() + )) + })?; + Ok(role_symbol.to_string()) + } +} + // WORD IMPLS FOR NATIVE TYPES // ================================================================================================ @@ -818,5 +847,14 @@ mod tests { assert!(SCHEMA_TYPE_REGISTRY.try_parse_felt(&bool_type, "yes").is_err()); assert!(SCHEMA_TYPE_REGISTRY.try_parse_felt(&bool_type, "2").is_err()); assert!(SCHEMA_TYPE_REGISTRY.validate_felt_value(&bool_type, Felt::new(2)).is_err()); + + let role_symbol_type = SchemaType::role_symbol(); + let role_symbol = + SCHEMA_TYPE_REGISTRY.try_parse_felt(&role_symbol_type, "MINTER_ADMIN").unwrap(); + assert_eq!( + SCHEMA_TYPE_REGISTRY.display_felt(&role_symbol_type, role_symbol), + "MINTER_ADMIN" + ); + assert!(SCHEMA_TYPE_REGISTRY.try_parse_felt(&role_symbol_type, "minter").is_err()); } } diff --git a/crates/miden-protocol/src/asset/mod.rs b/crates/miden-protocol/src/asset/mod.rs index 4bdec21c38..30b3276a9e 100644 --- a/crates/miden-protocol/src/asset/mod.rs +++ b/crates/miden-protocol/src/asset/mod.rs @@ -1,5 +1,5 @@ use super::account::AccountType; -use super::errors::{AssetError, TokenSymbolError}; +use super::errors::{AssetError, RoleSymbolError, ShortCapitalStringError, TokenSymbolError}; use super::utils::serde::{ ByteReader, ByteWriter, @@ -20,6 +20,10 @@ pub use nonfungible::{NonFungibleAsset, NonFungibleAssetDetails}; mod token_symbol; pub use token_symbol::TokenSymbol; +mod short_capital_string; +pub use short_capital_string::ShortCapitalString; +mod role_symbol; +pub use role_symbol::RoleSymbol; mod asset_callbacks; pub use asset_callbacks::AssetCallbacks; diff --git a/crates/miden-protocol/src/asset/role_symbol.rs b/crates/miden-protocol/src/asset/role_symbol.rs new file mode 100644 index 0000000000..22ffaf5efa --- /dev/null +++ b/crates/miden-protocol/src/asset/role_symbol.rs @@ -0,0 +1,131 @@ +use alloc::fmt; + +use super::{Felt, RoleSymbolError, ShortCapitalString}; + +/// Represents a role symbol for role-based access control. +/// +/// Role symbols can consist of up to 12 uppercase Latin characters and underscores, e.g. +/// "MINTER", "BURNER", "MINTER_ADMIN". +/// +/// The label is stored as a [`ShortCapitalString`] and can be converted to a [`Felt`] encoding via +/// [`as_element()`](Self::as_element). +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct RoleSymbol(ShortCapitalString); + +impl RoleSymbol { + /// Alphabet used for role symbols (`A-Z` and `_`). + pub const ALPHABET: &'static [u8; 27] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ_"; + + /// The length of the set of characters that can be used in a role symbol. + pub const ALPHABET_LENGTH: u64 = 27; + + /// The minimum integer value of an encoded [`RoleSymbol`]. + /// + /// This value encodes the "A" role symbol. + pub const MIN_ENCODED_VALUE: u64 = 1; + + /// The maximum integer value of an encoded [`RoleSymbol`]. + /// + /// This value encodes the "____________" role symbol (12 underscores). + pub const MAX_ENCODED_VALUE: u64 = 4052555153018976252; + + /// Constructs a new [`RoleSymbol`] from a string, panicking on invalid input. + /// + /// # Panics + /// + /// Panics if: + /// - The length of the provided string is less than 1 or greater than 12. + /// - The provided role symbol contains characters outside `A-Z` and `_`. + pub fn new_unchecked(role_symbol: &str) -> Self { + Self::new(role_symbol).expect("invalid role symbol") + } + + /// Creates a new [`RoleSymbol`] from the provided role symbol string. + /// + /// # Errors + /// Returns an error if: + /// - The length of the provided string is less than 1 or greater than 12. + /// - The provided role symbol contains characters outside `A-Z` and `_`. + pub fn new(role_symbol: &str) -> Result { + ShortCapitalString::from_ascii_uppercase_and_underscore(role_symbol) + .map(Self) + .map_err(Into::into) + } + + /// Returns the [`Felt`] encoding of this role symbol. + pub fn as_element(&self) -> Felt { + self.0.as_element(Self::ALPHABET).expect("RoleSymbol alphabet is always valid") + } +} + +impl fmt::Display for RoleSymbol { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl From for Felt { + fn from(role_symbol: RoleSymbol) -> Self { + role_symbol.as_element() + } +} + +impl From<&RoleSymbol> for Felt { + fn from(role_symbol: &RoleSymbol) -> Self { + role_symbol.as_element() + } +} + +impl TryFrom<&str> for RoleSymbol { + type Error = RoleSymbolError; + + fn try_from(role_symbol: &str) -> Result { + Self::new(role_symbol) + } +} + +impl TryFrom for RoleSymbol { + type Error = RoleSymbolError; + + /// Decodes a [`Felt`] representation of the role symbol into a [`RoleSymbol`]. + fn try_from(felt: Felt) -> Result { + ShortCapitalString::try_from_encoded_felt( + felt, + Self::ALPHABET, + Self::MIN_ENCODED_VALUE, + Self::MAX_ENCODED_VALUE, + ) + .map(Self) + .map_err(Into::into) + } +} + +#[cfg(test)] +mod tests { + use alloc::string::ToString; + + use assert_matches::assert_matches; + + use super::{Felt, RoleSymbol, RoleSymbolError}; + + #[test] + fn test_role_symbol_roundtrip_and_validation() { + let role_symbols = ["MINTER", "BURNER", "MINTER_ADMIN", "A", "A_B_C"]; + for role_symbol in role_symbols { + let encoded: Felt = RoleSymbol::new(role_symbol).unwrap().into(); + let decoded = RoleSymbol::try_from(encoded).unwrap(); + assert_eq!(decoded.to_string(), role_symbol); + } + + assert_matches!(RoleSymbol::new("").unwrap_err(), RoleSymbolError::InvalidLength(0)); + assert_matches!( + RoleSymbol::new("ABCDEFGHIJKLM").unwrap_err(), + RoleSymbolError::InvalidLength(13) + ); + assert_matches!( + RoleSymbol::new("MINTER-ADMIN").unwrap_err(), + RoleSymbolError::InvalidCharacter + ); + assert_matches!(RoleSymbol::new("mINTER").unwrap_err(), RoleSymbolError::InvalidCharacter); + } +} diff --git a/crates/miden-protocol/src/asset/short_capital_string.rs b/crates/miden-protocol/src/asset/short_capital_string.rs new file mode 100644 index 0000000000..c1319a8da4 --- /dev/null +++ b/crates/miden-protocol/src/asset/short_capital_string.rs @@ -0,0 +1,198 @@ +use alloc::fmt; +use alloc::string::String; + +use super::{Felt, ShortCapitalStringError}; + +/// A short string of uppercase ASCII (and optionally underscores) encoded into a [`Felt`] with a +/// configurable alphabet. +/// +/// Use [`Self::from_ascii_uppercase`] or [`Self::from_ascii_uppercase_and_underscore`] to construct +/// a validated value (same rules as [`crate::asset::TokenSymbol`] and +/// [`crate::asset::RoleSymbol`]). +/// +/// The text is stored as a [`String`] and can be converted to a [`Felt`] encoding via +/// [`as_element()`](Self::as_element), and decoded back via +/// [`try_from_encoded_felt()`](Self::try_from_encoded_felt). +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct ShortCapitalString(String); + +impl ShortCapitalString { + /// Maximum allowed string length. + pub const MAX_SYMBOL_LENGTH: usize = 12; + + /// Constructs a value from up to 12 uppercase ASCII Latin letters (`A`–`Z`). + /// + /// # Errors + /// Returns an error if: + /// - The length of the provided string is less than 1 or greater than 12. + /// - The string contains a character that is not uppercase ASCII. + pub fn from_ascii_uppercase(s: &str) -> Result { + let len = s.len(); + if len == 0 || len > Self::MAX_SYMBOL_LENGTH { + return Err(ShortCapitalStringError::InvalidLength(len)); + } + for byte in s.as_bytes() { + if !byte.is_ascii_uppercase() { + return Err(ShortCapitalStringError::InvalidCharacter); + } + } + Ok(Self(String::from(s))) + } + + /// Constructs a value from up to 12 characters from `A`–`Z` and `_`. + /// + /// # Errors + /// Returns an error if: + /// - The length of the provided string is less than 1 or greater than 12. + /// - The string contains a character outside `A`–`Z` and `_`. + pub fn from_ascii_uppercase_and_underscore(s: &str) -> Result { + let len = s.len(); + if len == 0 || len > Self::MAX_SYMBOL_LENGTH { + return Err(ShortCapitalStringError::InvalidLength(len)); + } + for byte in s.as_bytes() { + if !byte.is_ascii_uppercase() && *byte != b'_' { + return Err(ShortCapitalStringError::InvalidRoleCharacter); + } + } + Ok(Self(String::from(s))) + } + + /// Returns the [`Felt`] encoding of this string. + /// + /// The alphabet used in the encoding process is provided by the `alphabet` argument. + /// + /// The encoding is performed by multiplying the intermediate encoded value by the length of + /// the used alphabet and adding the relative index of each character. At the end of the + /// encoding process, the length of the initial string is added to the encoded value. + /// + /// # Errors + /// Returns an error if: + /// - The string contains a character that is not part of the provided alphabet. + pub fn as_element(&self, alphabet: &[u8]) -> Result { + let alphabet_len = alphabet.len() as u64; + let mut encoded_value: u64 = 0; + + for byte in self.0.as_bytes() { + let digit = alphabet + .iter() + .position(|ch| ch == byte) + .map(|pos| pos as u64) + .ok_or(ShortCapitalStringError::InvalidCharacter)?; + + encoded_value = encoded_value * alphabet_len + digit; + } + + // Append the original length so decoding is unambiguous. + encoded_value = encoded_value * alphabet_len + self.0.len() as u64; + Ok(Felt::new(encoded_value)) + } + + /// Decodes an encoded [`Felt`] value into a [`ShortCapitalString`]. + /// + /// The alphabet used in the decoding process is provided by the `alphabet` argument. + /// + /// The decoding is performed by reading the encoded length from the least-significant digit, + /// then repeatedly taking modulus by alphabet length to recover each character index. + /// + /// # Errors + /// Returns an error if: + /// - The encoded value is outside of the provided `min_encoded_value..=max_encoded_value`. + /// - The decoded length is not between 1 and 12. + /// - Decoding leaves non-zero trailing data. + pub fn try_from_encoded_felt( + felt: Felt, + alphabet: &[u8], + min_encoded_value: u64, + max_encoded_value: u64, + ) -> Result { + let encoded_value = felt.as_canonical_u64(); + if encoded_value < min_encoded_value { + return Err(ShortCapitalStringError::ValueTooSmall(encoded_value)); + } + if encoded_value > max_encoded_value { + return Err(ShortCapitalStringError::ValueTooLarge(encoded_value)); + } + + let alphabet_len = alphabet.len() as u64; + let mut remaining_value = encoded_value; + let symbol_len = (remaining_value % alphabet_len) as usize; + if symbol_len == 0 || symbol_len > Self::MAX_SYMBOL_LENGTH { + return Err(ShortCapitalStringError::InvalidLength(symbol_len)); + } + remaining_value /= alphabet_len; + + let mut decoded = String::new(); + for _ in 0..symbol_len { + let digit = (remaining_value % alphabet_len) as usize; + let char = *alphabet.get(digit).ok_or(ShortCapitalStringError::InvalidCharacter)?; + decoded.insert(0, char as char); + remaining_value /= alphabet_len; + } + + if remaining_value != 0 { + return Err(ShortCapitalStringError::DataNotFullyDecoded); + } + + Ok(Self(decoded)) + } +} + +impl fmt::Display for ShortCapitalString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +#[cfg(test)] +mod tests { + use alloc::string::ToString; + + use assert_matches::assert_matches; + + use super::{Felt, ShortCapitalString, ShortCapitalStringError}; + + #[test] + fn short_capital_string_encode_decode_roundtrip() { + let s = ShortCapitalString::from_ascii_uppercase("MIDEN").unwrap(); + let encoded = s.as_element(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ").unwrap(); + let decoded = ShortCapitalString::try_from_encoded_felt( + encoded, + b"ABCDEFGHIJKLMNOPQRSTUVWXYZ", + 1, + 2481152873203736562, + ) + .unwrap(); + assert_eq!(decoded.to_string(), "MIDEN"); + } + + #[test] + fn short_capital_string_rejects_invalid_values() { + assert_matches!( + ShortCapitalString::from_ascii_uppercase("").unwrap_err(), + ShortCapitalStringError::InvalidLength(0) + ); + assert_matches!( + ShortCapitalString::from_ascii_uppercase("ABCDEFGHIJKLM").unwrap_err(), + ShortCapitalStringError::InvalidLength(13) + ); + assert_matches!( + ShortCapitalString::from_ascii_uppercase("A_B").unwrap_err(), + ShortCapitalStringError::InvalidCharacter + ); + + assert_matches!( + ShortCapitalString::from_ascii_uppercase_and_underscore("MINTER-ADMIN").unwrap_err(), + ShortCapitalStringError::InvalidRoleCharacter + ); + + let err = ShortCapitalString::try_from_encoded_felt( + Felt::ZERO, + b"ABCDEFGHIJKLMNOPQRSTUVWXYZ", + 1, + 2481152873203736562, + ) + .unwrap_err(); + assert_matches!(err, ShortCapitalStringError::ValueTooSmall(0)); + } +} diff --git a/crates/miden-protocol/src/asset/token_symbol.rs b/crates/miden-protocol/src/asset/token_symbol.rs index 7189d6805b..ac303fde21 100644 --- a/crates/miden-protocol/src/asset/token_symbol.rs +++ b/crates/miden-protocol/src/asset/token_symbol.rs @@ -1,18 +1,19 @@ use alloc::fmt; -use alloc::string::String; -use super::{Felt, TokenSymbolError}; +use super::{Felt, ShortCapitalString, TokenSymbolError}; /// Represents a token symbol (e.g. "POL", "ETH"). /// /// Token Symbols can consist of up to 12 capital Latin characters, e.g. "C", "ETH", "MIDEN". /// -/// The symbol is stored as a [`String`] and can be converted to a [`Felt`] encoding via +/// The label is stored as a [`ShortCapitalString`] and can be converted to a [`Felt`] encoding via /// [`as_element()`](Self::as_element). #[derive(Clone, Debug, PartialEq, Eq)] -pub struct TokenSymbol(String); +pub struct TokenSymbol(ShortCapitalString); impl TokenSymbol { + pub const ALPHABET: &'static [u8; 26] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + /// Maximum allowed length of the token string. pub const MAX_SYMBOL_LENGTH: usize = 12; @@ -47,19 +48,7 @@ impl TokenSymbol { /// - The length of the provided string is less than 1 or greater than 12. /// - The provided token string contains characters that are not uppercase ASCII. pub fn new(symbol: &str) -> Result { - let len = symbol.len(); - - if len == 0 || len > Self::MAX_SYMBOL_LENGTH { - return Err(TokenSymbolError::InvalidLength(len)); - } - - for byte in symbol.as_bytes() { - if !byte.is_ascii_uppercase() { - return Err(TokenSymbolError::InvalidCharacter); - } - } - - Ok(Self(String::from(symbol))) + ShortCapitalString::from_ascii_uppercase(symbol).map(Self).map_err(Into::into) } /// Returns the [`Felt`] encoding of this token symbol. @@ -75,29 +64,13 @@ impl TokenSymbol { /// from the index of the currently processing character, e.g., `A = 65 - 65 = 0`, /// `B = 66 - 65 = 1`, `...` , `Z = 90 - 65 = 25`. pub fn as_element(&self) -> Felt { - let bytes = self.0.as_bytes(); - let len = bytes.len(); - - let mut encoded_value: u64 = 0; - let mut idx = 0; - - while idx < len { - let digit = (bytes[idx] - b'A') as u64; - encoded_value = encoded_value * Self::ALPHABET_LENGTH + digit; - idx += 1; - } - - // add token length to the encoded value to be able to decode the exact number of - // characters - encoded_value = encoded_value * Self::ALPHABET_LENGTH + len as u64; - - Felt::new(encoded_value) + self.0.as_element(Self::ALPHABET).expect("TokenSymbol alphabet is always valid") } } impl fmt::Display for TokenSymbol { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.0) + self.0.fmt(f) } } @@ -140,38 +113,14 @@ impl TryFrom for TokenSymbol { type Error = TokenSymbolError; fn try_from(felt: Felt) -> Result { - let encoded_value = felt.as_canonical_u64(); - if encoded_value < Self::MIN_ENCODED_VALUE { - return Err(TokenSymbolError::ValueTooSmall(encoded_value)); - } - if encoded_value > Self::MAX_ENCODED_VALUE { - return Err(TokenSymbolError::ValueTooLarge(encoded_value)); - } - - let mut decoded_string = String::new(); - let mut remaining_value = encoded_value; - - // get the token symbol length - let token_len = (remaining_value % Self::ALPHABET_LENGTH) as usize; - if token_len == 0 || token_len > Self::MAX_SYMBOL_LENGTH { - return Err(TokenSymbolError::InvalidLength(token_len)); - } - remaining_value /= Self::ALPHABET_LENGTH; - - for _ in 0..token_len { - let digit = (remaining_value % Self::ALPHABET_LENGTH) as u8; - let char = (digit + b'A') as char; - decoded_string.insert(0, char); - remaining_value /= Self::ALPHABET_LENGTH; - } - - // return an error if some data still remains after specified number of characters have - // been decoded. - if remaining_value != 0 { - return Err(TokenSymbolError::DataNotFullyDecoded); - } - - Ok(TokenSymbol(decoded_string)) + ShortCapitalString::try_from_encoded_felt( + felt, + Self::ALPHABET, + Self::MIN_ENCODED_VALUE, + Self::MAX_ENCODED_VALUE, + ) + .map(Self) + .map_err(Into::into) } } diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index d4d6169f7e..e390cde838 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -13,7 +13,7 @@ use miden_crypto::utils::HexParseError; use thiserror::Error; use super::account::AccountId; -use super::asset::{AssetVaultKey, FungibleAsset, NonFungibleAsset, TokenSymbol}; +use super::asset::{AssetVaultKey, FungibleAsset, NonFungibleAsset, RoleSymbol, TokenSymbol}; use super::crypto::merkle::MerkleError; use super::note::NoteId; use super::{MAX_BATCHES_PER_BLOCK, MAX_OUTPUT_NOTES_PER_BATCH, Word}; @@ -512,6 +512,70 @@ pub enum TokenSymbolError { DataNotFullyDecoded, } +impl From for TokenSymbolError { + fn from(value: ShortCapitalStringError) -> Self { + match value { + ShortCapitalStringError::ValueTooLarge(v) => Self::ValueTooLarge(v), + ShortCapitalStringError::ValueTooSmall(v) => Self::ValueTooSmall(v), + ShortCapitalStringError::InvalidLength(v) => Self::InvalidLength(v), + ShortCapitalStringError::InvalidCharacter + | ShortCapitalStringError::InvalidRoleCharacter => Self::InvalidCharacter, + ShortCapitalStringError::DataNotFullyDecoded => Self::DataNotFullyDecoded, + } + } +} + +// ROLE ERROR +// ================================================================================================ + +#[derive(Debug, Error)] +pub enum RoleSymbolError { + #[error("role symbol value {0} cannot exceed {max}", max = RoleSymbol::MAX_ENCODED_VALUE)] + ValueTooLarge(u64), + #[error("role symbol value {0} cannot be less than {min}", min = RoleSymbol::MIN_ENCODED_VALUE)] + ValueTooSmall(u64), + #[error("role symbol should have length between 1 and 12 characters, but {0} was provided")] + InvalidLength(usize), + #[error("role symbol contains a character that is not uppercase ASCII or underscore")] + InvalidCharacter, + #[error("role symbol data left after decoding the specified number of characters")] + DataNotFullyDecoded, +} + +impl From for RoleSymbolError { + fn from(value: ShortCapitalStringError) -> Self { + match value { + ShortCapitalStringError::ValueTooLarge(v) => Self::ValueTooLarge(v), + ShortCapitalStringError::ValueTooSmall(v) => Self::ValueTooSmall(v), + ShortCapitalStringError::InvalidLength(v) => Self::InvalidLength(v), + ShortCapitalStringError::InvalidCharacter + | ShortCapitalStringError::InvalidRoleCharacter => Self::InvalidCharacter, + ShortCapitalStringError::DataNotFullyDecoded => Self::DataNotFullyDecoded, + } + } +} + +// SHORT CAPITAL STRING ERROR +// ================================================================================================ + +#[derive(Debug, Error)] +pub enum ShortCapitalStringError { + #[error("short capital string value {0} is too large")] + ValueTooLarge(u64), + #[error("short capital string value {0} is too small")] + ValueTooSmall(u64), + #[error( + "short capital string should have length between 1 and 12 characters, but {0} was provided" + )] + InvalidLength(usize), + #[error("short capital string contains an invalid character")] + InvalidCharacter, + #[error("short capital string contains a character that is not uppercase ASCII or underscore")] + InvalidRoleCharacter, + #[error("short capital string data left after decoding the specified number of characters")] + DataNotFullyDecoded, +} + // ASSET VAULT ERROR // ================================================================================================