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