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, 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)]
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_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 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_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
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 = 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 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 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 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#[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; 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 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 assert_eq!(word[0], Felt::ZERO); 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}