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, 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)]
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_canonical_u64() > FungibleAsset::MAX_AMOUNT {
82            return Err(FungibleFaucetError::MaxSupplyTooLarge {
83                actual: max_supply.as_canonical_u64(),
84                max: FungibleAsset::MAX_AMOUNT,
85            });
86        }
87
88        if token_supply.as_canonical_u64() > max_supply.as_canonical_u64() {
89            return Err(FungibleFaucetError::TokenSupplyExceedsMaxSupply {
90                token_supply: token_supply.as_canonical_u64(),
91                max_supply: max_supply.as_canonical_u64(),
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_canonical_u64() > self.max_supply.as_canonical_u64() {
142            return Err(FungibleFaucetError::TokenSupplyExceedsMaxSupply {
143                token_supply: token_supply.as_canonical_u64(),
144                max_supply: self.max_supply.as_canonical_u64(),
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 = decimals.as_canonical_u64().try_into().map_err(|_| {
170            FungibleFaucetError::TooManyDecimals {
171                actual: decimals.as_canonical_u64(),
172                max: Self::MAX_DECIMALS,
173            }
174        })?;
175
176        Self::with_supply(symbol, decimals, max_supply, token_supply)
177    }
178}
179
180impl From<TokenMetadata> for Word {
181    fn from(metadata: TokenMetadata) -> Self {
182        // Storage layout: [token_supply, max_supply, decimals, symbol]
183        Word::new([
184            metadata.token_supply,
185            metadata.max_supply,
186            Felt::from(metadata.decimals),
187            metadata.symbol.as_element(),
188        ])
189    }
190}
191
192impl From<TokenMetadata> for StorageSlot {
193    fn from(metadata: TokenMetadata) -> Self {
194        StorageSlot::with_value(TokenMetadata::metadata_slot().clone(), metadata.into())
195    }
196}
197
198impl TryFrom<&StorageSlot> for TokenMetadata {
199    type Error = FungibleFaucetError;
200
201    /// Tries to create [`TokenMetadata`] from a storage slot.
202    ///
203    /// # Errors
204    /// Returns an error if:
205    /// - The slot name does not match the expected metadata slot name.
206    /// - The slot value cannot be parsed as valid token metadata.
207    fn try_from(slot: &StorageSlot) -> Result<Self, Self::Error> {
208        if slot.name() != Self::metadata_slot() {
209            return Err(FungibleFaucetError::SlotNameMismatch {
210                expected: Self::metadata_slot().clone(),
211                actual: slot.name().clone(),
212            });
213        }
214        TokenMetadata::try_from(slot.value())
215    }
216}
217
218impl TryFrom<&AccountStorage> for TokenMetadata {
219    type Error = FungibleFaucetError;
220
221    /// Tries to create [`TokenMetadata`] from account storage.
222    fn try_from(storage: &AccountStorage) -> Result<Self, Self::Error> {
223        let metadata_word = storage.get_item(TokenMetadata::metadata_slot()).map_err(|err| {
224            FungibleFaucetError::StorageLookupFailed {
225                slot_name: TokenMetadata::metadata_slot().clone(),
226                source: err,
227            }
228        })?;
229
230        TokenMetadata::try_from(metadata_word)
231    }
232}
233
234// TESTS
235// ================================================================================================
236
237#[cfg(test)]
238mod tests {
239    use miden_protocol::asset::TokenSymbol;
240    use miden_protocol::{Felt, Word};
241
242    use super::*;
243
244    #[test]
245    fn token_metadata_new() {
246        let symbol = TokenSymbol::new("TEST").unwrap();
247        let decimals = 8u8;
248        let max_supply = Felt::new(1_000_000);
249
250        let metadata = TokenMetadata::new(symbol.clone(), decimals, max_supply).unwrap();
251
252        assert_eq!(metadata.symbol(), &symbol);
253        assert_eq!(metadata.decimals(), decimals);
254        assert_eq!(metadata.max_supply(), max_supply);
255        assert_eq!(metadata.token_supply(), Felt::ZERO);
256    }
257
258    #[test]
259    fn token_metadata_with_supply() {
260        let symbol = TokenSymbol::new("TEST").unwrap();
261        let decimals = 8u8;
262        let max_supply = Felt::new(1_000_000);
263        let token_supply = Felt::new(500_000);
264
265        let metadata =
266            TokenMetadata::with_supply(symbol.clone(), decimals, max_supply, token_supply).unwrap();
267
268        assert_eq!(metadata.symbol(), &symbol);
269        assert_eq!(metadata.decimals(), decimals);
270        assert_eq!(metadata.max_supply(), max_supply);
271        assert_eq!(metadata.token_supply(), token_supply);
272    }
273
274    #[test]
275    fn token_metadata_too_many_decimals() {
276        let symbol = TokenSymbol::new("TEST").unwrap();
277        let decimals = 13u8; // exceeds MAX_DECIMALS
278        let max_supply = Felt::new(1_000_000);
279
280        let result = TokenMetadata::new(symbol, decimals, max_supply);
281        assert!(matches!(result, Err(FungibleFaucetError::TooManyDecimals { .. })));
282    }
283
284    #[test]
285    fn token_metadata_max_supply_too_large() {
286        use miden_protocol::asset::FungibleAsset;
287
288        let symbol = TokenSymbol::new("TEST").unwrap();
289        let decimals = 8u8;
290        // FungibleAsset::MAX_AMOUNT is 2^63 - 1, so we use MAX_AMOUNT + 1 to exceed it
291        let max_supply = Felt::new(FungibleAsset::MAX_AMOUNT + 1);
292
293        let result = TokenMetadata::new(symbol, decimals, max_supply);
294        assert!(matches!(result, Err(FungibleFaucetError::MaxSupplyTooLarge { .. })));
295    }
296
297    #[test]
298    fn token_metadata_to_word() {
299        let symbol = TokenSymbol::new("POL").unwrap();
300        let symbol_felt = symbol.as_element();
301        let decimals = 2u8;
302        let max_supply = Felt::new(123);
303
304        let metadata = TokenMetadata::new(symbol, decimals, max_supply).unwrap();
305        let word: Word = metadata.into();
306
307        // Storage layout: [token_supply, max_supply, decimals, symbol]
308        assert_eq!(word[0], Felt::ZERO); // token_supply
309        assert_eq!(word[1], max_supply);
310        assert_eq!(word[2], Felt::from(decimals));
311        assert_eq!(word[3], symbol_felt);
312    }
313
314    #[test]
315    fn token_metadata_from_storage_slot() {
316        let symbol = TokenSymbol::new("POL").unwrap();
317        let decimals = 2u8;
318        let max_supply = Felt::new(123);
319
320        let original = TokenMetadata::new(symbol.clone(), decimals, max_supply).unwrap();
321        let slot: StorageSlot = original.into();
322
323        let restored = TokenMetadata::try_from(&slot).unwrap();
324
325        assert_eq!(restored.symbol(), &symbol);
326        assert_eq!(restored.decimals(), decimals);
327        assert_eq!(restored.max_supply(), max_supply);
328        assert_eq!(restored.token_supply(), Felt::ZERO);
329    }
330
331    #[test]
332    fn token_metadata_roundtrip_with_supply() {
333        let symbol = TokenSymbol::new("POL").unwrap();
334        let decimals = 2u8;
335        let max_supply = Felt::new(1000);
336        let token_supply = Felt::new(500);
337
338        let original =
339            TokenMetadata::with_supply(symbol.clone(), decimals, max_supply, token_supply).unwrap();
340        let word: Word = original.into();
341        let restored = TokenMetadata::try_from(word).unwrap();
342
343        assert_eq!(restored.symbol(), &symbol);
344        assert_eq!(restored.decimals(), decimals);
345        assert_eq!(restored.max_supply(), max_supply);
346        assert_eq!(restored.token_supply(), token_supply);
347    }
348}