miden_objects/asset/token_symbol.rs
1use alloc::string::{String, ToString};
2
3use super::{Felt, TokenSymbolError};
4
5/// Represents a string token symbol (e.g. "POL", "ETH") as a single [`Felt`] value.
6///
7/// Token Symbols can consists of up to 6 capital Latin characters, e.g. "C", "ETH", "MIDENC".
8#[derive(Default, Clone, Copy, Debug, PartialEq)]
9pub struct TokenSymbol(Felt);
10
11impl TokenSymbol {
12 /// Maximum allowed length of the token string.
13 pub const MAX_SYMBOL_LENGTH: usize = 6;
14
15 /// The length of the set of characters that can be used in a token's name.
16 pub const ALPHABET_LENGTH: u64 = 26;
17
18 /// The maximum integer value of an encoded [`TokenSymbol`].
19 ///
20 /// This value encodes the "ZZZZZZ" token symbol.
21 pub const MAX_ENCODED_VALUE: u64 = 8031810156;
22
23 /// Creates a new [`TokenSymbol`] instance from the provided token name string.
24 ///
25 /// # Errors
26 /// Returns an error if:
27 /// - The length of the provided string is less than 1 or greater than 6.
28 /// - The provided token string contains characters that are not uppercase ASCII.
29 pub fn new(symbol: &str) -> Result<Self, TokenSymbolError> {
30 let felt = encode_symbol_to_felt(symbol)?;
31 Ok(Self(felt))
32 }
33
34 /// Returns the token name string from the encoded [`TokenSymbol`] value.
35 ///
36 /// # Errors
37 /// Returns an error if:
38 /// - The encoded value exceeds the maximum value of [`Self::MAX_ENCODED_VALUE`].
39 /// - The encoded token string length is less than 1 or greater than 6.
40 /// - The encoded token string length is less than the actual string length.
41 pub fn to_string(&self) -> Result<String, TokenSymbolError> {
42 decode_felt_to_symbol(self.0)
43 }
44}
45
46impl From<TokenSymbol> for Felt {
47 fn from(symbol: TokenSymbol) -> Self {
48 symbol.0
49 }
50}
51
52impl TryFrom<&str> for TokenSymbol {
53 type Error = TokenSymbolError;
54
55 fn try_from(symbol: &str) -> Result<Self, Self::Error> {
56 TokenSymbol::new(symbol)
57 }
58}
59
60impl TryFrom<Felt> for TokenSymbol {
61 type Error = TokenSymbolError;
62
63 fn try_from(felt: Felt) -> Result<Self, Self::Error> {
64 // Check if the felt value is within the valid range
65 if felt.as_int() > Self::MAX_ENCODED_VALUE {
66 return Err(TokenSymbolError::ValueTooLarge(felt.as_int()));
67 }
68 Ok(TokenSymbol(felt))
69 }
70}
71
72// HELPER FUNCTIONS
73// ================================================================================================
74
75/// Encodes the provided token symbol string into a single [`Felt`] value.
76///
77/// The alphabet used in the decoding process consists of the Latin capital letters as defined in
78/// the ASCII table, having the length of 26 characters.
79///
80/// The encoding is performed by multiplying the intermediate encrypted value by the length of the
81/// used alphabet and adding the relative index of the character to it. At the end of the encoding
82/// process the length of the initial token string is added to the encrypted value.
83///
84/// Relative character index is computed by subtracting the index of the character "A" (65) from the
85/// index of the currently processing character, e.g., `A = 65 - 65 = 0`, `B = 66 - 65 = 1`, `...` ,
86/// `Z = 90 - 65 = 25`.
87///
88/// # Errors
89/// Returns an error if:
90/// - The length of the provided string is less than 1 or greater than 6.
91/// - The provided token string contains characters that are not uppercase ASCII.
92fn encode_symbol_to_felt(s: &str) -> Result<Felt, TokenSymbolError> {
93 if s.is_empty() || s.len() > TokenSymbol::MAX_SYMBOL_LENGTH {
94 return Err(TokenSymbolError::InvalidLength(s.len()));
95 } else if s.chars().any(|c| !c.is_ascii_uppercase()) {
96 return Err(TokenSymbolError::InvalidCharacter(s.to_string()));
97 }
98
99 let mut encoded_value = 0;
100 for char in s.chars() {
101 let digit = char as u64 - b'A' as u64;
102 debug_assert!(digit < TokenSymbol::ALPHABET_LENGTH);
103 encoded_value = encoded_value * TokenSymbol::ALPHABET_LENGTH + digit;
104 }
105
106 // add token length to the encoded value to be able to decode the exact number of characters
107 encoded_value = encoded_value * TokenSymbol::ALPHABET_LENGTH + s.len() as u64;
108
109 Ok(Felt::new(encoded_value))
110}
111
112/// Decodes a [Felt] representation of the token symbol into a string.
113///
114/// The alphabet used in the decoding process consists of the Latin capital letters as defined in
115/// the ASCII table, having the length of 26 characters.
116///
117/// The decoding is performed by getting the modulus of the intermediate encrypted value by the
118/// length of the used alphabet and then dividing the intermediate value by the length of the
119/// alphabet to shift to the next character. At the beginning of the decoding process the length of
120/// the initial token string is obtained from the encrypted value. After that the value obtained
121/// after taking the modulus represents the relative character index, which then gets converted to
122/// the ASCII index.
123///
124/// Final ASCII character idex is computed by adding the index of the character "A" (65) to the
125/// index of the currently processing character, e.g., `A = 0 + 65 = 65`, `B = 1 + 65 = 66`, `...` ,
126/// `Z = 25 + 65 = 90`.
127///
128/// # Errors
129/// Returns an error if:
130/// - The encoded value exceeds the maximum value of [`TokenSymbol::MAX_ENCODED_VALUE`].
131/// - The encoded token string length is less than 1 or greater than 6.
132/// - The encoded token string length is less than the actual string length.
133fn decode_felt_to_symbol(encoded_felt: Felt) -> Result<String, TokenSymbolError> {
134 let encoded_value = encoded_felt.as_int();
135
136 // Check if the encoded value is within the valid range
137 if encoded_value > TokenSymbol::MAX_ENCODED_VALUE {
138 return Err(TokenSymbolError::ValueTooLarge(encoded_value));
139 }
140
141 let mut decoded_string = String::new();
142 let mut remaining_value = encoded_value;
143
144 // get the token symbol length
145 let token_len = (remaining_value % TokenSymbol::ALPHABET_LENGTH) as usize;
146 if token_len == 0 || token_len > TokenSymbol::MAX_SYMBOL_LENGTH {
147 return Err(TokenSymbolError::InvalidLength(token_len));
148 }
149 remaining_value /= TokenSymbol::ALPHABET_LENGTH;
150
151 for _ in 0..token_len {
152 let digit = (remaining_value % TokenSymbol::ALPHABET_LENGTH) as u8;
153 let char = (digit + b'A') as char;
154 decoded_string.insert(0, char);
155 remaining_value /= TokenSymbol::ALPHABET_LENGTH;
156 }
157
158 // return an error if some data still remains after specified number of characters have been
159 // decoded.
160 if remaining_value != 0 {
161 return Err(TokenSymbolError::DataNotFullyDecoded);
162 }
163
164 Ok(decoded_string)
165}
166
167// TESTS
168// ================================================================================================
169
170#[cfg(test)]
171mod test {
172 use assert_matches::assert_matches;
173
174 use super::{
175 Felt, TokenSymbol, TokenSymbolError, decode_felt_to_symbol, encode_symbol_to_felt,
176 };
177
178 #[test]
179 fn test_token_symbol_decoding_encoding() {
180 let symbols = vec!["AAAAAA", "AAAAB", "AAAC", "ABC", "BC", "A", "B", "ZZZZZZ"];
181 for symbol in symbols {
182 let token_symbol = TokenSymbol::try_from(symbol).unwrap();
183 let decoded_symbol = TokenSymbol::to_string(&token_symbol).unwrap();
184 assert_eq!(symbol, decoded_symbol);
185 }
186
187 let symbol = "";
188 let felt = encode_symbol_to_felt(symbol);
189 assert_matches!(felt.unwrap_err(), TokenSymbolError::InvalidLength(0));
190
191 let symbol = "ABCDEFG";
192 let felt = encode_symbol_to_felt(symbol);
193 assert_matches!(felt.unwrap_err(), TokenSymbolError::InvalidLength(7));
194
195 let symbol = "$$$";
196 let felt = encode_symbol_to_felt(symbol);
197 assert_matches!(felt.unwrap_err(), TokenSymbolError::InvalidCharacter(s) if s == *"$$$");
198
199 let symbol = "ABCDEF";
200 let token_symbol = TokenSymbol::try_from(symbol);
201 assert!(token_symbol.is_ok());
202 let token_symbol_felt: Felt = token_symbol.unwrap().into();
203 assert_eq!(token_symbol_felt, encode_symbol_to_felt(symbol).unwrap());
204 }
205
206 /// Checks that if the encoded length of the token is less than the actual number of token
207 /// characters, [decode_felt_to_symbol] procedure should return the
208 /// [TokenSymbolError::DataNotFullyDecoded] error.
209 #[test]
210 fn test_invalid_token_len() {
211 // encoded value of this token has `6` as the length of the initial token string
212 let encoded_symbol = TokenSymbol::try_from("ABCDEF").unwrap();
213
214 // decrease encoded length by, for example, `3`
215 let invalid_encoded_symbol_u64 = Felt::from(encoded_symbol).as_int() - 3;
216
217 // check that `decode_felt_to_symbol()` procedure returns an error in attempt to create a
218 // token from encoded token with invalid length
219 let err = decode_felt_to_symbol(Felt::new(invalid_encoded_symbol_u64)).unwrap_err();
220 assert_matches!(err, TokenSymbolError::DataNotFullyDecoded);
221 }
222
223 /// Utility test just to make sure that the [TokenSymbol::MAX_ENCODED_VALUE] constant still
224 /// represents the maximum possible encoded value.
225 #[test]
226 fn test_token_symbol_max_value() {
227 let token_symbol = TokenSymbol::try_from("ZZZZZZ").unwrap();
228 assert_eq!(Felt::from(token_symbol).as_int(), TokenSymbol::MAX_ENCODED_VALUE);
229 }
230}