miden_protocol/asset/vault/
asset_id.rs1use alloc::boxed::Box;
2use alloc::string::{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 pub const SERIALIZED_SIZE: usize = Word::SERIALIZED_SIZE;
58
59 pub(in crate::asset) const METADATA_BYTE_MASK: u8 = 0xff;
64
65 pub(in crate::asset) const COMPOSITION_MASK: u8 = 0b11;
69
70 pub(in crate::asset) const METADATA_RESERVED_MASK: u8 = 0b1111_1100;
72
73 pub fn new(
85 asset_class: AssetClass,
86 faucet_id: AccountId,
87 composition: AssetComposition,
88 ) -> Result<Self, AssetError> {
89 if composition.is_custom() {
91 return Err(AssetError::UnsupportedAssetComposition(AssetComposition::Custom));
92 }
93
94 if composition.is_fungible() && !asset_class.is_empty() {
95 return Err(AssetError::FungibleAssetClassMustBeZero(asset_class));
96 }
97
98 Ok(Self { asset_class, faucet_id, composition })
99 }
100
101 pub fn new_fungible(faucet_id: AccountId) -> Self {
103 Self::new(AssetClass::default(), faucet_id, AssetComposition::Fungible).expect(
104 "passing AssetComposition::Fungible together with AssetClass::default should be valid",
105 )
106 }
107
108 pub fn to_word(&self) -> Word {
115 let faucet_suffix = self.faucet_id.suffix().as_canonical_u64();
116 debug_assert!(
119 faucet_suffix & Self::METADATA_BYTE_MASK as u64 == 0,
120 "lower 8 bits of faucet suffix must be zero",
121 );
122 let metadata_byte = self.composition.as_u8();
123 let faucet_id_suffix_and_metadata = faucet_suffix | metadata_byte as u64;
124 let faucet_id_suffix_and_metadata = Felt::try_from(faucet_id_suffix_and_metadata)
125 .expect("highest bit should still be zero resulting in a valid felt");
126
127 Word::new([
128 self.asset_class.suffix(),
129 self.asset_class.prefix(),
130 faucet_id_suffix_and_metadata,
131 self.faucet_id.prefix().as_felt(),
132 ])
133 }
134
135 pub fn asset_class(&self) -> AssetClass {
138 self.asset_class
139 }
140
141 pub fn faucet_id(&self) -> AccountId {
143 self.faucet_id
144 }
145
146 pub fn callback_flag(&self) -> AssetCallbackFlag {
148 self.faucet_id.asset_callback_flag()
149 }
150
151 pub fn composition(&self) -> AssetComposition {
153 self.composition
154 }
155
156 pub fn hash(&self) -> AssetIdHash {
159 AssetIdHash::from_raw(Hasher::hash_elements(self.to_word().as_elements()))
160 }
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, WordWrapper)]
171pub struct AssetIdHash(Word);
172
173impl AssetIdHash {
174 pub fn to_leaf_index(&self) -> LeafIndex<SMT_DEPTH> {
176 self.0.into()
177 }
178}
179
180impl From<AssetIdHash> for Word {
181 fn from(id_hash: AssetIdHash) -> Self {
182 id_hash.0
183 }
184}
185
186impl From<AssetId> for AssetIdHash {
187 fn from(id: AssetId) -> Self {
188 id.hash()
189 }
190}
191
192impl From<AssetId> for Word {
196 fn from(asset_id: AssetId) -> Self {
197 asset_id.to_word()
198 }
199}
200
201impl Ord for AssetId {
202 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
204 self.to_word().cmp(&other.to_word())
205 }
206}
207
208impl PartialOrd for AssetId {
209 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
210 Some(self.cmp(other))
211 }
212}
213
214impl TryFrom<Word> for AssetId {
215 type Error = AssetError;
216
217 fn try_from(id: Word) -> Result<Self, Self::Error> {
227 let asset_class_suffix = id[0];
228 let asset_class_prefix = id[1];
229 let faucet_id_suffix_and_metadata = id[2];
230 let faucet_id_prefix = id[3];
231
232 let raw = faucet_id_suffix_and_metadata.as_canonical_u64();
233 let metadata_byte = (raw & Self::METADATA_BYTE_MASK as u64) as u8;
234
235 if metadata_byte & Self::METADATA_RESERVED_MASK != 0 {
237 return Err(AssetError::ReservedAssetMetadata(metadata_byte));
238 }
239
240 let composition = AssetComposition::try_from(metadata_byte & Self::COMPOSITION_MASK)?;
241
242 let faucet_id_suffix = Felt::try_from(raw & !(Self::METADATA_BYTE_MASK as u64))
243 .expect("clearing lower bits should not produce an invalid felt");
244
245 let asset_class = AssetClass::new(asset_class_suffix, asset_class_prefix);
246 let faucet_id = AccountId::try_from_elements(faucet_id_suffix, faucet_id_prefix)
247 .map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?;
248
249 Self::new(asset_class, faucet_id, composition)
250 }
251}
252
253impl fmt::Display for AssetId {
254 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255 f.write_str(&self.to_word().to_hex())
256 }
257}
258
259impl From<Asset> for AssetId {
260 fn from(asset: Asset) -> Self {
261 asset.id()
262 }
263}
264
265impl From<FungibleAsset> for AssetId {
266 fn from(fungible_asset: FungibleAsset) -> Self {
267 fungible_asset.id()
268 }
269}
270
271impl From<NonFungibleAsset> for AssetId {
272 fn from(non_fungible_asset: NonFungibleAsset) -> Self {
273 non_fungible_asset.id()
274 }
275}
276
277impl Serializable for AssetId {
281 fn write_into<W: ByteWriter>(&self, target: &mut W) {
282 self.to_word().write_into(target);
283 }
284
285 fn get_size_hint(&self) -> usize {
286 Self::SERIALIZED_SIZE
287 }
288}
289
290impl Deserializable for AssetId {
291 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
292 let word: Word = source.read()?;
293 Self::try_from(word).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
294 }
295}
296
297#[cfg(test)]
301mod tests {
302 use assert_matches::assert_matches;
303
304 use super::*;
305 use crate::asset::AssetComposition;
306 use crate::asset::tests::{asset_metadata, set_asset_metadata};
307 use crate::testing::account_id::{
308 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
309 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
310 };
311
312 #[test]
313 fn asset_id_word_roundtrip() -> anyhow::Result<()> {
314 let fungible_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?;
315 let nonfungible_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET)?;
316
317 let id = AssetId::new(AssetClass::default(), fungible_faucet, AssetComposition::Fungible)?;
319 assert_eq!(id.composition(), AssetComposition::Fungible);
320 let roundtripped = AssetId::try_from(id.to_word())?;
321 assert_eq!(id, roundtripped);
322 assert_eq!(id, AssetId::read_from_bytes(&id.to_bytes())?);
323
324 let id = AssetId::new(
326 AssetClass::new(Felt::from(42u32), Felt::from(99u32)),
327 nonfungible_faucet,
328 AssetComposition::None,
329 )?;
330 assert_eq!(id.composition(), AssetComposition::None);
331 let roundtripped = AssetId::try_from(id.to_word())?;
332 assert_eq!(id, roundtripped);
333 assert_eq!(id, AssetId::read_from_bytes(&id.to_bytes())?);
334
335 Ok(())
336 }
337
338 #[test]
339 fn decoding_word_with_reserved_bits_set_fails() -> anyhow::Result<()> {
340 let id = FungibleAsset::mock(42).id();
341 let valid_metadata = asset_metadata(id);
342 let word = set_asset_metadata(id, valid_metadata | AssetId::METADATA_RESERVED_MASK);
344
345 let err = AssetId::try_from(word).unwrap_err();
346 assert_matches!(err, AssetError::ReservedAssetMetadata(_));
347
348 Ok(())
349 }
350
351 #[test]
352 fn decoding_word_with_invalid_composition_value_fails() -> anyhow::Result<()> {
353 let id = FungibleAsset::mock(42).id();
354 let invalid_metadata = AssetId::COMPOSITION_MASK;
356 let word = set_asset_metadata(id, invalid_metadata);
357
358 let err = AssetId::try_from(word).unwrap_err();
359 assert_matches!(err, AssetError::UnknownAssetComposition(_));
360
361 Ok(())
362 }
363}