miden_objects/asset/
mod.rs

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