miden_objects/asset/
mod.rs

1use super::account::AccountType;
2use super::utils::serde::{
3    ByteReader,
4    ByteWriter,
5    Deserializable,
6    DeserializationError,
7    Serializable,
8};
9use super::{AssetError, Felt, Hasher, TokenSymbolError, Word, ZERO};
10use crate::account::AccountIdPrefix;
11
12mod fungible;
13use alloc::boxed::Box;
14
15pub use fungible::FungibleAsset;
16
17mod nonfungible;
18
19pub use nonfungible::{NonFungibleAsset, NonFungibleAssetDetails};
20
21mod token_symbol;
22pub use token_symbol::TokenSymbol;
23
24mod vault;
25pub use vault::{AssetVault, AssetVaultKey, AssetWitness, PartialVault};
26
27// ASSET
28// ================================================================================================
29
30/// A fungible or a non-fungible asset.
31///
32/// All assets are encoded using a single word (4 elements) such that it is easy to determine the
33/// type of an asset both inside and outside Miden VM. Specifically:
34///
35/// Element 1 of the asset will be:
36/// - ZERO for a fungible asset.
37/// - non-ZERO for a non-fungible asset.
38///
39/// Element 3 of both asset types is an [`AccountIdPrefix`] or equivalently, the prefix of an
40/// [`AccountId`](crate::account::AccountId), which can be used to distinguish assets
41/// based on [`AccountIdPrefix::account_type`].
42///
43/// For element 3 of the vault keys of assets, the bit at index 5 (referred to as the
44/// "fungible bit" will be):
45/// - `1` for a fungible asset.
46/// - `0` for a non-fungible asset.
47///
48/// The above properties guarantee that there can never be a collision between a fungible and a
49/// non-fungible asset.
50///
51/// The methodology for constructing fungible and non-fungible assets is described below.
52///
53/// # Fungible assets
54///
55/// - A fungible asset's data layout is: `[amount, 0, faucet_id_suffix, faucet_id_prefix]`.
56/// - A fungible asset's vault key layout is: `[0, 0, faucet_id_suffix, faucet_id_prefix]`.
57///
58/// The most significant elements of a fungible asset are set to the prefix (`faucet_id_prefix`) and
59/// suffix (`faucet_id_suffix`) of the ID of the faucet which issues the asset. This guarantees the
60/// properties described above (the fungible bit is `1`).
61///
62/// The least significant element is set to the amount of the asset. This amount cannot be greater
63/// than [`FungibleAsset::MAX_AMOUNT`] and thus fits into a felt.
64///
65/// Elements 1 and 2 are set to ZERO.
66///
67/// It is impossible to find a collision between two fungible assets issued by different faucets as
68/// the faucet_id is included in the description of the asset and this is guaranteed to be different
69/// for each 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, faucet_id_prefix]`.
74/// - A non-fungible asset's vault key layout is: `[faucet_id_prefix, hash1, hash2, hash0']`, where
75///   `hash0'` is equivalent to `hash0` with the fungible bit set to `0`. See
76///   [`NonFungibleAsset::vault_key`] for more details.
77///
78/// The 4 elements of non-fungible assets are computed as follows:
79/// - First the asset data is hashed. This compresses an asset of an arbitrary length to 4 field
80///   elements: `[hash0, hash1, hash2, hash3]`.
81/// - `hash3` is then replaced with the prefix of the faucet ID (`faucet_id_prefix`) which issues
82///   the asset: `[hash0, hash1, hash2, faucet_id_prefix]`.
83///
84/// It is impossible to find a collision between two non-fungible assets issued by different faucets
85/// as the faucet_id is included in the description of the non-fungible asset and this is guaranteed
86/// to be different as per the faucet creation logic. Collision resistance for non-fungible assets
87/// issued by the same faucet is ~2^95.
88#[derive(Debug, Copy, Clone, PartialEq, Eq)]
89pub enum Asset {
90    Fungible(FungibleAsset),
91    NonFungible(NonFungibleAsset),
92}
93
94impl Asset {
95    /// Creates a new [Asset] without checking its validity.
96    pub(crate) fn new_unchecked(value: Word) -> Asset {
97        if is_not_a_non_fungible_asset(value) {
98            Asset::Fungible(FungibleAsset::new_unchecked(value))
99        } else {
100            Asset::NonFungible(unsafe { NonFungibleAsset::new_unchecked(value) })
101        }
102    }
103
104    /// Returns true if this asset is the same as the specified asset.
105    ///
106    /// Two assets are defined to be the same if:
107    /// - For fungible assets, if they were issued by the same faucet.
108    /// - For non-fungible assets, if the assets are identical.
109    pub fn is_same(&self, other: &Self) -> bool {
110        use Asset::*;
111        match (self, other) {
112            (Fungible(l), Fungible(r)) => l.is_from_same_faucet(r),
113            (NonFungible(l), NonFungible(r)) => l == r,
114            _ => false,
115        }
116    }
117
118    /// Returns true if this asset is a fungible asset.
119    pub const fn is_fungible(&self) -> bool {
120        matches!(self, Self::Fungible(_))
121    }
122
123    /// Returns true if this asset is a non fungible asset.
124    pub const fn is_non_fungible(&self) -> bool {
125        matches!(self, Self::NonFungible(_))
126    }
127
128    /// Returns the prefix of the faucet ID which issued this asset.
129    ///
130    /// To get the full [`AccountId`](crate::account::AccountId) of a fungible asset the asset
131    /// must be matched on.
132    pub fn faucet_id_prefix(&self) -> AccountIdPrefix {
133        match self {
134            Self::Fungible(asset) => asset.faucet_id_prefix(),
135            Self::NonFungible(asset) => asset.faucet_id_prefix(),
136        }
137    }
138
139    /// Returns the key which is used to store this asset in the account vault.
140    pub fn vault_key(&self) -> AssetVaultKey {
141        match self {
142            Self::Fungible(asset) => asset.vault_key(),
143            Self::NonFungible(asset) => asset.vault_key(),
144        }
145    }
146
147    /// Returns the inner [`FungibleAsset`].
148    ///
149    /// # Panics
150    ///
151    /// Panics if the asset is non-fungible.
152    pub fn unwrap_fungible(&self) -> FungibleAsset {
153        match self {
154            Asset::Fungible(asset) => *asset,
155            Asset::NonFungible(_) => panic!("the asset is non-fungible"),
156        }
157    }
158
159    /// Returns the inner [`NonFungibleAsset`].
160    ///
161    /// # Panics
162    ///
163    /// Panics if the asset is fungible.
164    pub fn unwrap_non_fungible(&self) -> NonFungibleAsset {
165        match self {
166            Asset::Fungible(_) => panic!("the asset is fungible"),
167            Asset::NonFungible(asset) => *asset,
168        }
169    }
170}
171
172impl From<Asset> for Word {
173    fn from(asset: Asset) -> Self {
174        match asset {
175            Asset::Fungible(asset) => asset.into(),
176            Asset::NonFungible(asset) => asset.into(),
177        }
178    }
179}
180
181impl From<&Asset> for Word {
182    fn from(value: &Asset) -> Self {
183        (*value).into()
184    }
185}
186
187impl TryFrom<&Word> for Asset {
188    type Error = AssetError;
189
190    fn try_from(value: &Word) -> Result<Self, Self::Error> {
191        (*value).try_into()
192    }
193}
194
195impl TryFrom<Word> for Asset {
196    type Error = AssetError;
197
198    fn try_from(value: Word) -> Result<Self, Self::Error> {
199        // Return an error if element 3 is not a valid account ID prefix, which cannot be checked by
200        // is_not_a_non_fungible_asset.
201        // Keep in mind serialized assets do _not_ carry the suffix required to reconstruct the full
202        // account identifier.
203        let prefix = AccountIdPrefix::try_from(value[3])
204            .map_err(|err| AssetError::InvalidFaucetAccountId(Box::from(err)))?;
205        match prefix.account_type() {
206            AccountType::FungibleFaucet => FungibleAsset::try_from(value).map(Asset::from),
207            AccountType::NonFungibleFaucet => NonFungibleAsset::try_from(value).map(Asset::from),
208            _ => Err(AssetError::InvalidFaucetAccountIdPrefix(prefix)),
209        }
210    }
211}
212
213// SERIALIZATION
214// ================================================================================================
215
216impl Serializable for Asset {
217    fn write_into<W: ByteWriter>(&self, target: &mut W) {
218        match self {
219            Asset::Fungible(fungible_asset) => fungible_asset.write_into(target),
220            Asset::NonFungible(non_fungible_asset) => non_fungible_asset.write_into(target),
221        }
222    }
223
224    fn get_size_hint(&self) -> usize {
225        match self {
226            Asset::Fungible(fungible_asset) => fungible_asset.get_size_hint(),
227            Asset::NonFungible(non_fungible_asset) => non_fungible_asset.get_size_hint(),
228        }
229    }
230}
231
232impl Deserializable for Asset {
233    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
234        // Both asset types have their faucet ID prefix as the first element, so we can use it to
235        // inspect what type of asset it is.
236        let faucet_id_prefix: AccountIdPrefix = source.read()?;
237
238        match faucet_id_prefix.account_type() {
239            AccountType::FungibleFaucet => {
240                FungibleAsset::deserialize_with_faucet_id_prefix(faucet_id_prefix, source)
241                    .map(Asset::from)
242            },
243            AccountType::NonFungibleFaucet => {
244                NonFungibleAsset::deserialize_with_faucet_id_prefix(faucet_id_prefix, source)
245                    .map(Asset::from)
246            },
247            other_type => Err(DeserializationError::InvalidValue(format!(
248                "failed to deserialize asset: expected an account ID prefix of type faucet, found {other_type:?}"
249            ))),
250        }
251    }
252}
253
254// HELPER FUNCTIONS
255// ================================================================================================
256
257/// Returns `true` if asset in [Word] is not a non-fungible asset.
258///
259/// Note: this does not mean that the word is a fungible asset as the word may contain a value
260/// which is not a valid asset.
261fn is_not_a_non_fungible_asset(asset: Word) -> bool {
262    match AccountIdPrefix::try_from(asset[3]) {
263        Ok(prefix) => {
264            matches!(prefix.account_type(), AccountType::FungibleFaucet)
265        },
266        Err(_err) => {
267            #[cfg(debug_assertions)]
268            panic!("invalid account ID prefix passed to is_not_a_non_fungible_asset: {_err}");
269            #[cfg(not(debug_assertions))]
270            false
271        },
272    }
273}
274
275// TESTS
276// ================================================================================================
277
278#[cfg(test)]
279mod tests {
280
281    use miden_crypto::Word;
282    use miden_crypto::utils::{Deserializable, Serializable};
283
284    use super::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails};
285    use crate::account::{AccountId, AccountIdPrefix};
286    use crate::testing::account_id::{
287        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
288        ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
289        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
290        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
291        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
292        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
293        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
294        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
295    };
296
297    #[test]
298    fn test_asset_serde() {
299        for fungible_account_id in [
300            ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
301            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
302            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
303            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
304            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
305        ] {
306            let account_id = AccountId::try_from(fungible_account_id).unwrap();
307            let fungible_asset: Asset = FungibleAsset::new(account_id, 10).unwrap().into();
308            assert_eq!(fungible_asset, Asset::read_from_bytes(&fungible_asset.to_bytes()).unwrap());
309        }
310
311        for non_fungible_account_id in [
312            ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
313            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
314            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
315        ] {
316            let account_id = AccountId::try_from(non_fungible_account_id).unwrap();
317            let details = NonFungibleAssetDetails::new(account_id.prefix(), vec![1, 2, 3]).unwrap();
318            let non_fungible_asset: Asset = NonFungibleAsset::new(&details).unwrap().into();
319            assert_eq!(
320                non_fungible_asset,
321                Asset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap()
322            );
323        }
324    }
325
326    #[test]
327    fn test_new_unchecked() {
328        for fungible_account_id in [
329            ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
330            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
331            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
332            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
333            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
334        ] {
335            let account_id = AccountId::try_from(fungible_account_id).unwrap();
336            let fungible_asset: Asset = FungibleAsset::new(account_id, 10).unwrap().into();
337            assert_eq!(fungible_asset, Asset::new_unchecked(Word::from(&fungible_asset)));
338        }
339
340        for non_fungible_account_id in [
341            ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
342            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
343            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
344        ] {
345            let account_id = AccountId::try_from(non_fungible_account_id).unwrap();
346            let details = NonFungibleAssetDetails::new(account_id.prefix(), vec![1, 2, 3]).unwrap();
347            let non_fungible_asset: Asset = NonFungibleAsset::new(&details).unwrap().into();
348            assert_eq!(non_fungible_asset, Asset::new_unchecked(Word::from(non_fungible_asset)));
349        }
350    }
351
352    /// This test asserts that account ID's prefix is serialized in the first felt of assets.
353    /// Asset deserialization relies on that fact and if this changes the serialization must
354    /// be updated.
355    #[test]
356    fn test_account_id_prefix_is_in_first_serialized_felt() {
357        for asset in [FungibleAsset::mock(300), NonFungibleAsset::mock(&[0xaa, 0xbb])] {
358            let serialized_asset = asset.to_bytes();
359            let prefix = AccountIdPrefix::read_from_bytes(&serialized_asset).unwrap();
360            assert_eq!(prefix, asset.faucet_id_prefix());
361        }
362    }
363}