Skip to main content

miden_protocol/asset/
mod.rs

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