miden_objects/asset/
mod.rs

1use super::{
2    AssetError, Felt, Hasher, TokenSymbolError, Word, ZERO,
3    account::AccountType,
4    utils::serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
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, PartialVault};
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 [`FungibleAsset`].
141    ///
142    /// # Panics
143    ///
144    /// Panics if the asset is non-fungible.
145    pub fn unwrap_fungible(&self) -> FungibleAsset {
146        match self {
147            Asset::Fungible(asset) => *asset,
148            Asset::NonFungible(_) => panic!("the asset is non-fungible"),
149        }
150    }
151
152    /// Returns the inner [`NonFungibleAsset`].
153    ///
154    /// # Panics
155    ///
156    /// Panics if the asset is fungible.
157    pub fn unwrap_non_fungible(&self) -> NonFungibleAsset {
158        match self {
159            Asset::Fungible(_) => panic!("the asset is fungible"),
160            Asset::NonFungible(asset) => *asset,
161        }
162    }
163}
164
165impl From<Asset> for Word {
166    fn from(asset: Asset) -> Self {
167        match asset {
168            Asset::Fungible(asset) => asset.into(),
169            Asset::NonFungible(asset) => asset.into(),
170        }
171    }
172}
173
174impl From<&Asset> for Word {
175    fn from(value: &Asset) -> Self {
176        (*value).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        (*value).try_into()
185    }
186}
187
188impl TryFrom<Word> for Asset {
189    type Error = AssetError;
190
191    fn try_from(value: Word) -> Result<Self, Self::Error> {
192        if is_not_a_non_fungible_asset(value) {
193            FungibleAsset::try_from(value).map(Asset::from)
194        } else {
195            NonFungibleAsset::try_from(value).map(Asset::from)
196        }
197    }
198}
199
200// SERIALIZATION
201// ================================================================================================
202
203impl Serializable for Asset {
204    fn write_into<W: ByteWriter>(&self, target: &mut W) {
205        match self {
206            Asset::Fungible(fungible_asset) => fungible_asset.write_into(target),
207            Asset::NonFungible(non_fungible_asset) => non_fungible_asset.write_into(target),
208        }
209    }
210
211    fn get_size_hint(&self) -> usize {
212        match self {
213            Asset::Fungible(fungible_asset) => fungible_asset.get_size_hint(),
214            Asset::NonFungible(non_fungible_asset) => non_fungible_asset.get_size_hint(),
215        }
216    }
217}
218
219impl Deserializable for Asset {
220    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
221        // Both asset types have their faucet ID prefix as the first element, so we can use it to
222        // inspect what type of asset it is.
223        let faucet_id_prefix: AccountIdPrefix = source.read()?;
224
225        match faucet_id_prefix.account_type() {
226            AccountType::FungibleFaucet => {
227                FungibleAsset::deserialize_with_faucet_id_prefix(faucet_id_prefix, source)
228                    .map(Asset::from)
229            },
230            AccountType::NonFungibleFaucet => {
231                NonFungibleAsset::deserialize_with_faucet_id_prefix(faucet_id_prefix, source)
232                    .map(Asset::from)
233            },
234            other_type => Err(DeserializationError::InvalidValue(format!(
235                "failed to deserialize asset: expected an account ID prefix of type faucet, found {other_type:?}"
236            ))),
237        }
238    }
239}
240
241// HELPER FUNCTIONS
242// ================================================================================================
243
244/// Returns `true` if asset in [Word] is not a non-fungible asset.
245///
246/// Note: this does not mean that the word is a fungible asset as the word may contain a value
247/// which is not a valid asset.
248fn is_not_a_non_fungible_asset(asset: Word) -> bool {
249    match AccountIdPrefix::try_from(asset[3]) {
250        Ok(prefix) => {
251            matches!(prefix.account_type(), AccountType::FungibleFaucet)
252        },
253        Err(_err) => {
254            #[cfg(debug_assertions)]
255            panic!("invalid account ID prefix passed to is_not_a_non_fungible_asset: {_err}");
256            #[cfg(not(debug_assertions))]
257            false
258        },
259    }
260}
261
262// TESTS
263// ================================================================================================
264
265#[cfg(test)]
266mod tests {
267
268    use miden_crypto::{
269        Word,
270        utils::{Deserializable, Serializable},
271    };
272
273    use super::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails};
274    use crate::{
275        account::{AccountId, AccountIdPrefix},
276        testing::account_id::{
277            ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
278            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
279            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
280            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
281        },
282    };
283
284    #[test]
285    fn test_asset_serde() {
286        for fungible_account_id in [
287            ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
288            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
289            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
290            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
291            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
292        ] {
293            let account_id = AccountId::try_from(fungible_account_id).unwrap();
294            let fungible_asset: Asset = FungibleAsset::new(account_id, 10).unwrap().into();
295            assert_eq!(fungible_asset, Asset::read_from_bytes(&fungible_asset.to_bytes()).unwrap());
296        }
297
298        for non_fungible_account_id in [
299            ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
300            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
301            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
302        ] {
303            let account_id = AccountId::try_from(non_fungible_account_id).unwrap();
304            let details = NonFungibleAssetDetails::new(account_id.prefix(), vec![1, 2, 3]).unwrap();
305            let non_fungible_asset: Asset = NonFungibleAsset::new(&details).unwrap().into();
306            assert_eq!(
307                non_fungible_asset,
308                Asset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap()
309            );
310        }
311    }
312
313    #[test]
314    fn test_new_unchecked() {
315        for fungible_account_id in [
316            ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
317            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
318            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
319            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
320            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
321        ] {
322            let account_id = AccountId::try_from(fungible_account_id).unwrap();
323            let fungible_asset: Asset = FungibleAsset::new(account_id, 10).unwrap().into();
324            assert_eq!(fungible_asset, Asset::new_unchecked(Word::from(&fungible_asset)));
325        }
326
327        for non_fungible_account_id in [
328            ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
329            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
330            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
331        ] {
332            let account_id = AccountId::try_from(non_fungible_account_id).unwrap();
333            let details = NonFungibleAssetDetails::new(account_id.prefix(), vec![1, 2, 3]).unwrap();
334            let non_fungible_asset: Asset = NonFungibleAsset::new(&details).unwrap().into();
335            assert_eq!(non_fungible_asset, Asset::new_unchecked(Word::from(non_fungible_asset)));
336        }
337    }
338
339    /// This test asserts that account ID's prefix is serialized in the first felt of assets.
340    /// Asset deserialization relies on that fact and if this changes the serialization must
341    /// be updated.
342    #[test]
343    fn test_account_id_prefix_is_in_first_serialized_felt() {
344        for asset in [FungibleAsset::mock(300), NonFungibleAsset::mock(&[0xaa, 0xbb])] {
345            let serialized_asset = asset.to_bytes();
346            let prefix = AccountIdPrefix::read_from_bytes(&serialized_asset).unwrap();
347            assert_eq!(prefix, asset.faucet_id_prefix());
348        }
349    }
350}