miden_protocol/asset/vault/
asset_id.rs1use alloc::boxed::Box;
2use alloc::string::ToString;
3use core::fmt;
4
5use miden_crypto::merkle::smt::LeafIndex;
6use miden_crypto_derive::WordWrapper;
7
8use crate::account::{AccountId, AssetCallbackFlag};
9use crate::asset::vault::AssetClass;
10use crate::asset::{Asset, AssetComposition, FungibleAsset, NonFungibleAsset};
11use crate::crypto::merkle::smt::SMT_DEPTH;
12use crate::errors::AssetError;
13use crate::utils::serde::{
14 ByteReader,
15 ByteWriter,
16 Deserializable,
17 DeserializationError,
18 Serializable,
19};
20use crate::{Felt, Hasher, Word};
21
22#[derive(Debug, PartialEq, Eq, Clone, Copy)]
42pub struct AssetId {
43 asset_class: AssetClass,
45
46 faucet_id: AccountId,
48
49 composition: AssetComposition,
51}
52
53impl AssetId {
54 const FUNGIBLE_SERIALIZED_SIZE: usize =
58 AssetComposition::SERIALIZED_SIZE + AccountId::SERIALIZED_SIZE;
59
60 const NON_FUNGIBLE_SERIALIZED_SIZE: usize =
62 Self::FUNGIBLE_SERIALIZED_SIZE + AssetClass::SERIALIZED_SIZE;
63
64 pub(in crate::asset) const METADATA_BYTE_MASK: u8 = 0xff;
69
70 pub(in crate::asset) const COMPOSITION_MASK: u8 = 0b11;
74
75 pub(in crate::asset) const METADATA_RESERVED_MASK: u8 = 0b1111_1100;
77
78 pub fn new(
90 asset_class: AssetClass,
91 faucet_id: AccountId,
92 composition: AssetComposition,
93 ) -> Result<Self, AssetError> {
94 if composition.is_custom() {
96 return Err(AssetError::UnsupportedAssetComposition(AssetComposition::Custom));
97 }
98
99 if composition.is_fungible() && !asset_class.is_empty() {
100 return Err(AssetError::FungibleAssetClassMustBeZero(asset_class));
101 }
102
103 Ok(Self { asset_class, faucet_id, composition })
104 }
105
106 pub fn new_fungible(faucet_id: AccountId) -> Self {
108 Self::new(AssetClass::default(), faucet_id, AssetComposition::Fungible).expect(
109 "passing AssetComposition::Fungible together with AssetClass::default should be valid",
110 )
111 }
112
113 pub fn to_word(&self) -> Word {
120 let faucet_suffix = self.faucet_id.suffix().as_canonical_u64();
121 debug_assert!(
124 faucet_suffix & Self::METADATA_BYTE_MASK as u64 == 0,
125 "lower 8 bits of faucet suffix must be zero",
126 );
127 let metadata_byte = self.composition.as_u8();
128 let faucet_id_suffix_and_metadata = faucet_suffix | metadata_byte as u64;
129 let faucet_id_suffix_and_metadata = Felt::try_from(faucet_id_suffix_and_metadata)
130 .expect("highest bit should still be zero resulting in a valid felt");
131
132 Word::new([
133 self.asset_class.suffix(),
134 self.asset_class.prefix(),
135 faucet_id_suffix_and_metadata,
136 self.faucet_id.prefix().as_felt(),
137 ])
138 }
139
140 pub fn asset_class(&self) -> AssetClass {
143 self.asset_class
144 }
145
146 pub fn faucet_id(&self) -> AccountId {
148 self.faucet_id
149 }
150
151 pub fn callback_flag(&self) -> AssetCallbackFlag {
153 self.faucet_id.asset_callback_flag()
154 }
155
156 pub fn composition(&self) -> AssetComposition {
158 self.composition
159 }
160
161 pub fn hash(&self) -> AssetIdHash {
164 AssetIdHash::from_raw(Hasher::hash_elements(self.to_word().as_elements()))
165 }
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, WordWrapper)]
176pub struct AssetIdHash(Word);
177
178impl AssetIdHash {
179 pub fn to_leaf_index(&self) -> LeafIndex<SMT_DEPTH> {
181 self.0.into()
182 }
183}
184
185impl From<AssetIdHash> for Word {
186 fn from(id_hash: AssetIdHash) -> Self {
187 id_hash.0
188 }
189}
190
191impl From<AssetId> for AssetIdHash {
192 fn from(id: AssetId) -> Self {
193 id.hash()
194 }
195}
196
197impl From<AssetId> for Word {
201 fn from(asset_id: AssetId) -> Self {
202 asset_id.to_word()
203 }
204}
205
206impl Ord for AssetId {
207 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
209 self.to_word().cmp(&other.to_word())
210 }
211}
212
213impl PartialOrd for AssetId {
214 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
215 Some(self.cmp(other))
216 }
217}
218
219impl TryFrom<Word> for AssetId {
220 type Error = AssetError;
221
222 fn try_from(id: Word) -> Result<Self, Self::Error> {
232 let asset_class_suffix = id[0];
233 let asset_class_prefix = id[1];
234 let faucet_id_suffix_and_metadata = id[2];
235 let faucet_id_prefix = id[3];
236
237 let raw = faucet_id_suffix_and_metadata.as_canonical_u64();
238 let metadata_byte = (raw & Self::METADATA_BYTE_MASK as u64) as u8;
239
240 if metadata_byte & Self::METADATA_RESERVED_MASK != 0 {
242 return Err(AssetError::ReservedAssetMetadata(metadata_byte));
243 }
244
245 let composition = AssetComposition::try_from(metadata_byte & Self::COMPOSITION_MASK)?;
246
247 let faucet_id_suffix = Felt::try_from(raw & !(Self::METADATA_BYTE_MASK as u64))
248 .expect("clearing lower bits should not produce an invalid felt");
249
250 let asset_class = AssetClass::new(asset_class_suffix, asset_class_prefix);
251 let faucet_id = AccountId::try_from_elements(faucet_id_suffix, faucet_id_prefix)
252 .map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?;
253
254 Self::new(asset_class, faucet_id, composition)
255 }
256}
257
258impl fmt::Display for AssetId {
259 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260 f.write_str(&self.to_word().to_hex())
261 }
262}
263
264impl From<Asset> for AssetId {
265 fn from(asset: Asset) -> Self {
266 asset.id()
267 }
268}
269
270impl From<FungibleAsset> for AssetId {
271 fn from(fungible_asset: FungibleAsset) -> Self {
272 fungible_asset.id()
273 }
274}
275
276impl From<NonFungibleAsset> for AssetId {
277 fn from(non_fungible_asset: NonFungibleAsset) -> Self {
278 non_fungible_asset.id()
279 }
280}
281
282impl Serializable for AssetId {
286 fn write_into<W: ByteWriter>(&self, target: &mut W) {
290 target.write(self.composition);
292 target.write(self.faucet_id);
293
294 if !self.composition.is_fungible() {
295 target.write(self.asset_class);
296 }
297 }
298
299 fn get_size_hint(&self) -> usize {
300 if self.composition.is_fungible() {
301 Self::FUNGIBLE_SERIALIZED_SIZE
302 } else {
303 Self::NON_FUNGIBLE_SERIALIZED_SIZE
304 }
305 }
306}
307
308impl Deserializable for AssetId {
309 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
310 let composition: AssetComposition = source.read()?;
311 let faucet_id: AccountId = source.read()?;
312 let asset_class = if composition.is_fungible() {
313 AssetClass::default()
314 } else {
315 source.read()?
316 };
317
318 Self::new(asset_class, faucet_id, composition)
319 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
320 }
321}
322
323#[cfg(test)]
327mod tests {
328 use assert_matches::assert_matches;
329
330 use super::*;
331 use crate::asset::AssetComposition;
332 use crate::asset::tests::{asset_metadata, set_asset_metadata};
333 use crate::testing::account_id::{
334 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
335 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
336 };
337
338 #[test]
339 fn asset_id_word_roundtrip() -> anyhow::Result<()> {
340 let fungible_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?;
341 let nonfungible_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET)?;
342
343 let id = AssetId::new(AssetClass::default(), fungible_faucet, AssetComposition::Fungible)?;
345 assert_eq!(id.composition(), AssetComposition::Fungible);
346 let roundtripped = AssetId::try_from(id.to_word())?;
347 assert_eq!(id, roundtripped);
348 assert_eq!(id, AssetId::read_from_bytes(&id.to_bytes())?);
349 assert_eq!(id.to_bytes().len(), AssetId::FUNGIBLE_SERIALIZED_SIZE);
350 assert_eq!(id.to_bytes().len(), id.get_size_hint());
351
352 let id = AssetId::new(
354 AssetClass::new(Felt::from(42u32), Felt::from(99u32)),
355 nonfungible_faucet,
356 AssetComposition::None,
357 )?;
358 assert_eq!(id.composition(), AssetComposition::None);
359 let roundtripped = AssetId::try_from(id.to_word())?;
360 assert_eq!(id, roundtripped);
361 assert_eq!(id, AssetId::read_from_bytes(&id.to_bytes())?);
362 assert_eq!(id.to_bytes().len(), AssetId::NON_FUNGIBLE_SERIALIZED_SIZE);
363 assert_eq!(id.to_bytes().len(), id.get_size_hint());
364
365 Ok(())
366 }
367
368 #[test]
369 fn decoding_word_with_reserved_bits_set_fails() -> anyhow::Result<()> {
370 let id = FungibleAsset::mock(42).id();
371 let valid_metadata = asset_metadata(id);
372 let word = set_asset_metadata(id, valid_metadata | AssetId::METADATA_RESERVED_MASK);
374
375 let err = AssetId::try_from(word).unwrap_err();
376 assert_matches!(err, AssetError::ReservedAssetMetadata(_));
377
378 Ok(())
379 }
380
381 #[test]
382 fn decoding_word_with_invalid_composition_value_fails() -> anyhow::Result<()> {
383 let id = FungibleAsset::mock(42).id();
384 let invalid_metadata = AssetId::COMPOSITION_MASK;
386 let word = set_asset_metadata(id, invalid_metadata);
387
388 let err = AssetId::try_from(word).unwrap_err();
389 assert_matches!(err, AssetError::UnknownAssetComposition(_));
390
391 Ok(())
392 }
393}