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}