use alloc::fmt;
use alloc::string::String;
use super::{Felt, TokenSymbolError};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TokenSymbol(String);
impl TokenSymbol {
pub const MAX_SYMBOL_LENGTH: usize = 12;
pub const ALPHABET_LENGTH: u64 = 26;
pub const MIN_ENCODED_VALUE: u64 = 1;
pub const MAX_ENCODED_VALUE: u64 = 2481152873203736562;
pub fn new_unchecked(symbol: &str) -> Self {
Self::new(symbol).expect("invalid token symbol")
}
pub fn new(symbol: &str) -> Result<Self, TokenSymbolError> {
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)))
}
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;
}
encoded_value = encoded_value * Self::ALPHABET_LENGTH + len as u64;
Felt::new(encoded_value)
}
}
impl fmt::Display for TokenSymbol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<TokenSymbol> for Felt {
fn from(symbol: TokenSymbol) -> Self {
symbol.as_element()
}
}
impl From<&TokenSymbol> for Felt {
fn from(symbol: &TokenSymbol) -> Self {
symbol.as_element()
}
}
impl TryFrom<&str> for TokenSymbol {
type Error = TokenSymbolError;
fn try_from(symbol: &str) -> Result<Self, Self::Error> {
TokenSymbol::new(symbol)
}
}
impl TryFrom<Felt> for TokenSymbol {
type Error = TokenSymbolError;
fn try_from(felt: Felt) -> Result<Self, Self::Error> {
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;
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;
}
if remaining_value != 0 {
return Err(TokenSymbolError::DataNotFullyDecoded);
}
Ok(TokenSymbol(decoded_string))
}
}
#[cfg(test)]
mod test {
use alloc::string::ToString;
use assert_matches::assert_matches;
use super::{Felt, TokenSymbol, TokenSymbolError};
#[test]
fn test_token_symbol_decoding_encoding() {
let symbols = vec![
"AAAAAA",
"AAAAB",
"AAAC",
"ABC",
"BC",
"A",
"B",
"ZZZZZZ",
"ABCDEFGH",
"MIDENCRYPTO",
"ZZZZZZZZZZZZ",
];
for symbol in symbols {
let token_symbol = TokenSymbol::try_from(symbol).unwrap();
let decoded_symbol = token_symbol.to_string();
assert_eq!(symbol, decoded_symbol);
}
let err = TokenSymbol::new("").unwrap_err();
assert_matches!(err, TokenSymbolError::InvalidLength(0));
let err = TokenSymbol::new("ABCDEFGHIJKLM").unwrap_err();
assert_matches!(err, TokenSymbolError::InvalidLength(13));
let err = TokenSymbol::new("$$$").unwrap_err();
assert_matches!(err, TokenSymbolError::InvalidCharacter);
let symbol = "ABCDEFGHIJKL";
let token_symbol = TokenSymbol::new(symbol).unwrap();
let token_symbol_felt: Felt = token_symbol.into();
assert_eq!(token_symbol_felt, TokenSymbol::new(symbol).unwrap().as_element());
}
#[test]
fn test_invalid_token_len() {
let encoded_symbol = TokenSymbol::try_from("ABCDEF").unwrap();
let invalid_encoded_symbol_u64 = Felt::from(encoded_symbol).as_canonical_u64() - 3;
let err = TokenSymbol::try_from(Felt::new(invalid_encoded_symbol_u64)).unwrap_err();
assert_matches!(err, TokenSymbolError::DataNotFullyDecoded);
}
#[test]
fn test_token_symbol_max_value() {
let token_symbol = TokenSymbol::try_from("ZZZZZZZZZZZZ").unwrap();
assert_eq!(Felt::from(token_symbol).as_canonical_u64(), TokenSymbol::MAX_ENCODED_VALUE);
}
#[test]
fn test_token_symbol_min_value() {
let token_symbol = TokenSymbol::try_from("A").unwrap();
assert_eq!(Felt::from(token_symbol).as_canonical_u64(), TokenSymbol::MIN_ENCODED_VALUE);
}
#[test]
fn test_token_symbol_underflow() {
let err = TokenSymbol::try_from(Felt::ZERO).unwrap_err();
assert_matches!(err, TokenSymbolError::ValueTooSmall(0));
}
#[test]
fn test_new_unchecked_matches_new() {
let symbols = ["A", "BC", "ETH", "MIDEN", "ZZZZZZ", "ABCDEFGH", "ZZZZZZZZZZZZ"];
for symbol in symbols {
let from_new = TokenSymbol::new(symbol).unwrap();
let from_static = TokenSymbol::new_unchecked(symbol);
assert_eq!(from_new, from_static, "Mismatch for symbol: {}", symbol);
}
}
#[test]
#[should_panic(expected = "invalid token symbol")]
fn token_symbol_panics_on_empty_string() {
TokenSymbol::new_unchecked("");
}
#[test]
#[should_panic(expected = "invalid token symbol")]
fn token_symbol_panics_on_too_long_string() {
TokenSymbol::new_unchecked("ABCDEFGHIJKLM");
}
#[test]
#[should_panic(expected = "invalid token symbol")]
fn token_symbol_panics_on_lowercase() {
TokenSymbol::new_unchecked("eth");
}
#[test]
#[should_panic(expected = "invalid token symbol")]
fn token_symbol_panics_on_invalid_character() {
TokenSymbol::new_unchecked("ET$");
}
#[test]
#[should_panic(expected = "invalid token symbol")]
fn token_symbol_panics_on_number() {
TokenSymbol::new_unchecked("ETH1");
}
}