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