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
22type AssetIdVersion = u8;
23
24#[derive(Debug, PartialEq, Eq, Clone, Copy)]
44pub struct AssetId {
45 asset_class: AssetClass,
47
48 faucet_id: AccountId,
50
51 composition: AssetComposition,
53}
54
55impl AssetId {
56 const FUNGIBLE_SERIALIZED_SIZE: usize = core::mem::size_of::<AssetIdVersion>()
60 + AssetComposition::SERIALIZED_SIZE
61 + AccountId::SERIALIZED_SIZE;
62
63 const NON_FUNGIBLE_SERIALIZED_SIZE: usize =
65 Self::FUNGIBLE_SERIALIZED_SIZE + AssetClass::SERIALIZED_SIZE;
66
67 pub(in crate::asset) const METADATA_BYTE_MASK: u8 = 0xff;
72
73 pub(in crate::asset) const VERSION_1: u8 = 1;
75
76 pub(in crate::asset) const VERSION_MASK: u8 = 0b1111;
78
79 pub(in crate::asset) const COMPOSITION_SHIFT: u8 = 4;
81
82 pub(in crate::asset) const METADATA_RESERVED_MASK: u8 = 0b1100_0000;
84
85 pub fn new(
97 asset_class: AssetClass,
98 faucet_id: AccountId,
99 composition: AssetComposition,
100 ) -> Result<Self, AssetError> {
101 if composition.is_custom() {
103 return Err(AssetError::UnsupportedAssetComposition(AssetComposition::Custom));
104 }
105
106 if composition.is_fungible() && !asset_class.is_empty() {
107 return Err(AssetError::FungibleAssetClassMustBeZero(asset_class));
108 }
109
110 Ok(Self { asset_class, faucet_id, composition })
111 }
112
113 pub fn new_fungible(faucet_id: AccountId) -> Self {
115 Self::new(AssetClass::default(), faucet_id, AssetComposition::Fungible).expect(
116 "passing AssetComposition::Fungible together with AssetClass::default should be valid",
117 )
118 }
119
120 pub fn to_word(&self) -> Word {
127 let faucet_suffix = self.faucet_id.suffix().as_canonical_u64();
128 debug_assert!(
131 faucet_suffix & Self::METADATA_BYTE_MASK as u64 == 0,
132 "lower 8 bits of faucet suffix must be zero",
133 );
134 let metadata_byte = Self::encode_metadata(self.composition);
135 let faucet_id_suffix_and_metadata = faucet_suffix | metadata_byte as u64;
136 let faucet_id_suffix_and_metadata = Felt::try_from(faucet_id_suffix_and_metadata)
137 .expect("highest bit should still be zero resulting in a valid felt");
138
139 Word::new([
140 self.asset_class.suffix(),
141 self.asset_class.prefix(),
142 faucet_id_suffix_and_metadata,
143 self.faucet_id.prefix().as_felt(),
144 ])
145 }
146
147 pub fn asset_class(&self) -> AssetClass {
150 self.asset_class
151 }
152
153 pub fn faucet_id(&self) -> AccountId {
155 self.faucet_id
156 }
157
158 pub fn callback_flag(&self) -> AssetCallbackFlag {
160 self.faucet_id.asset_callback_flag()
161 }
162
163 pub fn composition(&self) -> AssetComposition {
165 self.composition
166 }
167
168 pub fn hash(&self) -> AssetIdHash {
171 AssetIdHash::from_raw(Hasher::hash_elements(self.to_word().as_elements()))
172 }
173
174 pub(in crate::asset) fn encode_metadata(composition: AssetComposition) -> u8 {
179 (composition.as_u8() << Self::COMPOSITION_SHIFT) | Self::VERSION_1
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, WordWrapper)]
191pub struct AssetIdHash(Word);
192
193impl AssetIdHash {
194 pub fn to_leaf_index(&self) -> LeafIndex<SMT_DEPTH> {
196 self.0.into()
197 }
198}
199
200impl From<AssetIdHash> for Word {
201 fn from(id_hash: AssetIdHash) -> Self {
202 id_hash.0
203 }
204}
205
206impl From<AssetId> for AssetIdHash {
207 fn from(id: AssetId) -> Self {
208 id.hash()
209 }
210}
211
212impl From<AssetId> for Word {
216 fn from(asset_id: AssetId) -> Self {
217 asset_id.to_word()
218 }
219}
220
221impl Ord for AssetId {
222 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
224 self.to_word().cmp(&other.to_word())
225 }
226}
227
228impl PartialOrd for AssetId {
229 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
230 Some(self.cmp(other))
231 }
232}
233
234impl TryFrom<Word> for AssetId {
235 type Error = AssetError;
236
237 fn try_from(id: Word) -> Result<Self, Self::Error> {
248 let asset_class_suffix = id[0];
249 let asset_class_prefix = id[1];
250 let faucet_id_suffix_and_metadata = id[2];
251 let faucet_id_prefix = id[3];
252
253 let raw = faucet_id_suffix_and_metadata.as_canonical_u64();
254 let metadata_byte = (raw & Self::METADATA_BYTE_MASK as u64) as u8;
255
256 let version = metadata_byte & Self::VERSION_MASK;
258 if version != Self::VERSION_1 {
259 return Err(AssetError::UnknownAssetIdVersion(version));
260 }
261
262 if metadata_byte & Self::METADATA_RESERVED_MASK != 0 {
264 return Err(AssetError::ReservedAssetMetadata(metadata_byte));
265 }
266
267 let composition = AssetComposition::try_from(metadata_byte >> Self::COMPOSITION_SHIFT)?;
268
269 let faucet_id_suffix = Felt::try_from(raw & !(Self::METADATA_BYTE_MASK as u64))
270 .expect("clearing lower bits should not produce an invalid felt");
271
272 let asset_class = AssetClass::new(asset_class_suffix, asset_class_prefix);
273 let faucet_id = AccountId::try_from_elements(faucet_id_suffix, faucet_id_prefix)
274 .map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?;
275
276 Self::new(asset_class, faucet_id, composition)
277 }
278}
279
280impl fmt::Display for AssetId {
281 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282 f.write_str(&self.to_word().to_hex())
283 }
284}
285
286impl From<Asset> for AssetId {
287 fn from(asset: Asset) -> Self {
288 asset.id()
289 }
290}
291
292impl From<FungibleAsset> for AssetId {
293 fn from(fungible_asset: FungibleAsset) -> Self {
294 fungible_asset.id()
295 }
296}
297
298impl From<NonFungibleAsset> for AssetId {
299 fn from(non_fungible_asset: NonFungibleAsset) -> Self {
300 non_fungible_asset.id()
301 }
302}
303
304impl Serializable for AssetId {
308 fn write_into<W: ByteWriter>(&self, target: &mut W) {
312 target.write(AssetId::VERSION_1);
313 target.write(self.composition);
314 target.write(self.faucet_id);
315
316 if !self.composition.is_fungible() {
317 target.write(self.asset_class);
318 }
319 }
320
321 fn get_size_hint(&self) -> usize {
322 if self.composition.is_fungible() {
323 Self::FUNGIBLE_SERIALIZED_SIZE
324 } else {
325 Self::NON_FUNGIBLE_SERIALIZED_SIZE
326 }
327 }
328}
329
330impl Deserializable for AssetId {
331 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
332 let version: u8 = source.read()?;
333
334 if version != Self::VERSION_1 {
335 return Err(DeserializationError::InvalidValue(format!(
336 "asset version is {} but only version {} is supported",
337 version,
338 Self::VERSION_1,
339 )));
340 }
341
342 let composition: AssetComposition = source.read()?;
343 let faucet_id: AccountId = source.read()?;
344 let asset_class = if composition.is_fungible() {
345 AssetClass::default()
346 } else {
347 source.read()?
348 };
349
350 Self::new(asset_class, faucet_id, composition)
351 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
352 }
353}
354
355#[cfg(test)]
359mod tests {
360 use assert_matches::assert_matches;
361
362 use super::*;
363 use crate::asset::AssetComposition;
364 use crate::asset::tests::{asset_metadata, set_asset_metadata};
365 use crate::testing::account_id::{
366 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
367 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
368 };
369
370 #[test]
371 fn asset_id_word_roundtrip() -> anyhow::Result<()> {
372 let fungible_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?;
373 let nonfungible_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET)?;
374
375 let id = AssetId::new(AssetClass::default(), fungible_faucet, AssetComposition::Fungible)?;
377 assert_eq!(id.composition(), AssetComposition::Fungible);
378 let roundtripped = AssetId::try_from(id.to_word())?;
379 assert_eq!(id, roundtripped);
380 assert_eq!(id, AssetId::read_from_bytes(&id.to_bytes())?);
381 assert_eq!(id.to_bytes().len(), AssetId::FUNGIBLE_SERIALIZED_SIZE);
382 assert_eq!(id.to_bytes().len(), id.get_size_hint());
383
384 let id = AssetId::new(
386 AssetClass::new(Felt::from(42u32), Felt::from(99u32)),
387 nonfungible_faucet,
388 AssetComposition::None,
389 )?;
390 assert_eq!(id.composition(), AssetComposition::None);
391 let roundtripped = AssetId::try_from(id.to_word())?;
392 assert_eq!(id, roundtripped);
393 assert_eq!(id, AssetId::read_from_bytes(&id.to_bytes())?);
394 assert_eq!(id.to_bytes().len(), AssetId::NON_FUNGIBLE_SERIALIZED_SIZE);
395 assert_eq!(id.to_bytes().len(), id.get_size_hint());
396
397 Ok(())
398 }
399
400 #[rstest::rstest]
402 #[case::version_zero(0, AssetError::UnknownAssetIdVersion(0))]
403 #[case::unknown_version(AssetId::VERSION_1 + 1, AssetError::UnknownAssetIdVersion(2))]
404 #[case::reserved_bits_set(
405 AssetId::encode_metadata(AssetComposition::Fungible) | AssetId::METADATA_RESERVED_MASK,
406 AssetError::ReservedAssetMetadata(0b1101_0001)
407 )]
408 #[case::unknown_composition(
410 0b0011_0000 | AssetId::VERSION_1,
411 AssetError::UnknownAssetComposition(0b11)
412 )]
413 fn decoding_word_with_invalid_metadata_fails(
414 #[case] metadata: u8,
415 #[case] expected_err: AssetError,
416 ) -> anyhow::Result<()> {
417 let word = set_asset_metadata(FungibleAsset::mock(42).id(), metadata);
418
419 let err = AssetId::try_from(word).unwrap_err();
420 assert_eq!(err.to_string(), expected_err.to_string());
421
422 Ok(())
423 }
424
425 #[test]
426 fn metadata_encodes_version_and_composition() -> anyhow::Result<()> {
427 let fungible =
428 AssetId::new_fungible(AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?);
429 assert_eq!(asset_metadata(fungible), 0b0001_0001);
430
431 let non_fungible = AssetId::new(
432 AssetClass::new(Felt::from(42u32), Felt::from(99u32)),
433 AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET)?,
434 AssetComposition::None,
435 )?;
436 assert_eq!(asset_metadata(non_fungible), 0b0000_0001);
437
438 Ok(())
439 }
440
441 #[test]
442 fn asset_id_deserialization_rejects_unsupported_version() {
443 let error = AssetId::read_from_bytes(&[0]).unwrap_err();
444
445 assert_matches!(error, DeserializationError::InvalidValue(message) => {
446 assert!(message.contains("asset version is 0"));
447 });
448 }
449}