Skip to main content

miden_protocol/asset/
token_symbol.rs

1use alloc::fmt;
2use alloc::string::String;
3
4use super::{Felt, TokenSymbolError};
5
6/// Represents a token symbol (e.g. "POL", "ETH").
7///
8/// Token Symbols can consist of up to 12 capital Latin characters, e.g. "C", "ETH", "MIDEN".
9///
10/// The symbol is stored as a [`String`] and can be converted to a [`Felt`] encoding via
11/// [`as_element()`](Self::as_element).
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct TokenSymbol(String);
14
15impl TokenSymbol {
16    /// Maximum allowed length of the token string.
17    pub const MAX_SYMBOL_LENGTH: usize = 12;
18
19    /// The length of the set of characters that can be used in a token's name.
20    pub const ALPHABET_LENGTH: u64 = 26;
21
22    /// The minimum integer value of an encoded [`TokenSymbol`].
23    ///
24    /// This value encodes the "A" token symbol.
25    pub const MIN_ENCODED_VALUE: u64 = 1;
26
27    /// The maximum integer value of an encoded [`TokenSymbol`].
28    ///
29    /// This value encodes the "ZZZZZZZZZZZZ" token symbol.
30    pub const MAX_ENCODED_VALUE: u64 = 2481152873203736562;
31
32    /// Constructs a new [`TokenSymbol`] from a string, panicking on invalid input.
33    ///
34    /// # Panics
35    ///
36    /// Panics if:
37    /// - The length of the provided string is less than 1 or greater than 12.
38    /// - The provided token string contains characters that are not uppercase ASCII.
39    pub fn new_unchecked(symbol: &str) -> Self {
40        Self::new(symbol).expect("invalid token symbol")
41    }
42
43    /// Creates a new [`TokenSymbol`] instance from the provided token name string.
44    ///
45    /// # Errors
46    /// Returns an error if:
47    /// - The length of the provided string is less than 1 or greater than 12.
48    /// - The provided token string contains characters that are not uppercase ASCII.
49    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    /// Returns the [`Felt`] encoding of this token symbol.
66    ///
67    /// The alphabet used in the encoding process consists of the Latin capital letters as defined
68    /// in the ASCII table, having the length of 26 characters.
69    ///
70    /// The encoding is performed by multiplying the intermediate encoded value by the length of
71    /// the used alphabet and adding the relative index of the character to it. At the end of the
72    /// encoding process the length of the initial token string is added to the encoded value.
73    ///
74    /// Relative character index is computed by subtracting the index of the character "A" (65)
75    /// from the index of the currently processing character, e.g., `A = 65 - 65 = 0`,
76    /// `B = 66 - 65 = 1`, `...` , `Z = 90 - 65 = 25`.
77    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        // add token length to the encoded value to be able to decode the exact number of
91        // characters
92        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
124/// Decodes a [`Felt`] representation of the token symbol into a [`TokenSymbol`].
125///
126/// The alphabet used in the decoding process consists of the Latin capital letters as defined in
127/// the ASCII table, having the length of 26 characters.
128///
129/// The decoding is performed by getting the modulus of the intermediate encoded value by the
130/// length of the used alphabet and then dividing the intermediate value by the length of the
131/// alphabet to shift to the next character. At the beginning of the decoding process the length
132/// of the initial token string is obtained from the encoded value. After that the value obtained
133/// after taking the modulus represents the relative character index, which then gets converted to
134/// the ASCII index.
135///
136/// Final ASCII character index is computed by adding the index of the character "A" (65) to the
137/// index of the currently processing character, e.g., `A = 0 + 65 = 65`, `B = 1 + 65 = 66`,
138/// `...` , `Z = 25 + 65 = 90`.
139impl 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        // get the token symbol length
155        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        // return an error if some data still remains after specified number of characters have
169        // been decoded.
170        if remaining_value != 0 {
171            return Err(TokenSymbolError::DataNotFullyDecoded);
172        }
173
174        Ok(TokenSymbol(decoded_string))
175    }
176}
177
178// TESTS
179// ================================================================================================
180
181#[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    /// Checks that if the encoded length of the token is less than the actual number of token
226    /// characters, decoding should return the [TokenSymbolError::DataNotFullyDecoded] error.
227    #[test]
228    fn test_invalid_token_len() {
229        // encoded value of this token has `6` as the length of the initial token string
230        let encoded_symbol = TokenSymbol::try_from("ABCDEF").unwrap();
231
232        // decrease encoded length by, for example, `3`
233        let invalid_encoded_symbol_u64 = Felt::from(encoded_symbol).as_canonical_u64() - 3;
234
235        // check that decoding returns an error for a token with invalid length
236        let err = TokenSymbol::try_from(Felt::new(invalid_encoded_symbol_u64)).unwrap_err();
237        assert_matches!(err, TokenSymbolError::DataNotFullyDecoded);
238    }
239
240    /// Utility test just to make sure that the [TokenSymbol::MAX_ENCODED_VALUE] constant still
241    /// represents the maximum possible encoded value.
242    #[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    /// Utility test to make sure that the [TokenSymbol::MIN_ENCODED_VALUE] constant still
249    /// represents the minimum possible encoded value.
250    #[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    /// Checks that [TokenSymbol::try_from(Felt)] returns an error for values below the minimum.
257    #[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    // new_unchecked tests
264    // --------------------------------------------------------------------------------------------
265
266    #[test]
267    fn test_new_unchecked_matches_new() {
268        // Test that new_unchecked produces the same result as new
269        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}