miden_protocol/asset/
token_symbol.rs1use alloc::fmt;
2use alloc::string::String;
3
4use super::{Felt, TokenSymbolError};
5
6#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct TokenSymbol(String);
14
15impl TokenSymbol {
16 pub const MAX_SYMBOL_LENGTH: usize = 12;
18
19 pub const ALPHABET_LENGTH: u64 = 26;
21
22 pub const MIN_ENCODED_VALUE: u64 = 1;
26
27 pub const MAX_ENCODED_VALUE: u64 = 2481152873203736562;
31
32 pub fn new_unchecked(symbol: &str) -> Self {
40 Self::new(symbol).expect("invalid token symbol")
41 }
42
43 pub fn new(symbol: &str) -> Result<Self, TokenSymbolError> {
50 let len = symbol.len();
51
52 if len == 0 || len > Self::MAX_SYMBOL_LENGTH {
53 return Err(TokenSymbolError::InvalidLength(len));
54 }
55
56 for byte in symbol.as_bytes() {
57 if !byte.is_ascii_uppercase() {
58 return Err(TokenSymbolError::InvalidCharacter);
59 }
60 }
61
62 Ok(Self(String::from(symbol)))
63 }
64
65 pub fn as_element(&self) -> Felt {
78 let bytes = self.0.as_bytes();
79 let len = bytes.len();
80
81 let mut encoded_value: u64 = 0;
82 let mut idx = 0;
83
84 while idx < len {
85 let digit = (bytes[idx] - b'A') as u64;
86 encoded_value = encoded_value * Self::ALPHABET_LENGTH + digit;
87 idx += 1;
88 }
89
90 encoded_value = encoded_value * Self::ALPHABET_LENGTH + len as u64;
93
94 Felt::new(encoded_value)
95 }
96}
97
98impl fmt::Display for TokenSymbol {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 f.write_str(&self.0)
101 }
102}
103
104impl From<TokenSymbol> for Felt {
105 fn from(symbol: TokenSymbol) -> Self {
106 symbol.as_element()
107 }
108}
109
110impl From<&TokenSymbol> for Felt {
111 fn from(symbol: &TokenSymbol) -> Self {
112 symbol.as_element()
113 }
114}
115
116impl TryFrom<&str> for TokenSymbol {
117 type Error = TokenSymbolError;
118
119 fn try_from(symbol: &str) -> Result<Self, Self::Error> {
120 TokenSymbol::new(symbol)
121 }
122}
123
124impl TryFrom<Felt> for TokenSymbol {
140 type Error = TokenSymbolError;
141
142 fn try_from(felt: Felt) -> Result<Self, Self::Error> {
143 let encoded_value = felt.as_canonical_u64();
144 if encoded_value < Self::MIN_ENCODED_VALUE {
145 return Err(TokenSymbolError::ValueTooSmall(encoded_value));
146 }
147 if encoded_value > Self::MAX_ENCODED_VALUE {
148 return Err(TokenSymbolError::ValueTooLarge(encoded_value));
149 }
150
151 let mut decoded_string = String::new();
152 let mut remaining_value = encoded_value;
153
154 let token_len = (remaining_value % Self::ALPHABET_LENGTH) as usize;
156 if token_len == 0 || token_len > Self::MAX_SYMBOL_LENGTH {
157 return Err(TokenSymbolError::InvalidLength(token_len));
158 }
159 remaining_value /= Self::ALPHABET_LENGTH;
160
161 for _ in 0..token_len {
162 let digit = (remaining_value % Self::ALPHABET_LENGTH) as u8;
163 let char = (digit + b'A') as char;
164 decoded_string.insert(0, char);
165 remaining_value /= Self::ALPHABET_LENGTH;
166 }
167
168 if remaining_value != 0 {
171 return Err(TokenSymbolError::DataNotFullyDecoded);
172 }
173
174 Ok(TokenSymbol(decoded_string))
175 }
176}
177
178#[cfg(test)]
182mod test {
183 use alloc::string::ToString;
184
185 use assert_matches::assert_matches;
186
187 use super::{Felt, TokenSymbol, TokenSymbolError};
188
189 #[test]
190 fn test_token_symbol_decoding_encoding() {
191 let symbols = vec![
192 "AAAAAA",
193 "AAAAB",
194 "AAAC",
195 "ABC",
196 "BC",
197 "A",
198 "B",
199 "ZZZZZZ",
200 "ABCDEFGH",
201 "MIDENCRYPTO",
202 "ZZZZZZZZZZZZ",
203 ];
204 for symbol in symbols {
205 let token_symbol = TokenSymbol::try_from(symbol).unwrap();
206 let decoded_symbol = token_symbol.to_string();
207 assert_eq!(symbol, decoded_symbol);
208 }
209
210 let err = TokenSymbol::new("").unwrap_err();
211 assert_matches!(err, TokenSymbolError::InvalidLength(0));
212
213 let err = TokenSymbol::new("ABCDEFGHIJKLM").unwrap_err();
214 assert_matches!(err, TokenSymbolError::InvalidLength(13));
215
216 let err = TokenSymbol::new("$$$").unwrap_err();
217 assert_matches!(err, TokenSymbolError::InvalidCharacter);
218
219 let symbol = "ABCDEFGHIJKL";
220 let token_symbol = TokenSymbol::new(symbol).unwrap();
221 let token_symbol_felt: Felt = token_symbol.into();
222 assert_eq!(token_symbol_felt, TokenSymbol::new(symbol).unwrap().as_element());
223 }
224
225 #[test]
228 fn test_invalid_token_len() {
229 let encoded_symbol = TokenSymbol::try_from("ABCDEF").unwrap();
231
232 let invalid_encoded_symbol_u64 = Felt::from(encoded_symbol).as_canonical_u64() - 3;
234
235 let err = TokenSymbol::try_from(Felt::new(invalid_encoded_symbol_u64)).unwrap_err();
237 assert_matches!(err, TokenSymbolError::DataNotFullyDecoded);
238 }
239
240 #[test]
243 fn test_token_symbol_max_value() {
244 let token_symbol = TokenSymbol::try_from("ZZZZZZZZZZZZ").unwrap();
245 assert_eq!(Felt::from(token_symbol).as_canonical_u64(), TokenSymbol::MAX_ENCODED_VALUE);
246 }
247
248 #[test]
251 fn test_token_symbol_min_value() {
252 let token_symbol = TokenSymbol::try_from("A").unwrap();
253 assert_eq!(Felt::from(token_symbol).as_canonical_u64(), TokenSymbol::MIN_ENCODED_VALUE);
254 }
255
256 #[test]
258 fn test_token_symbol_underflow() {
259 let err = TokenSymbol::try_from(Felt::ZERO).unwrap_err();
260 assert_matches!(err, TokenSymbolError::ValueTooSmall(0));
261 }
262
263 #[test]
267 fn test_new_unchecked_matches_new() {
268 let symbols = ["A", "BC", "ETH", "MIDEN", "ZZZZZZ", "ABCDEFGH", "ZZZZZZZZZZZZ"];
270 for symbol in symbols {
271 let from_new = TokenSymbol::new(symbol).unwrap();
272 let from_static = TokenSymbol::new_unchecked(symbol);
273 assert_eq!(from_new, from_static, "Mismatch for symbol: {}", symbol);
274 }
275 }
276
277 #[test]
278 #[should_panic(expected = "invalid token symbol")]
279 fn token_symbol_panics_on_empty_string() {
280 TokenSymbol::new_unchecked("");
281 }
282
283 #[test]
284 #[should_panic(expected = "invalid token symbol")]
285 fn token_symbol_panics_on_too_long_string() {
286 TokenSymbol::new_unchecked("ABCDEFGHIJKLM");
287 }
288
289 #[test]
290 #[should_panic(expected = "invalid token symbol")]
291 fn token_symbol_panics_on_lowercase() {
292 TokenSymbol::new_unchecked("eth");
293 }
294
295 #[test]
296 #[should_panic(expected = "invalid token symbol")]
297 fn token_symbol_panics_on_invalid_character() {
298 TokenSymbol::new_unchecked("ET$");
299 }
300
301 #[test]
302 #[should_panic(expected = "invalid token symbol")]
303 fn token_symbol_panics_on_number() {
304 TokenSymbol::new_unchecked("ETH1");
305 }
306}