Skip to main content

miden_protocol/asset/
token_symbol.rs

1use alloc::fmt;
2
3use super::{Felt, TokenSymbolError};
4use crate::utils::ShortCapitalString;
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 label is stored internally as a validated short uppercase string and can be converted to a
11/// [`Felt`] encoding via [`as_element()`](Self::as_element).
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct TokenSymbol(ShortCapitalString);
14
15impl TokenSymbol {
16    /// Alphabet used for token symbols (`A`–`Z`).
17    pub const ALPHABET: &'static str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
18
19    /// Maximum allowed length of the token string.
20    pub const MAX_SYMBOL_LENGTH: usize = 12;
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        ShortCapitalString::from_ascii_uppercase(symbol).map(Self).map_err(Into::into)
51    }
52
53    /// Returns the [`Felt`] encoding of this token symbol.
54    ///
55    /// The alphabet used in the encoding process consists of the Latin capital letters as defined
56    /// in the ASCII table, having the length of 26 characters.
57    ///
58    /// The encoding is performed by multiplying the intermediate encoded value by the length of
59    /// the used alphabet and adding the relative index of the character to it. At the end of the
60    /// encoding process the length of the initial token string is added to the encoded value.
61    ///
62    /// Relative character index is computed by subtracting the index of the character "A" (65)
63    /// from the index of the currently processing character, e.g., `A = 65 - 65 = 0`,
64    /// `B = 66 - 65 = 1`, `...` , `Z = 90 - 65 = 25`.
65    pub fn as_element(&self) -> Felt {
66        self.0.as_element(Self::ALPHABET).expect("TokenSymbol alphabet is always valid")
67    }
68}
69
70impl fmt::Display for TokenSymbol {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        self.0.fmt(f)
73    }
74}
75
76impl From<TokenSymbol> for Felt {
77    fn from(symbol: TokenSymbol) -> Self {
78        symbol.as_element()
79    }
80}
81
82impl From<&TokenSymbol> for Felt {
83    fn from(symbol: &TokenSymbol) -> Self {
84        symbol.as_element()
85    }
86}
87
88impl TryFrom<&str> for TokenSymbol {
89    type Error = TokenSymbolError;
90
91    fn try_from(symbol: &str) -> Result<Self, Self::Error> {
92        TokenSymbol::new(symbol)
93    }
94}
95
96/// Decodes a [`Felt`] representation of the token symbol into a [`TokenSymbol`].
97///
98/// The alphabet used in the decoding process consists of the Latin capital letters as defined in
99/// the ASCII table, having the length of 26 characters.
100///
101/// The decoding is performed by getting the modulus of the intermediate encoded value by the
102/// length of the used alphabet and then dividing the intermediate value by the length of the
103/// alphabet to shift to the next character. At the beginning of the decoding process the length
104/// of the initial token string is obtained from the encoded value. After that the value obtained
105/// after taking the modulus represents the relative character index, which then gets converted to
106/// the ASCII index.
107///
108/// Final ASCII character index is computed by adding the index of the character "A" (65) to the
109/// index of the currently processing character, e.g., `A = 0 + 65 = 65`, `B = 1 + 65 = 66`,
110/// `...` , `Z = 25 + 65 = 90`.
111impl TryFrom<Felt> for TokenSymbol {
112    type Error = TokenSymbolError;
113
114    fn try_from(felt: Felt) -> Result<Self, Self::Error> {
115        ShortCapitalString::try_from_encoded_felt(
116            felt,
117            Self::ALPHABET,
118            Self::MIN_ENCODED_VALUE,
119            Self::MAX_ENCODED_VALUE,
120        )
121        .map(Self)
122        .map_err(Into::into)
123    }
124}
125
126// TESTS
127// ================================================================================================
128
129#[cfg(test)]
130mod test {
131    use alloc::string::ToString;
132
133    use assert_matches::assert_matches;
134
135    use super::{Felt, TokenSymbol, TokenSymbolError};
136
137    #[test]
138    fn test_token_symbol_decoding_encoding() {
139        let symbols = vec![
140            "AAAAAA",
141            "AAAAB",
142            "AAAC",
143            "ABC",
144            "BC",
145            "A",
146            "B",
147            "ZZZZZZ",
148            "ABCDEFGH",
149            "MIDENCRYPTO",
150            "ZZZZZZZZZZZZ",
151        ];
152        for symbol in symbols {
153            let token_symbol = TokenSymbol::try_from(symbol).unwrap();
154            let decoded_symbol = token_symbol.to_string();
155            assert_eq!(symbol, decoded_symbol);
156        }
157
158        let err = TokenSymbol::new("").unwrap_err();
159        assert_matches!(err, TokenSymbolError::InvalidLength(0));
160
161        let err = TokenSymbol::new("ABCDEFGHIJKLM").unwrap_err();
162        assert_matches!(err, TokenSymbolError::InvalidLength(13));
163
164        let err = TokenSymbol::new("$$$").unwrap_err();
165        assert_matches!(err, TokenSymbolError::InvalidCharacter);
166
167        let symbol = "ABCDEFGHIJKL";
168        let token_symbol = TokenSymbol::new(symbol).unwrap();
169        let token_symbol_felt: Felt = token_symbol.into();
170        assert_eq!(token_symbol_felt, TokenSymbol::new(symbol).unwrap().as_element());
171    }
172
173    /// Checks that if the encoded length of the token is less than the actual number of token
174    /// characters, decoding should return the [TokenSymbolError::DataNotFullyDecoded] error.
175    #[test]
176    fn test_invalid_token_len() {
177        // encoded value of this token has `6` as the length of the initial token string
178        let encoded_symbol = TokenSymbol::try_from("ABCDEF").unwrap();
179
180        // decrease encoded length by, for example, `3`
181        let invalid_encoded_symbol_u64 = Felt::from(encoded_symbol).as_canonical_u64() - 3;
182
183        // check that decoding returns an error for a token with invalid length
184        let err =
185            TokenSymbol::try_from(Felt::new_unchecked(invalid_encoded_symbol_u64)).unwrap_err();
186        assert_matches!(err, TokenSymbolError::DataNotFullyDecoded);
187    }
188
189    /// Utility test just to make sure that the [TokenSymbol::MAX_ENCODED_VALUE] constant still
190    /// represents the maximum possible encoded value.
191    #[test]
192    fn test_token_symbol_max_value() {
193        let token_symbol = TokenSymbol::try_from("ZZZZZZZZZZZZ").unwrap();
194        assert_eq!(Felt::from(token_symbol).as_canonical_u64(), TokenSymbol::MAX_ENCODED_VALUE);
195    }
196
197    /// Utility test to make sure that the [TokenSymbol::MIN_ENCODED_VALUE] constant still
198    /// represents the minimum possible encoded value.
199    #[test]
200    fn test_token_symbol_min_value() {
201        let token_symbol = TokenSymbol::try_from("A").unwrap();
202        assert_eq!(Felt::from(token_symbol).as_canonical_u64(), TokenSymbol::MIN_ENCODED_VALUE);
203    }
204
205    /// Checks that [TokenSymbol::try_from(Felt)] returns an error for values below the minimum.
206    #[test]
207    fn test_token_symbol_underflow() {
208        let err = TokenSymbol::try_from(Felt::ZERO).unwrap_err();
209        assert_matches!(err, TokenSymbolError::ValueTooSmall(0));
210    }
211
212    // new_unchecked tests
213    // --------------------------------------------------------------------------------------------
214
215    #[test]
216    fn test_new_unchecked_matches_new() {
217        // Test that new_unchecked produces the same result as new
218        let symbols = ["A", "BC", "ETH", "MIDEN", "ZZZZZZ", "ABCDEFGH", "ZZZZZZZZZZZZ"];
219        for symbol in symbols {
220            let from_new = TokenSymbol::new(symbol).unwrap();
221            let from_static = TokenSymbol::new_unchecked(symbol);
222            assert_eq!(from_new, from_static, "Mismatch for symbol: {}", symbol);
223        }
224    }
225
226    #[test]
227    #[should_panic(expected = "invalid token symbol")]
228    fn token_symbol_panics_on_empty_string() {
229        TokenSymbol::new_unchecked("");
230    }
231
232    #[test]
233    #[should_panic(expected = "invalid token symbol")]
234    fn token_symbol_panics_on_too_long_string() {
235        TokenSymbol::new_unchecked("ABCDEFGHIJKLM");
236    }
237
238    #[test]
239    #[should_panic(expected = "invalid token symbol")]
240    fn token_symbol_panics_on_lowercase() {
241        TokenSymbol::new_unchecked("eth");
242    }
243
244    #[test]
245    #[should_panic(expected = "invalid token symbol")]
246    fn token_symbol_panics_on_invalid_character() {
247        TokenSymbol::new_unchecked("ET$");
248    }
249
250    #[test]
251    #[should_panic(expected = "invalid token symbol")]
252    fn token_symbol_panics_on_number() {
253        TokenSymbol::new_unchecked("ETH1");
254    }
255}