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, Word};
11use crate::account::AccountId;
12
13mod fungible;
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::{AssetId, AssetVault, AssetVaultKey, AssetWitness, PartialVault};
26
27// ASSET
28// ================================================================================================
29
30/// A fungible or a non-fungible asset.
31///
32/// All assets are encoded as the vault key of the asset and its value, each represented as one word
33/// (4 elements). This makes it is easy to determine the type of an asset both inside and outside
34/// Miden VM. Specifically:
35///
36/// The vault key of an asset contains the [`AccountId`] of the faucet that issues the asset. It can
37/// be used to distinguish assets based on the encoded [`AccountId::account_type`]. In the vault
38/// keys of assets, the account type bits at index 4 and 5 determine whether the asset is fungible
39/// or non-fungible.
40///
41/// This property guarantees 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 value layout is: `[amount, 0, 0, 0]`.
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's key are set to the prefix
52/// (`faucet_id_prefix`) and suffix (`faucet_id_suffix`) of the ID of the faucet which issues the
53/// asset. The asset ID limbs are set to zero, which means two instances of the same fungible asset
54/// have the same asset key and will be merged together when stored in the same account's vault.
55///
56/// The least significant element of the value is set to the amount of the asset and the remaining
57/// felts are zero. This amount cannot be greater than [`FungibleAsset::MAX_AMOUNT`] and thus fits
58/// into a felt.
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, hash3]`.
67/// - A non-fungible asset's vault key layout is: `[hash0, hash1, faucet_id_suffix,
68///   faucet_id_prefix]`.
69///
70/// The 4 elements of non-fungible assets are computed by hashing the asset data. This compresses an
71/// asset of an arbitrary length to 4 field elements: `[hash0, hash1, hash2, hash3]`.
72///
73/// It is impossible to find a collision between two non-fungible assets issued by different faucets
74/// as the faucet ID is included in the description of the non-fungible asset and this is guaranteed
75/// to be different as per the faucet creation logic.
76///
77/// The most significant elements of a non-fungible asset's key are set to the prefix
78/// (`faucet_id_prefix`) and suffix (`faucet_id_suffix`) of the ID of the faucet which issues the
79/// asset. The asset ID limbs are set to hashes from the asset's value. This means the collision
80/// resistance of non-fungible assets issued by the same faucet is ~2^64, due to the 128-bit asset
81/// ID that is unique per non-fungible asset. In other words, two non-fungible assets issued by the
82/// same faucet are very unlikely to have the same asset key and thus should not collide when stored
83/// in the same account's vault.
84#[derive(Debug, Copy, Clone, PartialEq, Eq)]
85pub enum Asset {
86    Fungible(FungibleAsset),
87    NonFungible(NonFungibleAsset),
88}
89
90impl Asset {
91    /// Creates an asset from the provided key and value.
92    ///
93    /// # Errors
94    ///
95    /// Returns an error if:
96    /// - [`FungibleAsset::from_key_value`] or [`NonFungibleAsset::from_key_value`] fails.
97    pub fn from_key_value(key: AssetVaultKey, value: Word) -> Result<Self, AssetError> {
98        if matches!(key.faucet_id().account_type(), AccountType::FungibleFaucet) {
99            FungibleAsset::from_key_value(key, value).map(Asset::Fungible)
100        } else {
101            NonFungibleAsset::from_key_value(key, value).map(Asset::NonFungible)
102        }
103    }
104
105    /// Creates an asset from the provided key and value.
106    ///
107    /// Prefer [`Self::from_key_value`] for more type safety.
108    ///
109    /// # Errors
110    ///
111    /// Returns an error if:
112    /// - The provided key does not contain a valid faucet ID.
113    /// - [`Self::from_key_value`] fails.
114    pub fn from_key_value_words(key: Word, value: Word) -> Result<Self, AssetError> {
115        let vault_key = AssetVaultKey::try_from(key)?;
116        Self::from_key_value(vault_key, value)
117    }
118
119    /// Returns true if this asset is the same as the specified asset.
120    ///
121    /// Two assets are defined to be the same if:
122    /// - For fungible assets, if they were issued by the same faucet.
123    /// - For non-fungible assets, if the assets are identical.
124    pub fn is_same(&self, other: &Self) -> bool {
125        use Asset::*;
126        match (self, other) {
127            (Fungible(l), Fungible(r)) => l.is_from_same_faucet(r),
128            (NonFungible(l), NonFungible(r)) => l == r,
129            _ => false,
130        }
131    }
132
133    /// Returns true if this asset is a fungible asset.
134    pub fn is_fungible(&self) -> bool {
135        matches!(self, Self::Fungible(_))
136    }
137
138    /// Returns true if this asset is a non fungible asset.
139    pub fn is_non_fungible(&self) -> bool {
140        matches!(self, Self::NonFungible(_))
141    }
142
143    /// Returns the ID of the faucet that issued this asset.
144    pub fn faucet_id(&self) -> AccountId {
145        match self {
146            Self::Fungible(asset) => asset.faucet_id(),
147            Self::NonFungible(asset) => asset.faucet_id(),
148        }
149    }
150
151    /// Returns the key which is used to store this asset in the account vault.
152    pub fn vault_key(&self) -> AssetVaultKey {
153        match self {
154            Self::Fungible(asset) => asset.vault_key(),
155            Self::NonFungible(asset) => asset.vault_key(),
156        }
157    }
158
159    /// Returns the asset's key encoded to a [`Word`].
160    pub fn to_key_word(&self) -> Word {
161        self.vault_key().to_word()
162    }
163
164    /// Returns the asset's value encoded to a [`Word`].
165    pub fn to_value_word(&self) -> Word {
166        match self {
167            Asset::Fungible(fungible_asset) => fungible_asset.to_value_word(),
168            Asset::NonFungible(non_fungible_asset) => non_fungible_asset.to_value_word(),
169        }
170    }
171
172    /// Returns the asset encoded as elements.
173    ///
174    /// The first four elements contain the asset key and the last four elements contain the asset
175    /// value.
176    pub fn as_elements(&self) -> [Felt; 8] {
177        let mut elements = [Felt::ZERO; 8];
178        elements[0..4].copy_from_slice(self.to_key_word().as_elements());
179        elements[4..8].copy_from_slice(self.to_value_word().as_elements());
180        elements
181    }
182
183    /// Returns the inner [`FungibleAsset`].
184    ///
185    /// # Panics
186    ///
187    /// Panics if the asset is non-fungible.
188    pub fn unwrap_fungible(&self) -> FungibleAsset {
189        match self {
190            Asset::Fungible(asset) => *asset,
191            Asset::NonFungible(_) => panic!("the asset is non-fungible"),
192        }
193    }
194
195    /// Returns the inner [`NonFungibleAsset`].
196    ///
197    /// # Panics
198    ///
199    /// Panics if the asset is fungible.
200    pub fn unwrap_non_fungible(&self) -> NonFungibleAsset {
201        match self {
202            Asset::Fungible(_) => panic!("the asset is fungible"),
203            Asset::NonFungible(asset) => *asset,
204        }
205    }
206}
207
208// SERIALIZATION
209// ================================================================================================
210
211impl Serializable for Asset {
212    fn write_into<W: ByteWriter>(&self, target: &mut W) {
213        match self {
214            Asset::Fungible(fungible_asset) => fungible_asset.write_into(target),
215            Asset::NonFungible(non_fungible_asset) => non_fungible_asset.write_into(target),
216        }
217    }
218
219    fn get_size_hint(&self) -> usize {
220        match self {
221            Asset::Fungible(fungible_asset) => fungible_asset.get_size_hint(),
222            Asset::NonFungible(non_fungible_asset) => non_fungible_asset.get_size_hint(),
223        }
224    }
225}
226
227impl Deserializable for Asset {
228    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
229        // Both asset types have their faucet ID as the first element, so we can use it to inspect
230        // what type of asset it is.
231        let faucet_id: AccountId = source.read()?;
232
233        match faucet_id.account_type() {
234            AccountType::FungibleFaucet => {
235                FungibleAsset::deserialize_with_faucet_id(faucet_id, source).map(Asset::from)
236            },
237            AccountType::NonFungibleFaucet => {
238                NonFungibleAsset::deserialize_with_faucet_id(faucet_id, source).map(Asset::from)
239            },
240            other_type => Err(DeserializationError::InvalidValue(format!(
241                "failed to deserialize asset: expected an account ID prefix of type faucet, found {other_type}"
242            ))),
243        }
244    }
245}
246
247// TESTS
248// ================================================================================================
249
250#[cfg(test)]
251mod tests {
252
253    use miden_crypto::utils::{Deserializable, Serializable};
254
255    use super::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails};
256    use crate::account::AccountId;
257    use crate::testing::account_id::{
258        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
259        ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
260        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
261        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
262        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
263        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
264        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
265        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
266    };
267
268    /// Tests the serialization roundtrip for assets for assets <-> bytes and assets <-> words.
269    #[test]
270    fn test_asset_serde() -> anyhow::Result<()> {
271        for fungible_account_id in [
272            ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
273            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
274            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
275            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
276            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
277        ] {
278            let account_id = AccountId::try_from(fungible_account_id).unwrap();
279            let fungible_asset: Asset = FungibleAsset::new(account_id, 10).unwrap().into();
280            assert_eq!(fungible_asset, Asset::read_from_bytes(&fungible_asset.to_bytes()).unwrap());
281            assert_eq!(
282                fungible_asset,
283                Asset::from_key_value_words(
284                    fungible_asset.to_key_word(),
285                    fungible_asset.to_value_word()
286                )?,
287            );
288        }
289
290        for non_fungible_account_id in [
291            ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
292            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
293            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
294        ] {
295            let account_id = AccountId::try_from(non_fungible_account_id).unwrap();
296            let details = NonFungibleAssetDetails::new(account_id, 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            assert_eq!(
303                non_fungible_asset,
304                Asset::from_key_value_words(
305                    non_fungible_asset.to_key_word(),
306                    non_fungible_asset.to_value_word()
307                )?
308            );
309        }
310
311        Ok(())
312    }
313
314    /// This test asserts that account ID's is serialized in the first felt of assets.
315    /// Asset deserialization relies on that fact and if this changes the serialization must
316    /// be updated.
317    #[test]
318    fn test_account_id_is_serialized_first() {
319        for asset in [FungibleAsset::mock(300), NonFungibleAsset::mock(&[0xaa, 0xbb])] {
320            let serialized_asset = asset.to_bytes();
321            let prefix = AccountId::read_from_bytes(&serialized_asset).unwrap();
322            assert_eq!(prefix, asset.faucet_id());
323        }
324    }
325}