Skip to main content

miden_standards/account/faucets/
token_metadata.rs

1use miden_protocol::account::{AccountStorage, StorageSlot, StorageSlotName};
2use miden_protocol::asset::{FungibleAsset, TokenSymbol};
3use miden_protocol::utils::sync::LazyLock;
4use miden_protocol::{Felt, FieldElement, Word};
5
6use super::FungibleFaucetError;
7
8// CONSTANTS
9// ================================================================================================
10
11static METADATA_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
12    StorageSlotName::new("miden::standards::fungible_faucets::metadata")
13        .expect("storage slot name should be valid")
14});
15
16// TOKEN METADATA
17// ================================================================================================
18
19/// Token metadata for fungible faucet accounts.
20///
21/// This struct encapsulates the metadata associated with a fungible token faucet:
22/// - `token_supply`: The current amount of tokens issued by the faucet.
23/// - `max_supply`: The maximum amount of tokens that can be issued.
24/// - `decimals`: The number of decimal places for token amounts.
25/// - `symbol`: The token symbol.
26///
27/// The metadata is stored in a single storage slot as:
28/// `[token_supply, max_supply, decimals, symbol]`
29#[derive(Debug, Clone, Copy)]
30pub struct TokenMetadata {
31    token_supply: Felt,
32    max_supply: Felt,
33    decimals: u8,
34    symbol: TokenSymbol,
35}
36
37impl TokenMetadata {
38    // CONSTANTS
39    // --------------------------------------------------------------------------------------------
40
41    /// The maximum number of decimals supported.
42    pub const MAX_DECIMALS: u8 = 12;
43
44    // CONSTRUCTORS
45    // --------------------------------------------------------------------------------------------
46
47    /// Creates a new [`TokenMetadata`] with the specified metadata and zero token supply.
48    ///
49    /// # Errors
50    /// Returns an error if:
51    /// - The decimals parameter exceeds [`Self::MAX_DECIMALS`].
52    /// - The max supply parameter exceeds [`FungibleAsset::MAX_AMOUNT`].
53    pub fn new(
54        symbol: TokenSymbol,
55        decimals: u8,
56        max_supply: Felt,
57    ) -> Result<Self, FungibleFaucetError> {
58        Self::with_supply(symbol, decimals, max_supply, Felt::ZERO)
59    }
60
61    /// Creates a new [`TokenMetadata`] with the specified metadata and token supply.
62    ///
63    /// # Errors
64    /// Returns an error if:
65    /// - The decimals parameter exceeds [`Self::MAX_DECIMALS`].
66    /// - The max supply parameter exceeds [`FungibleAsset::MAX_AMOUNT`].
67    /// - The token supply exceeds the max supply.
68    pub fn with_supply(
69        symbol: TokenSymbol,
70        decimals: u8,
71        max_supply: Felt,
72        token_supply: Felt,
73    ) -> Result<Self, FungibleFaucetError> {
74        if decimals > Self::MAX_DECIMALS {
75            return Err(FungibleFaucetError::TooManyDecimals {
76                actual: decimals as u64,
77                max: Self::MAX_DECIMALS,
78            });
79        }
80
81        if max_supply.as_int() > FungibleAsset::MAX_AMOUNT {
82            return Err(FungibleFaucetError::MaxSupplyTooLarge {
83                actual: max_supply.as_int(),
84                max: FungibleAsset::MAX_AMOUNT,
85            });
86        }
87
88        if token_supply.as_int() > max_supply.as_int() {
89            return Err(FungibleFaucetError::TokenSupplyExceedsMaxSupply {
90                token_supply: token_supply.as_int(),
91                max_supply: max_supply.as_int(),
92            });
93        }
94
95        Ok(Self {
96            token_supply,
97            max_supply,
98            decimals,
99            symbol,
100        })
101    }
102
103    // PUBLIC ACCESSORS
104    // --------------------------------------------------------------------------------------------
105
106    /// Returns the [`StorageSlotName`] where the token metadata is stored.
107    pub fn metadata_slot() -> &'static StorageSlotName {
108        &METADATA_SLOT_NAME
109    }
110
111    /// Returns the current token supply (amount issued).
112    pub fn token_supply(&self) -> Felt {
113        self.token_supply
114    }
115
116    /// Returns the maximum token supply.
117    pub fn max_supply(&self) -> Felt {
118        self.max_supply
119    }
120
121    /// Returns the number of decimals.
122    pub fn decimals(&self) -> u8 {
123        self.decimals
124    }
125
126    /// Returns the token symbol.
127    pub fn symbol(&self) -> TokenSymbol {
128        self.symbol
129    }
130
131    // MUTATORS
132    // --------------------------------------------------------------------------------------------
133
134    /// Sets the token_supply (in base units).
135    ///
136    /// # Errors
137    ///
138    /// Returns an error if:
139    /// - the token supply exceeds the max supply.
140    pub fn with_token_supply(mut self, token_supply: Felt) -> Result<Self, FungibleFaucetError> {
141        if token_supply.as_int() > self.max_supply.as_int() {
142            return Err(FungibleFaucetError::TokenSupplyExceedsMaxSupply {
143                token_supply: token_supply.as_int(),
144                max_supply: self.max_supply.as_int(),
145            });
146        }
147
148        self.token_supply = token_supply;
149
150        Ok(self)
151    }
152}
153
154// TRAIT IMPLEMENTATIONS
155// ================================================================================================
156
157impl TryFrom<Word> for TokenMetadata {
158    type Error = FungibleFaucetError;
159
160    /// Parses token metadata from a Word.
161    ///
162    /// The Word is expected to be in the format: `[token_supply, max_supply, decimals, symbol]`
163    fn try_from(word: Word) -> Result<Self, Self::Error> {
164        let [token_supply, max_supply, decimals, token_symbol] = *word;
165
166        let symbol =
167            TokenSymbol::try_from(token_symbol).map_err(FungibleFaucetError::InvalidTokenSymbol)?;
168
169        let decimals =
170            decimals.as_int().try_into().map_err(|_| FungibleFaucetError::TooManyDecimals {
171                actual: decimals.as_int(),
172                max: Self::MAX_DECIMALS,
173            })?;
174
175        Self::with_supply(symbol, decimals, max_supply, token_supply)
176    }
177}
178
179impl From<TokenMetadata> for Word {
180    fn from(metadata: TokenMetadata) -> Self {
181        // Storage layout: [token_supply, max_supply, decimals, symbol]
182        Word::new([
183            metadata.token_supply,
184            metadata.max_supply,
185            Felt::from(metadata.decimals),
186            metadata.symbol.into(),
187        ])
188    }
189}
190
191impl From<TokenMetadata> for StorageSlot {
192    fn from(metadata: TokenMetadata) -> Self {
193        StorageSlot::with_value(TokenMetadata::metadata_slot().clone(), metadata.into())
194    }
195}
196
197impl TryFrom<&StorageSlot> for TokenMetadata {
198    type Error = FungibleFaucetError;
199
200    /// Tries to create [`TokenMetadata`] from a storage slot.
201    ///
202    /// # Errors
203    /// Returns an error if:
204    /// - The slot name does not match the expected metadata slot name.
205    /// - The slot value cannot be parsed as valid token metadata.
206    fn try_from(slot: &StorageSlot) -> Result<Self, Self::Error> {
207        if slot.name() != Self::metadata_slot() {
208            return Err(FungibleFaucetError::SlotNameMismatch {
209                expected: Self::metadata_slot().clone(),
210                actual: slot.name().clone(),
211            });
212        }
213        TokenMetadata::try_from(slot.value())
214    }
215}
216
217impl TryFrom<&AccountStorage> for TokenMetadata {
218    type Error = FungibleFaucetError;
219
220    /// Tries to create [`TokenMetadata`] from account storage.
221    fn try_from(storage: &AccountStorage) -> Result<Self, Self::Error> {
222        let metadata_word = storage.get_item(TokenMetadata::metadata_slot()).map_err(|err| {
223            FungibleFaucetError::StorageLookupFailed {
224                slot_name: TokenMetadata::metadata_slot().clone(),
225                source: err,
226            }
227        })?;
228
229        TokenMetadata::try_from(metadata_word)
230    }
231}
232
233// TESTS
234// ================================================================================================
235
236#[cfg(test)]
237mod tests {
238    use miden_protocol::asset::TokenSymbol;
239    use miden_protocol::{Felt, FieldElement, Word};
240
241    use super::*;
242
243    #[test]
244    fn token_metadata_new() {
245        let symbol = TokenSymbol::new("TEST").unwrap();
246        let decimals = 8u8;
247        let max_supply = Felt::new(1_000_000);
248
249        let metadata = TokenMetadata::new(symbol, decimals, max_supply).unwrap();
250
251        assert_eq!(metadata.symbol(), symbol);
252        assert_eq!(metadata.decimals(), decimals);
253        assert_eq!(metadata.max_supply(), max_supply);
254        assert_eq!(metadata.token_supply(), Felt::ZERO);
255    }
256
257    #[test]
258    fn token_metadata_with_supply() {
259        let symbol = TokenSymbol::new("TEST").unwrap();
260        let decimals = 8u8;
261        let max_supply = Felt::new(1_000_000);
262        let token_supply = Felt::new(500_000);
263
264        let metadata =
265            TokenMetadata::with_supply(symbol, decimals, max_supply, token_supply).unwrap();
266
267        assert_eq!(metadata.symbol(), symbol);
268        assert_eq!(metadata.decimals(), decimals);
269        assert_eq!(metadata.max_supply(), max_supply);
270        assert_eq!(metadata.token_supply(), token_supply);
271    }
272
273    #[test]
274    fn token_metadata_too_many_decimals() {
275        let symbol = TokenSymbol::new("TEST").unwrap();
276        let decimals = 13u8; // exceeds MAX_DECIMALS
277        let max_supply = Felt::new(1_000_000);
278
279        let result = TokenMetadata::new(symbol, decimals, max_supply);
280        assert!(matches!(result, Err(FungibleFaucetError::TooManyDecimals { .. })));
281    }
282
283    #[test]
284    fn token_metadata_max_supply_too_large() {
285        use miden_protocol::asset::FungibleAsset;
286
287        let symbol = TokenSymbol::new("TEST").unwrap();
288        let decimals = 8u8;
289        // FungibleAsset::MAX_AMOUNT is 2^63 - 1, so we use MAX_AMOUNT + 1 to exceed it
290        let max_supply = Felt::new(FungibleAsset::MAX_AMOUNT + 1);
291
292        let result = TokenMetadata::new(symbol, decimals, max_supply);
293        assert!(matches!(result, Err(FungibleFaucetError::MaxSupplyTooLarge { .. })));
294    }
295
296    #[test]
297    fn token_metadata_to_word() {
298        let symbol = TokenSymbol::new("POL").unwrap();
299        let decimals = 2u8;
300        let max_supply = Felt::new(123);
301
302        let metadata = TokenMetadata::new(symbol, decimals, max_supply).unwrap();
303        let word: Word = metadata.into();
304
305        // Storage layout: [token_supply, max_supply, decimals, symbol]
306        assert_eq!(word[0], Felt::ZERO); // token_supply
307        assert_eq!(word[1], max_supply);
308        assert_eq!(word[2], Felt::from(decimals));
309        assert_eq!(word[3], symbol.into());
310    }
311
312    #[test]
313    fn token_metadata_from_storage_slot() {
314        let symbol = TokenSymbol::new("POL").unwrap();
315        let decimals = 2u8;
316        let max_supply = Felt::new(123);
317
318        let original = TokenMetadata::new(symbol, decimals, max_supply).unwrap();
319        let slot: StorageSlot = original.into();
320
321        let restored = TokenMetadata::try_from(&slot).unwrap();
322
323        assert_eq!(restored.symbol(), symbol);
324        assert_eq!(restored.decimals(), decimals);
325        assert_eq!(restored.max_supply(), max_supply);
326        assert_eq!(restored.token_supply(), Felt::ZERO);
327    }
328
329    #[test]
330    fn token_metadata_roundtrip_with_supply() {
331        let symbol = TokenSymbol::new("POL").unwrap();
332        let decimals = 2u8;
333        let max_supply = Felt::new(1000);
334        let token_supply = Felt::new(500);
335
336        let original =
337            TokenMetadata::with_supply(symbol, decimals, max_supply, token_supply).unwrap();
338        let word: Word = original.into();
339        let restored = TokenMetadata::try_from(word).unwrap();
340
341        assert_eq!(restored.symbol(), symbol);
342        assert_eq!(restored.decimals(), decimals);
343        assert_eq!(restored.max_supply(), max_supply);
344        assert_eq!(restored.token_supply(), token_supply);
345    }
346}