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}