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