Skip to main content

miden_protocol/asset/
mod.rs

1use alloc::string::ToString;
2use core::fmt;
3
4use super::errors::{AssetError, TokenSymbolError};
5use super::utils::serde::{
6    ByteReader,
7    ByteWriter,
8    Deserializable,
9    DeserializationError,
10    Serializable,
11};
12use super::{Felt, Word};
13use crate::account::AccountId;
14
15mod asset_amount;
16pub use asset_amount::AssetAmount;
17
18mod asset_value;
19pub use asset_value::AssetValue;
20
21mod fungible;
22
23pub use fungible::FungibleAsset;
24
25mod nonfungible;
26
27pub use nonfungible::{NonFungibleAsset, NonFungibleAssetDetails};
28
29mod token_symbol;
30pub use token_symbol::TokenSymbol;
31
32mod asset_callbacks;
33pub use asset_callbacks::AssetCallbacks;
34
35mod asset_composition;
36pub use asset_composition::AssetComposition;
37
38mod vault;
39pub use vault::{AssetClass, AssetId, AssetIdHash, AssetVault, AssetWitness, PartialVault};
40
41// ASSET
42// ================================================================================================
43
44/// Assets are encoded as an [`AssetId`] and an [`AssetValue`], each encodable as one word.
45///
46/// The [`AssetId`] uniquely identifies the asset and contains the [`AccountId`] of the issuer and
47/// the [`AssetClass`], which can further divide a single account's assets into different classes.
48/// It also contains the [`AssetComposition`],  which describes how assets compose, meaning whether
49/// they can be merged or split. For example, cominbing two fungible assets with the same ID and
50/// amounts 3 and 4 into a single one with amount 7 is called "merging". Splitting would be the
51/// reverse operation.
52///
53/// It is impossible to find a collision between two fungible assets issued by different faucets as
54/// the faucet ID is part of the asset's ID and the protocol's
55/// [`AccountTree`](crate::block::account_tree::AccountTree) guarantees that account IDs are
56/// globally unique.
57///
58/// Assets are generally opaque to the protocol, with the [`FungibleAsset]` being the exception.
59/// It is built-in in the sense that the tx kernel knows how to merge and split such assets without
60/// requiring a procedure call to the issuing account, which improves performance.
61///
62/// ## Fungible assets
63///
64/// All assets carrying [`AssetComposition::Fungible`] are interpreted as a fungible
65/// asset, and this composition allows merging and splitting of assets.
66///
67///
68/// - A fungible asset's value layout is: `[amount, 0, 0, 0]`.
69/// - A fungible asset's ID layout is: `[0, 0, faucet_id_suffix_and_metadata, faucet_id_prefix]`.
70///
71/// Where:
72/// - `amount` is the [`AssetAmount`] that the asset holds and cannot be greater than
73///   [`AssetAmount::MAX`] and thus fits into a felt.
74/// - the remaining elements in the value word must be zero.
75/// - `faucet_id_prefix` is the prefix of the faucet ID which issues the asset.
76/// - `faucet_id_suffix_and_metadata` is the suffix of the faucet ID which issues the asset and the
77///   asset metadata, which is the encoding version together with the [`AssetComposition`]. See
78///   [`AssetId`] for more details on the ID's layout.
79/// - the asset class limbs must be zero, which means two instances of the same fungible asset have
80///   the same asset ID and will be merged together when stored in the same account's vault.
81#[derive(Debug, Copy, Clone, PartialEq, Eq)]
82pub struct Asset {
83    id: AssetId,
84    value: AssetValue,
85}
86
87impl Asset {
88    /// Creates an asset from the provided ID and value.
89    ///
90    /// The value of a fungible asset is validated, see [`FungibleAsset::from_id_and_value`]. The
91    /// value of any other asset is opaque to the protocol and therefore not validated.
92    ///
93    /// # Errors
94    ///
95    /// Returns an error if:
96    /// - The asset is fungible and [`FungibleAsset::from_id_and_value`] fails.
97    pub fn new(id: AssetId, value: Word) -> Result<Self, AssetError> {
98        // An AssetId cannot be constructed with a Custom composition, so only the fungible case
99        // needs to be validated here.
100        if id.composition().is_fungible() {
101            FungibleAsset::from_id_and_value(id, value)?;
102        }
103
104        // TODO: Propagate the AssetValue type through the Asset API and beyond.
105        Ok(Self { id, value: AssetValue::from_raw(value) })
106    }
107
108    /// Creates an asset from the provided ID and value.
109    ///
110    /// Prefer [`Self::new`] for more type safety.
111    ///
112    /// # Errors
113    ///
114    /// Returns an error if:
115    /// - The provided ID does not contain a valid faucet ID.
116    /// - [`Self::new`] fails.
117    pub fn from_id_and_value_words(id: Word, value: Word) -> Result<Self, AssetError> {
118        let asset_id = AssetId::try_from(id)?;
119        Self::new(asset_id, value)
120    }
121
122    /// Returns true if this asset is the same as the specified asset.
123    ///
124    /// Two assets are defined to be the same if their asset IDs match.
125    pub fn is_same(&self, other: &Self) -> bool {
126        self.id() == other.id()
127    }
128
129    /// Returns true if this asset has [`AssetComposition::Fungible`], `false` otherwise.
130    pub fn is_fungible(&self) -> bool {
131        self.id.composition().is_fungible()
132    }
133
134    /// Returns true if this asset has [`AssetComposition::None`], `false` otherwise.
135    pub fn is_non_fungible(&self) -> bool {
136        self.id.composition().is_none()
137    }
138
139    /// Returns the ID of the faucet that issued this asset.
140    pub fn faucet_id(&self) -> AccountId {
141        self.id.faucet_id()
142    }
143
144    /// Returns the [`AssetId`] which uniquely identifies this asset in the account vault.
145    pub fn id(&self) -> AssetId {
146        self.id
147    }
148
149    /// Returns the [`AssetValue`] of this asset.
150    pub fn value(&self) -> AssetValue {
151        self.value
152    }
153
154    /// Returns the asset's [`AssetId`] encoded to a [`Word`].
155    pub fn to_id_word(&self) -> Word {
156        self.id().to_word()
157    }
158
159    /// Returns the asset's value encoded to a [`Word`].
160    pub fn to_value_word(&self) -> Word {
161        self.value.as_word()
162    }
163
164    /// Returns the asset encoded as elements.
165    ///
166    /// The first four elements contain the asset ID and the last four elements contain the asset
167    /// value.
168    pub fn as_elements(&self) -> [Felt; 8] {
169        let mut elements = [Felt::ZERO; 8];
170        elements[0..4].copy_from_slice(self.to_id_word().as_elements());
171        elements[4..8].copy_from_slice(self.to_value_word().as_elements());
172        elements
173    }
174
175    /// Returns this asset as a [`FungibleAsset`], or `None` if the asset is not a valid fungible
176    /// asset.
177    pub fn as_fungible(&self) -> Option<FungibleAsset> {
178        FungibleAsset::from_id_and_value(self.id, self.to_value_word()).ok()
179    }
180
181    /// Returns this asset as a [`FungibleAsset`].
182    ///
183    /// # Panics
184    ///
185    /// Panics if the asset is not fungible.
186    pub fn unwrap_fungible(&self) -> FungibleAsset {
187        self.as_fungible().expect("the asset should be fungible")
188    }
189}
190
191impl fmt::Display for Asset {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        write!(f, "Asset(id: {}, value: {})", self.id, self.value)
194    }
195}
196
197// SERIALIZATION
198// ================================================================================================
199
200impl Serializable for Asset {
201    fn write_into<W: ByteWriter>(&self, target: &mut W) {
202        target.write(self.id);
203        target.write(self.value);
204    }
205
206    fn get_size_hint(&self) -> usize {
207        self.id.get_size_hint() + self.value.get_size_hint()
208    }
209}
210
211impl Deserializable for Asset {
212    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
213        let id: AssetId = source.read()?;
214        let value: AssetValue = source.read()?;
215
216        Asset::new(id, value.as_word())
217            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
218    }
219}
220
221// TESTS
222// ================================================================================================
223
224#[cfg(test)]
225mod tests {
226
227    use assert_matches::assert_matches;
228    use miden_core::Word;
229    use miden_crypto::utils::{Deserializable, Serializable};
230
231    use super::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails};
232    use crate::Felt;
233    use crate::account::AccountId;
234    use crate::asset::{AssetClass, AssetComposition, AssetId};
235    use crate::errors::AssetError;
236    use crate::testing::account_id::{
237        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
238        ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
239        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
240        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
241        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
242        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
243        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
244        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
245    };
246
247    /// Returns the metadata byte encoded in an asset ID word.
248    pub(super) fn asset_metadata(id: AssetId) -> u8 {
249        (id.to_word()[2].as_canonical_u64() & AssetId::METADATA_BYTE_MASK as u64) as u8
250    }
251
252    /// Overwrites the metadata byte of the third element of an asset ID word.
253    pub(super) fn set_asset_metadata(id: AssetId, byte: u8) -> Word {
254        let mut id_word = id.to_word();
255        let raw = id_word[2].as_canonical_u64();
256        let new_raw = (raw & !(AssetId::METADATA_BYTE_MASK as u64)) | byte as u64;
257        id_word[2] =
258            Felt::try_from(new_raw).expect("clearing lower bits should produce a valid felt");
259        id_word
260    }
261
262    /// Tests the serialization roundtrip for assets for assets <-> bytes and assets <-> words.
263    #[test]
264    fn test_asset_serde() -> anyhow::Result<()> {
265        for fungible_account_id in [
266            ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
267            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
268            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
269            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
270            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
271        ] {
272            let account_id = AccountId::try_from(fungible_account_id).unwrap();
273            let fungible_asset: Asset = FungibleAsset::new(account_id, 10).unwrap().into();
274            assert_eq!(fungible_asset, Asset::read_from_bytes(&fungible_asset.to_bytes()).unwrap());
275            assert_eq!(
276                fungible_asset,
277                Asset::from_id_and_value_words(
278                    fungible_asset.to_id_word(),
279                    fungible_asset.to_value_word()
280                )?,
281            );
282        }
283
284        for non_fungible_account_id in [
285            ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
286            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
287            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
288        ] {
289            let account_id = AccountId::try_from(non_fungible_account_id).unwrap();
290            let details = NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]);
291            let non_fungible_asset: Asset = NonFungibleAsset::new(&details).into();
292            assert_eq!(
293                non_fungible_asset,
294                Asset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap()
295            );
296            assert_eq!(
297                non_fungible_asset,
298                Asset::from_id_and_value_words(
299                    non_fungible_asset.to_id_word(),
300                    non_fungible_asset.to_value_word()
301                )?
302            );
303        }
304
305        Ok(())
306    }
307
308    /// `Asset::from_id_and_value` must reject a [`AssetComposition::Custom`] asset ID with
309    /// `UnsupportedAssetComposition`.
310    #[test]
311    fn test_from_id_and_value_rejects_custom_composition() -> anyhow::Result<()> {
312        let err = AssetId::new(
313            AssetClass::default(),
314            ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?,
315            AssetComposition::Custom,
316        )
317        .unwrap_err();
318
319        assert_matches!(err, AssetError::UnsupportedAssetComposition(AssetComposition::Custom));
320
321        Ok(())
322    }
323
324    /// Roundtrip an asset with composition `None` through the `Asset` type.
325    #[test]
326    fn test_opaque_asset_roundtrip() -> anyhow::Result<()> {
327        let asset_id = AssetId::new(
328            AssetClass::new(Felt::from(1u32), Felt::from(2u32)),
329            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET.try_into()?,
330            AssetComposition::None,
331        )?;
332        let value = Word::from([7, 8, 9, 10u32]);
333        let asset = Asset::new(asset_id, value)?;
334
335        assert_eq!(asset.id(), asset_id);
336        assert_eq!(asset.to_value_word(), value);
337        assert_eq!(asset, Asset::read_from_bytes(&asset.to_bytes()).unwrap());
338        assert_eq!(asset.to_bytes().len(), asset.get_size_hint());
339        assert_eq!(asset, Asset::from_id_and_value_words(asset.to_id_word(), value)?);
340
341        Ok(())
342    }
343}