miden_standards/account/faucets/
token_metadata.rs1use 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
8static 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#[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 pub const MAX_DECIMALS: u8 = 12;
43
44 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 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 pub fn metadata_slot() -> &'static StorageSlotName {
108 &METADATA_SLOT_NAME
109 }
110
111 pub fn token_supply(&self) -> Felt {
113 self.token_supply
114 }
115
116 pub fn max_supply(&self) -> Felt {
118 self.max_supply
119 }
120
121 pub fn decimals(&self) -> u8 {
123 self.decimals
124 }
125
126 pub fn symbol(&self) -> TokenSymbol {
128 self.symbol
129 }
130
131 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
154impl TryFrom<Word> for TokenMetadata {
158 type Error = FungibleFaucetError;
159
160 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 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 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 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#[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; 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 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 assert_eq!(word[0], Felt::ZERO); 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}