Skip to main content

miden_protocol/asset/
mod.rs

1use super::errors::{AssetError, TokenSymbolError};
2use super::utils::serde::{
3    ByteReader,
4    ByteWriter,
5    Deserializable,
6    DeserializationError,
7    Serializable,
8};
9use super::{Felt, Word};
10use crate::account::AccountId;
11
12mod asset_amount;
13pub use asset_amount::AssetAmount;
14
15mod fungible;
16
17pub use fungible::FungibleAsset;
18
19mod nonfungible;
20
21pub use nonfungible::{NonFungibleAsset, NonFungibleAssetDetails};
22
23mod token_symbol;
24pub use token_symbol::TokenSymbol;
25
26mod asset_callbacks;
27pub use asset_callbacks::AssetCallbacks;
28
29mod asset_composition;
30pub use asset_composition::AssetComposition;
31
32mod vault;
33pub use vault::{AssetClass, AssetId, AssetIdHash, AssetVault, AssetWitness, PartialVault};
34
35// ASSET
36// ================================================================================================
37
38/// A fungible or a non-fungible asset.
39///
40/// All assets are encoded as the asset ID of the asset and its value, each represented as one word
41/// (4 elements). This makes it is easy to determine the type of an asset both inside and outside
42/// Miden VM. Specifically:
43///
44/// The asset ID of an asset contains the [`AssetComposition`] which describes how assets compose,
45/// meaning whether they can be merged or split.
46///
47/// This property guarantees that there can never be a collision between a fungible and a
48/// non-fungible asset.
49///
50/// The methodology for constructing fungible and non-fungible assets is described below.
51///
52/// # Fungible assets
53///
54/// - A fungible asset's value layout is: `[amount, 0, 0, 0]`.
55/// - A fungible asset's ID layout is: `[0, 0, faucet_id_suffix_and_metadata, faucet_id_prefix]`.
56///
57/// Where:
58/// - `amount` is the [`AssetAmount`] that the asset holds and cannot be greater than
59///   [`AssetAmount::MAX`] and thus fits into a felt.
60/// - the remaining elements in the value word must be zero.
61/// - `faucet_id_prefix` is the prefix of the faucet ID which issues the asset.
62/// - `faucet_id_suffix_and_metadata` is the suffix of the faucet ID which issues the asset and the
63///   asset metadata ([`AssetComposition`]). See [`AssetId`] for more details on the ID's layout.
64/// - the asset class limbs must be zero, which means two instances of the same fungible asset have
65///   the same asset ID and will be merged together when stored in the same account's vault.
66///
67/// It is impossible to find a collision between two fungible assets issued by different faucets as
68/// the faucet ID is part of the asset's ID and this is guaranteed to be different for each
69/// faucet as per the faucet creation logic.
70///
71/// # Non-fungible assets
72///
73/// - A non-fungible asset's data layout is:      `[hash0, hash1, hash2, hash3]`.
74/// - A non-fungible asset's ID layout is: `[hash0, hash1, faucet_id_suffix_and_metadata,
75///   faucet_id_prefix]`.
76///
77/// Where:
78/// - the 4 elements of non-fungible asset values are computed by hashing the asset data. This
79///   compresses an asset of an arbitrary length to 4 field elements.
80/// - `faucet_id_prefix` is the prefix of the faucet ID which issues the asset.
81/// - `faucet_id_suffix_and_metadata` is the suffix of the faucet ID which issues the asset and the
82///   asset metadata ([`AssetComposition`]). See [`AssetId`] for more details on the ID's layout.
83/// - The asset class limbs are set to hashes from the asset's value (`hash0` and `hash1`).
84///
85/// It is impossible to find a collision between two non-fungible assets issued by different faucets
86/// as the faucet ID is part of the asset's ID and this is guaranteed to be different as per
87/// the faucet creation logic.
88///
89/// The collision resistance of non-fungible assets issued by the same faucet is ~2^64, due to the
90/// 128-bit asset class that is unique per non-fungible asset. In other words, two non-fungible
91/// assets issued by the same faucet are very unlikely to have the same asset ID and thus should
92/// not collide when stored in the same account's vault.
93#[derive(Debug, Copy, Clone, PartialEq, Eq)]
94pub enum Asset {
95    Fungible(FungibleAsset),
96    NonFungible(NonFungibleAsset),
97}
98
99impl Asset {
100    /// Creates an asset from the provided ID and value.
101    ///
102    /// # Errors
103    ///
104    /// Returns an error if:
105    /// - [`FungibleAsset::from_id_and_value`] or [`NonFungibleAsset::from_id_and_value`] fails.
106    pub fn from_id_and_value(id: AssetId, value: Word) -> Result<Self, AssetError> {
107        match id.composition() {
108            AssetComposition::Fungible => {
109                FungibleAsset::from_id_and_value(id, value).map(Asset::Fungible)
110            },
111            AssetComposition::None => {
112                NonFungibleAsset::from_id_and_value(id, value).map(Asset::NonFungible)
113            },
114            AssetComposition::Custom => {
115                Err(AssetError::UnsupportedAssetComposition(AssetComposition::Custom))
116            },
117        }
118    }
119
120    /// Creates an asset from the provided ID and value.
121    ///
122    /// Prefer [`Self::from_id_and_value`] for more type safety.
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if:
127    /// - The provided ID does not contain a valid faucet ID.
128    /// - [`Self::from_id_and_value`] fails.
129    pub fn from_id_and_value_words(id: Word, value: Word) -> Result<Self, AssetError> {
130        let asset_id = AssetId::try_from(id)?;
131        Self::from_id_and_value(asset_id, value)
132    }
133
134    /// Returns true if this asset is the same as the specified asset.
135    ///
136    /// Two assets are defined to be the same if their asset IDs match.
137    pub fn is_same(&self, other: &Self) -> bool {
138        self.id() == other.id()
139    }
140
141    /// Returns true if this asset is a fungible asset.
142    pub fn is_fungible(&self) -> bool {
143        matches!(self, Self::Fungible(_))
144    }
145
146    /// Returns true if this asset is a non fungible asset.
147    pub fn is_non_fungible(&self) -> bool {
148        matches!(self, Self::NonFungible(_))
149    }
150
151    /// Returns the ID of the faucet that issued this asset.
152    pub fn faucet_id(&self) -> AccountId {
153        match self {
154            Self::Fungible(asset) => asset.faucet_id(),
155            Self::NonFungible(asset) => asset.faucet_id(),
156        }
157    }
158
159    /// Returns the [`AssetId`] which uniquely identifies this asset in the account vault.
160    pub fn id(&self) -> AssetId {
161        match self {
162            Self::Fungible(asset) => asset.id(),
163            Self::NonFungible(asset) => asset.id(),
164        }
165    }
166
167    /// Returns the asset's [`AssetId`] encoded to a [`Word`].
168    pub fn to_id_word(&self) -> Word {
169        self.id().to_word()
170    }
171
172    /// Returns the asset's value encoded to a [`Word`].
173    pub fn to_value_word(&self) -> Word {
174        match self {
175            Asset::Fungible(fungible_asset) => fungible_asset.to_value_word(),
176            Asset::NonFungible(non_fungible_asset) => non_fungible_asset.to_value_word(),
177        }
178    }
179
180    /// Returns the asset encoded as elements.
181    ///
182    /// The first four elements contain the asset ID and the last four elements contain the asset
183    /// value.
184    pub fn as_elements(&self) -> [Felt; 8] {
185        let mut elements = [Felt::ZERO; 8];
186        elements[0..4].copy_from_slice(self.to_id_word().as_elements());
187        elements[4..8].copy_from_slice(self.to_value_word().as_elements());
188        elements
189    }
190
191    /// Returns the inner [`FungibleAsset`].
192    ///
193    /// # Panics
194    ///
195    /// Panics if the asset is non-fungible.
196    pub fn unwrap_fungible(&self) -> FungibleAsset {
197        match self {
198            Asset::Fungible(asset) => *asset,
199            Asset::NonFungible(_) => panic!("the asset is non-fungible"),
200        }
201    }
202
203    /// Returns the inner [`NonFungibleAsset`].
204    ///
205    /// # Panics
206    ///
207    /// Panics if the asset is fungible.
208    pub fn unwrap_non_fungible(&self) -> NonFungibleAsset {
209        match self {
210            Asset::Fungible(_) => panic!("the asset is fungible"),
211            Asset::NonFungible(asset) => *asset,
212        }
213    }
214}
215
216// SERIALIZATION
217// ================================================================================================
218
219impl Serializable for Asset {
220    fn write_into<W: ByteWriter>(&self, target: &mut W) {
221        match self {
222            Asset::Fungible(fungible_asset) => fungible_asset.write_into(target),
223            Asset::NonFungible(non_fungible_asset) => non_fungible_asset.write_into(target),
224        }
225    }
226
227    fn get_size_hint(&self) -> usize {
228        match self {
229            Asset::Fungible(fungible_asset) => fungible_asset.get_size_hint(),
230            Asset::NonFungible(non_fungible_asset) => non_fungible_asset.get_size_hint(),
231        }
232    }
233}
234
235impl Deserializable for Asset {
236    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
237        // All assets have their composition serialized as the first byte, so we can use it to
238        // inspect what type of asset it is.
239        let composition: AssetComposition = source.read()?;
240        match composition {
241            AssetComposition::Fungible => FungibleAsset::deserialize_body(source).map(Asset::from),
242            AssetComposition::None => NonFungibleAsset::deserialize_body(source).map(Asset::from),
243            AssetComposition::Custom => Err(DeserializationError::InvalidValue(
244                "Custom asset composition is not supported".into(),
245            )),
246        }
247    }
248}
249
250// TESTS
251// ================================================================================================
252
253#[cfg(test)]
254mod tests {
255
256    use assert_matches::assert_matches;
257    use miden_core::Word;
258    use miden_crypto::utils::{Deserializable, Serializable};
259
260    use super::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails};
261    use crate::Felt;
262    use crate::account::AccountId;
263    use crate::asset::{AssetClass, AssetComposition, AssetId};
264    use crate::errors::AssetError;
265    use crate::testing::account_id::{
266        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
267        ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
268        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
269        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
270        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
271        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
272        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
273        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
274    };
275
276    /// Returns the metadata byte encoded in an asset ID word.
277    pub(super) fn asset_metadata(id: AssetId) -> u8 {
278        (id.to_word()[2].as_canonical_u64() & AssetId::METADATA_BYTE_MASK as u64) as u8
279    }
280
281    /// Overwrites the metadata byte of the third element of an asset ID word.
282    pub(super) fn set_asset_metadata(id: AssetId, byte: u8) -> Word {
283        let mut id_word = id.to_word();
284        let raw = id_word[2].as_canonical_u64();
285        let new_raw = (raw & !(AssetId::METADATA_BYTE_MASK as u64)) | byte as u64;
286        id_word[2] =
287            Felt::try_from(new_raw).expect("clearing lower bits should produce a valid felt");
288        id_word
289    }
290
291    /// Tests the serialization roundtrip for assets for assets <-> bytes and assets <-> words.
292    #[test]
293    fn test_asset_serde() -> anyhow::Result<()> {
294        for fungible_account_id in [
295            ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
296            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
297            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
298            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
299            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
300        ] {
301            let account_id = AccountId::try_from(fungible_account_id).unwrap();
302            let fungible_asset: Asset = FungibleAsset::new(account_id, 10).unwrap().into();
303            assert_eq!(fungible_asset, Asset::read_from_bytes(&fungible_asset.to_bytes()).unwrap());
304            assert_eq!(
305                fungible_asset,
306                Asset::from_id_and_value_words(
307                    fungible_asset.to_id_word(),
308                    fungible_asset.to_value_word()
309                )?,
310            );
311        }
312
313        for non_fungible_account_id in [
314            ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
315            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
316            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
317        ] {
318            let account_id = AccountId::try_from(non_fungible_account_id).unwrap();
319            let details = NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]);
320            let non_fungible_asset: Asset = NonFungibleAsset::new(&details).into();
321            assert_eq!(
322                non_fungible_asset,
323                Asset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap()
324            );
325            assert_eq!(
326                non_fungible_asset,
327                Asset::from_id_and_value_words(
328                    non_fungible_asset.to_id_word(),
329                    non_fungible_asset.to_value_word()
330                )?
331            );
332        }
333
334        Ok(())
335    }
336
337    /// Asserts that every fully-serialized asset leads with an [`AssetComposition`] byte that
338    /// reflects the asset variant. Asset deserialization relies on this discriminator.
339    #[test]
340    fn test_composition_byte_is_serialized_first() {
341        let fungible_bytes = FungibleAsset::mock(300).to_bytes();
342        assert_eq!(fungible_bytes[0], AssetComposition::Fungible.as_u8());
343
344        let non_fungible_bytes = NonFungibleAsset::mock(&[0xaa, 0xbb]).to_bytes();
345        assert_eq!(non_fungible_bytes[0], AssetComposition::None.as_u8());
346    }
347
348    /// `Asset::from_id_and_value` must reject a [`AssetComposition::Custom`] asset ID with
349    /// `UnsupportedAssetComposition`.
350    #[test]
351    fn test_from_id_and_value_rejects_custom_composition() -> anyhow::Result<()> {
352        let err = AssetId::new(
353            AssetClass::default(),
354            ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?,
355            AssetComposition::Custom,
356        )
357        .unwrap_err();
358
359        assert_matches!(err, AssetError::UnsupportedAssetComposition(AssetComposition::Custom));
360
361        Ok(())
362    }
363}