miden_objects/asset/
nonfungible.rs

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