miden_objects/asset/
nonfungible.rs

1use alloc::boxed::Box;
2use alloc::string::ToString;
3use alloc::vec::Vec;
4use core::fmt;
5
6use super::vault::AssetVaultKey;
7use super::{AccountIdPrefix, AccountType, Asset, AssetError, Felt, Hasher, Word};
8use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable};
9use crate::{FieldElement, WORD_SIZE};
10
11/// Position of the faucet_id inside the [`NonFungibleAsset`] word having fields in BigEndian.
12const FAUCET_ID_POS_BE: usize = 3;
13
14// NON-FUNGIBLE ASSET
15// ================================================================================================
16
17/// A commitment to a non-fungible asset.
18///
19/// The commitment is constructed as follows:
20///
21/// - Hash the asset data producing `[hash0, hash1, hash2, hash3]`.
22/// - Replace the value of `hash3` with the prefix of the faucet id (`faucet_id_prefix`) producing
23///   `[hash0, hash1, hash2, faucet_id_prefix]`.
24/// - This layout ensures that fungible and non-fungible assets are distinguishable by interpreting
25///   the 3rd element of an asset as an [`AccountIdPrefix`] and checking its type.
26///
27/// [`NonFungibleAsset`] itself does not contain the actual asset data. The container for this data
28/// is [`NonFungibleAssetDetails`].
29#[derive(Debug, Copy, Clone, PartialEq, Eq)]
30pub struct NonFungibleAsset(Word);
31
32impl PartialOrd for NonFungibleAsset {
33    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
34        Some(self.cmp(other))
35    }
36}
37
38impl Ord for NonFungibleAsset {
39    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
40        self.0.cmp(&other.0)
41    }
42}
43
44impl NonFungibleAsset {
45    // CONSTANTS
46    // --------------------------------------------------------------------------------------------
47
48    /// The serialized size of a [`NonFungibleAsset`] in bytes.
49    ///
50    /// Currently represented as a word.
51    pub const SERIALIZED_SIZE: usize = Felt::ELEMENT_BYTES * WORD_SIZE;
52
53    // CONSTRUCTORS
54    // --------------------------------------------------------------------------------------------
55
56    /// Returns a non-fungible asset created from the specified asset details.
57    ///
58    /// # Errors
59    /// Returns an error if the provided faucet ID is not for a non-fungible asset faucet.
60    pub fn new(details: &NonFungibleAssetDetails) -> Result<Self, AssetError> {
61        let data_hash = Hasher::hash(details.asset_data());
62        Self::from_parts(details.faucet_id(), data_hash)
63    }
64
65    /// Return a non-fungible asset created from the specified faucet and using the provided
66    /// hash of the asset's data.
67    ///
68    /// Hash of the asset's data is expected to be computed from the binary representation of the
69    /// asset's data.
70    ///
71    /// # Errors
72    /// Returns an error if the provided faucet ID is not for a non-fungible asset faucet.
73    pub fn from_parts(faucet_id: AccountIdPrefix, mut data_hash: Word) -> Result<Self, AssetError> {
74        if !matches!(faucet_id.account_type(), AccountType::NonFungibleFaucet) {
75            return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id));
76        }
77
78        data_hash[FAUCET_ID_POS_BE] = Felt::from(faucet_id);
79
80        Ok(Self(data_hash))
81    }
82
83    /// Creates a new [NonFungibleAsset] without checking its validity.
84    ///
85    /// # Safety
86    /// This function required that the provided value is a valid word representation of a
87    /// [NonFungibleAsset].
88    pub unsafe fn new_unchecked(value: Word) -> NonFungibleAsset {
89        NonFungibleAsset(value)
90    }
91
92    // ACCESSORS
93    // --------------------------------------------------------------------------------------------
94
95    /// Returns the vault key of the [`NonFungibleAsset`].
96    ///
97    /// This is the same as the asset with the following modifications, in this order:
98    /// - Swaps the faucet ID at index 0 and `hash0` at index 3.
99    /// - Sets the fungible bit for `hash0` to `0`.
100    ///
101    /// # Rationale
102    ///
103    /// This means `hash0` will be used as the leaf index in the asset SMT which ensures that a
104    /// non-fungible faucet's assets generally end up in different leaves as the key is not based on
105    /// the faucet ID.
106    ///
107    /// It also ensures that there is never any collision in the leaf index between a non-fungible
108    /// asset and a fungible asset, as the former's vault key always has the fungible bit set to `0`
109    /// and the latter's vault key always has the bit set to `1`.
110    pub fn vault_key(&self) -> AssetVaultKey {
111        let mut vault_key = self.0;
112
113        // Swap prefix of faucet ID with hash0.
114        vault_key.swap(0, FAUCET_ID_POS_BE);
115
116        // Set the fungible bit to zero.
117        vault_key[3] =
118            AccountIdPrefix::clear_fungible_bit(self.faucet_id_prefix().version(), vault_key[3]);
119
120        AssetVaultKey::new_unchecked(vault_key)
121    }
122
123    /// Return ID prefix of the faucet which issued this asset.
124    pub fn faucet_id_prefix(&self) -> AccountIdPrefix {
125        AccountIdPrefix::new_unchecked(self.0[FAUCET_ID_POS_BE])
126    }
127
128    // HELPER FUNCTIONS
129    // --------------------------------------------------------------------------------------------
130
131    /// Validates this non-fungible asset.
132    /// # Errors
133    /// Returns an error if:
134    /// - The faucet_id is not a valid non-fungible faucet ID.
135    /// - The most significant bit of the asset is not ZERO.
136    fn validate(&self) -> Result<(), AssetError> {
137        let faucet_id = AccountIdPrefix::try_from(self.0[FAUCET_ID_POS_BE])
138            .map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?;
139
140        let account_type = faucet_id.account_type();
141        if !matches!(account_type, AccountType::NonFungibleFaucet) {
142            return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id));
143        }
144
145        Ok(())
146    }
147}
148
149impl From<NonFungibleAsset> for Word {
150    fn from(asset: NonFungibleAsset) -> Self {
151        asset.0
152    }
153}
154
155impl From<NonFungibleAsset> for Asset {
156    fn from(asset: NonFungibleAsset) -> Self {
157        Asset::NonFungible(asset)
158    }
159}
160
161impl TryFrom<Word> for NonFungibleAsset {
162    type Error = AssetError;
163
164    fn try_from(value: Word) -> Result<Self, Self::Error> {
165        let asset = Self(value);
166        asset.validate()?;
167        Ok(asset)
168    }
169}
170
171impl fmt::Display for NonFungibleAsset {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        write!(f, "{self:?}")
174    }
175}
176
177// SERIALIZATION
178// ================================================================================================
179
180impl Serializable for NonFungibleAsset {
181    fn write_into<W: ByteWriter>(&self, target: &mut W) {
182        // All assets should serialize their faucet ID at the first position to allow them to be
183        // easily distinguishable during deserialization.
184        target.write(self.faucet_id_prefix());
185        target.write(self.0[2]);
186        target.write(self.0[1]);
187        target.write(self.0[0]);
188    }
189
190    fn get_size_hint(&self) -> usize {
191        Self::SERIALIZED_SIZE
192    }
193}
194
195impl Deserializable for NonFungibleAsset {
196    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
197        let faucet_id_prefix: AccountIdPrefix = source.read()?;
198
199        Self::deserialize_with_faucet_id_prefix(faucet_id_prefix, source)
200            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
201    }
202}
203
204impl NonFungibleAsset {
205    /// Deserializes a [`NonFungibleAsset`] from an [`AccountIdPrefix`] and the remaining data from
206    /// the given `source`.
207    pub(super) fn deserialize_with_faucet_id_prefix<R: ByteReader>(
208        faucet_id_prefix: AccountIdPrefix,
209        source: &mut R,
210    ) -> Result<Self, DeserializationError> {
211        let hash_2: Felt = source.read()?;
212        let hash_1: Felt = source.read()?;
213        let hash_0: Felt = source.read()?;
214
215        // The last felt in the data_hash will be replaced by the faucet id, so we can set it to
216        // zero here.
217        NonFungibleAsset::from_parts(
218            faucet_id_prefix,
219            Word::from([hash_0, hash_1, hash_2, Felt::ZERO]),
220        )
221        .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
222    }
223}
224
225// NON-FUNGIBLE ASSET DETAILS
226// ================================================================================================
227
228/// Details about a non-fungible asset.
229///
230/// Unlike [NonFungibleAsset] struct, this struct contains full details of a non-fungible asset.
231#[derive(Debug, Clone, PartialEq, Eq)]
232pub struct NonFungibleAssetDetails {
233    faucet_id: AccountIdPrefix,
234    asset_data: Vec<u8>,
235}
236
237impl NonFungibleAssetDetails {
238    /// Returns asset details instantiated from the specified faucet ID and asset data.
239    ///
240    /// # Errors
241    /// Returns an error if the provided faucet ID is not for a non-fungible asset faucet.
242    pub fn new(faucet_id: AccountIdPrefix, asset_data: Vec<u8>) -> Result<Self, AssetError> {
243        if !matches!(faucet_id.account_type(), AccountType::NonFungibleFaucet) {
244            return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id));
245        }
246
247        Ok(Self { faucet_id, asset_data })
248    }
249
250    /// Returns ID of the faucet which issued this asset.
251    pub fn faucet_id(&self) -> AccountIdPrefix {
252        self.faucet_id
253    }
254
255    /// Returns asset data in binary format.
256    pub fn asset_data(&self) -> &[u8] {
257        &self.asset_data
258    }
259}
260
261// TESTS
262// ================================================================================================
263
264#[cfg(test)]
265mod tests {
266    use assert_matches::assert_matches;
267
268    use super::*;
269    use crate::account::AccountId;
270    use crate::testing::account_id::{
271        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
272        ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
273        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
274        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
275    };
276
277    #[test]
278    fn test_non_fungible_asset_serde() {
279        for non_fungible_account_id in [
280            ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
281            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
282            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
283        ] {
284            let account_id = AccountId::try_from(non_fungible_account_id).unwrap();
285            let details = NonFungibleAssetDetails::new(account_id.prefix(), vec![1, 2, 3]).unwrap();
286            let non_fungible_asset = NonFungibleAsset::new(&details).unwrap();
287            assert_eq!(
288                non_fungible_asset,
289                NonFungibleAsset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap()
290            );
291        }
292
293        let account = AccountId::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET).unwrap();
294        let details = NonFungibleAssetDetails::new(account.prefix(), vec![4, 5, 6, 7]).unwrap();
295        let asset = NonFungibleAsset::new(&details).unwrap();
296        let mut asset_bytes = asset.to_bytes();
297
298        let fungible_faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
299
300        // Set invalid Faucet ID Prefix.
301        asset_bytes[0..8].copy_from_slice(&fungible_faucet_id.prefix().to_bytes());
302
303        let err = NonFungibleAsset::read_from_bytes(&asset_bytes).unwrap_err();
304        assert_matches!(err, DeserializationError::InvalidValue(msg) if msg.contains("must be of type NonFungibleFaucet"));
305    }
306}