Skip to main content

miden_protocol/asset/
token_symbol.rs

1use alloc::string::String;
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 12 capital Latin characters, e.g. "C", "ETH", "MIDEN".
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 = 12;
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 "ZZZZZZZZZZZZ" token symbol.
21    pub const MAX_ENCODED_VALUE: u64 = 2481152873203736562;
22
23    /// Constructs a new [`TokenSymbol`] from a static string.
24    ///
25    /// This function is `const` and can be used to define token symbols as constants, e.g.:
26    ///
27    /// ```rust
28    /// # use miden_protocol::asset::TokenSymbol;
29    /// const TOKEN: TokenSymbol = TokenSymbol::from_static_str("ETH");
30    /// ```
31    ///
32    /// This is convenient because using a string that is not a valid token symbol fails to
33    /// compile.
34    ///
35    /// # Panics
36    ///
37    /// Panics if:
38    /// - The length of the provided string is less than 1 or greater than 12.
39    /// - The provided token string contains characters that are not uppercase ASCII.
40    pub const fn from_static_str(symbol: &'static str) -> Self {
41        match encode_symbol_to_felt(symbol) {
42            Ok(felt) => Self(felt),
43            // We cannot format the error in a const context.
44            Err(_) => panic!("invalid token symbol"),
45        }
46    }
47
48    /// Creates a new [`TokenSymbol`] instance from the provided token name string.
49    ///
50    /// # Errors
51    /// Returns an error if:
52    /// - The length of the provided string is less than 1 or greater than 12.
53    /// - The provided token string contains characters that are not uppercase ASCII.
54    pub fn new(symbol: &str) -> Result<Self, TokenSymbolError> {
55        let felt = encode_symbol_to_felt(symbol)?;
56        Ok(Self(felt))
57    }
58
59    /// Returns the token name string from the encoded [`TokenSymbol`] value.
60    ///     
61    /// # Errors
62    /// Returns an error if:
63    /// - The encoded value exceeds the maximum value of [`Self::MAX_ENCODED_VALUE`].
64    /// - The encoded token string length is less than 1 or greater than 12.
65    /// - The encoded token string length is less than the actual string length.
66    pub fn to_string(&self) -> Result<String, TokenSymbolError> {
67        decode_felt_to_symbol(self.0)
68    }
69}
70
71impl From<TokenSymbol> for Felt {
72    fn from(symbol: TokenSymbol) -> Self {
73        symbol.0
74    }
75}
76
77impl TryFrom<&str> for TokenSymbol {
78    type Error = TokenSymbolError;
79
80    fn try_from(symbol: &str) -> Result<Self, Self::Error> {
81        TokenSymbol::new(symbol)
82    }
83}
84
85impl TryFrom<Felt> for TokenSymbol {
86    type Error = TokenSymbolError;
87
88    fn try_from(felt: Felt) -> Result<Self, Self::Error> {
89        // Check if the felt value is within the valid range
90        if felt.as_int() > Self::MAX_ENCODED_VALUE {
91            return Err(TokenSymbolError::ValueTooLarge(felt.as_int()));
92        }
93        Ok(TokenSymbol(felt))
94    }
95}
96
97// HELPER FUNCTIONS
98// ================================================================================================
99
100/// Encodes the provided token symbol string into a single [`Felt`] value.
101///
102/// The alphabet used in the decoding process consists of the Latin capital letters as defined in
103/// the ASCII table, having the length of 26 characters.
104///
105/// The encoding is performed by multiplying the intermediate encrypted value by the length of the
106/// used alphabet and adding the relative index of the character to it. At the end of the encoding
107/// process the length of the initial token string is added to the encrypted value.
108///
109/// Relative character index is computed by subtracting the index of the character "A" (65) from the
110/// index of the currently processing character, e.g., `A = 65 - 65 = 0`, `B = 66 - 65 = 1`, `...` ,
111/// `Z = 90 - 65 = 25`.
112///
113/// # Errors
114/// Returns an error if:
115/// - The length of the provided string is less than 1 or greater than 12.
116/// - The provided token string contains characters that are not uppercase ASCII.
117const fn encode_symbol_to_felt(s: &str) -> Result<Felt, TokenSymbolError> {
118    let bytes = s.as_bytes();
119    let len = bytes.len();
120
121    if len == 0 || len > TokenSymbol::MAX_SYMBOL_LENGTH {
122        return Err(TokenSymbolError::InvalidLength(len));
123    }
124
125    let mut encoded_value: u64 = 0;
126    let mut idx = 0;
127
128    while idx < len {
129        let byte = bytes[idx];
130
131        if !byte.is_ascii_uppercase() {
132            return Err(TokenSymbolError::InvalidCharacter);
133        }
134
135        let digit = (byte - b'A') as u64;
136        encoded_value = encoded_value * TokenSymbol::ALPHABET_LENGTH + digit;
137        idx += 1;
138    }
139
140    // add token length to the encoded value to be able to decode the exact number of characters
141    encoded_value = encoded_value * TokenSymbol::ALPHABET_LENGTH + len as u64;
142
143    Ok(Felt::new(encoded_value))
144}
145
146/// Decodes a [Felt] representation of the token symbol into a string.
147///
148/// The alphabet used in the decoding process consists of the Latin capital letters as defined in
149/// the ASCII table, having the length of 26 characters.
150///
151/// The decoding is performed by getting the modulus of the intermediate encrypted value by the
152/// length of the used alphabet and then dividing the intermediate value by the length of the
153/// alphabet to shift to the next character. At the beginning of the decoding process the length of
154/// the initial token string is obtained from the encrypted value. After that the value obtained
155/// after taking the modulus represents the relative character index, which then gets converted to
156/// the ASCII index.
157///
158/// Final ASCII character idex is computed by adding the index of the character "A" (65) to the
159/// index of the currently processing character, e.g., `A = 0 + 65 = 65`, `B = 1 + 65 = 66`, `...` ,
160/// `Z = 25 + 65 = 90`.
161///
162/// # Errors
163/// Returns an error if:
164/// - The encoded value exceeds the maximum value of [`TokenSymbol::MAX_ENCODED_VALUE`].
165/// - The encoded token string length is less than 1 or greater than 12.
166/// - The encoded token string length is less than the actual string length.
167fn decode_felt_to_symbol(encoded_felt: Felt) -> Result<String, TokenSymbolError> {
168    let encoded_value = encoded_felt.as_int();
169
170    // Check if the encoded value is within the valid range
171    if encoded_value > TokenSymbol::MAX_ENCODED_VALUE {
172        return Err(TokenSymbolError::ValueTooLarge(encoded_value));
173    }
174
175    let mut decoded_string = String::new();
176    let mut remaining_value = encoded_value;
177
178    // get the token symbol length
179    let token_len = (remaining_value % TokenSymbol::ALPHABET_LENGTH) as usize;
180    if token_len == 0 || token_len > TokenSymbol::MAX_SYMBOL_LENGTH {
181        return Err(TokenSymbolError::InvalidLength(token_len));
182    }
183    remaining_value /= TokenSymbol::ALPHABET_LENGTH;
184
185    for _ in 0..token_len {
186        let digit = (remaining_value % TokenSymbol::ALPHABET_LENGTH) as u8;
187        let char = (digit + b'A') as char;
188        decoded_string.insert(0, char);
189        remaining_value /= TokenSymbol::ALPHABET_LENGTH;
190    }
191
192    // return an error if some data still remains after specified number of characters have been
193    // decoded.
194    if remaining_value != 0 {
195        return Err(TokenSymbolError::DataNotFullyDecoded);
196    }
197
198    Ok(decoded_string)
199}
200
201// TESTS
202// ================================================================================================
203
204#[cfg(test)]
205mod test {
206    use assert_matches::assert_matches;
207
208    use super::{
209        Felt,
210        TokenSymbol,
211        TokenSymbolError,
212        decode_felt_to_symbol,
213        encode_symbol_to_felt,
214    };
215
216    #[test]
217    fn test_token_symbol_decoding_encoding() {
218        let symbols = vec![
219            "AAAAAA",
220            "AAAAB",
221            "AAAC",
222            "ABC",
223            "BC",
224            "A",
225            "B",
226            "ZZZZZZ",
227            "ABCDEFGH",
228            "MIDENCRYPTO",
229            "ZZZZZZZZZZZZ",
230        ];
231        for symbol in symbols {
232            let token_symbol = TokenSymbol::try_from(symbol).unwrap();
233            let decoded_symbol = TokenSymbol::to_string(&token_symbol).unwrap();
234            assert_eq!(symbol, decoded_symbol);
235        }
236
237        let symbol = "";
238        let felt = encode_symbol_to_felt(symbol);
239        assert_matches!(felt.unwrap_err(), TokenSymbolError::InvalidLength(0));
240
241        let symbol = "ABCDEFGHIJKLM";
242        let felt = encode_symbol_to_felt(symbol);
243        assert_matches!(felt.unwrap_err(), TokenSymbolError::InvalidLength(13));
244
245        let symbol = "$$$";
246        let felt = encode_symbol_to_felt(symbol);
247        assert_matches!(felt.unwrap_err(), TokenSymbolError::InvalidCharacter);
248
249        let symbol = "ABCDEFGHIJKL";
250        let token_symbol = TokenSymbol::try_from(symbol);
251        assert!(token_symbol.is_ok());
252        let token_symbol_felt: Felt = token_symbol.unwrap().into();
253        assert_eq!(token_symbol_felt, encode_symbol_to_felt(symbol).unwrap());
254    }
255
256    /// Checks that if the encoded length of the token is less than the actual number of token
257    /// characters, [decode_felt_to_symbol] procedure should return the
258    /// [TokenSymbolError::DataNotFullyDecoded] error.
259    #[test]
260    fn test_invalid_token_len() {
261        // encoded value of this token has `6` as the length of the initial token string
262        let encoded_symbol = TokenSymbol::try_from("ABCDEF").unwrap();
263
264        // decrease encoded length by, for example, `3`
265        let invalid_encoded_symbol_u64 = Felt::from(encoded_symbol).as_int() - 3;
266
267        // check that `decode_felt_to_symbol()` procedure returns an error in attempt to create a
268        // token from encoded token with invalid length
269        let err = decode_felt_to_symbol(Felt::new(invalid_encoded_symbol_u64)).unwrap_err();
270        assert_matches!(err, TokenSymbolError::DataNotFullyDecoded);
271    }
272
273    /// Utility test just to make sure that the [TokenSymbol::MAX_ENCODED_VALUE] constant still
274    /// represents the maximum possible encoded value.
275    #[test]
276    fn test_token_symbol_max_value() {
277        let token_symbol = TokenSymbol::try_from("ZZZZZZZZZZZZ").unwrap();
278        assert_eq!(Felt::from(token_symbol).as_int(), TokenSymbol::MAX_ENCODED_VALUE);
279    }
280
281    // Const function tests
282    // --------------------------------------------------------------------------------------------
283
284    const _TOKEN0: TokenSymbol = TokenSymbol::from_static_str("A");
285    const _TOKEN1: TokenSymbol = TokenSymbol::from_static_str("ETH");
286    const _TOKEN2: TokenSymbol = TokenSymbol::from_static_str("MIDEN");
287    const _TOKEN3: TokenSymbol = TokenSymbol::from_static_str("ZZZZZZ");
288    const _TOKEN4: TokenSymbol = TokenSymbol::from_static_str("ABCDEFGH");
289    const _TOKEN5: TokenSymbol = TokenSymbol::from_static_str("ZZZZZZZZZZZZ");
290
291    #[test]
292    fn test_from_static_str_matches_new() {
293        // Test that from_static_str produces the same result as new
294        let symbols = ["A", "BC", "ETH", "MIDEN", "ZZZZZZ", "ABCDEFGH", "ZZZZZZZZZZZZ"];
295        for symbol in symbols {
296            let from_new = TokenSymbol::new(symbol).unwrap();
297            let from_static = TokenSymbol::from_static_str(symbol);
298            assert_eq!(
299                Felt::from(from_new),
300                Felt::from(from_static),
301                "Mismatch for symbol: {}",
302                symbol
303            );
304        }
305    }
306
307    #[test]
308    #[should_panic(expected = "invalid token symbol")]
309    fn token_symbol_panics_on_empty_string() {
310        TokenSymbol::from_static_str("");
311    }
312
313    #[test]
314    #[should_panic(expected = "invalid token symbol")]
315    fn token_symbol_panics_on_too_long_string() {
316        TokenSymbol::from_static_str("ABCDEFGHIJKLM");
317    }
318
319    #[test]
320    #[should_panic(expected = "invalid token symbol")]
321    fn token_symbol_panics_on_lowercase() {
322        TokenSymbol::from_static_str("eth");
323    }
324
325    #[test]
326    #[should_panic(expected = "invalid token symbol")]
327    fn token_symbol_panics_on_invalid_character() {
328        TokenSymbol::from_static_str("ET$");
329    }
330
331    #[test]
332    #[should_panic(expected = "invalid token symbol")]
333    fn token_symbol_panics_on_number() {
334        TokenSymbol::from_static_str("ETH1");
335    }
336}