Skip to main content

miden_protocol/asset/
nonfungible.rs

1use alloc::string::ToString;
2use alloc::vec::Vec;
3use core::fmt;
4
5use super::vault::AssetVaultKey;
6use super::{AccountType, Asset, AssetCallbackFlag, AssetError, Word};
7use crate::Hasher;
8use crate::account::AccountId;
9use crate::asset::vault::AssetId;
10use crate::utils::serde::{
11    ByteReader,
12    ByteWriter,
13    Deserializable,
14    DeserializationError,
15    Serializable,
16};
17
18// NON-FUNGIBLE ASSET
19// ================================================================================================
20
21/// A commitment to a non-fungible asset.
22///
23/// See [`Asset`] for details on how it is constructed.
24///
25/// [`NonFungibleAsset`] itself does not contain the actual asset data. The container for this data
26/// is [`NonFungibleAssetDetails`].
27///
28/// The non-fungible asset can have callbacks to the faucet enabled or disabled, depending on
29/// [`AssetCallbackFlag`]. See [`AssetCallbacks`](crate::asset::AssetCallbacks) for more details.
30#[derive(Debug, Copy, Clone, PartialEq, Eq)]
31pub struct NonFungibleAsset {
32    faucet_id: AccountId,
33    value: Word,
34    callbacks: AssetCallbackFlag,
35}
36
37impl NonFungibleAsset {
38    // CONSTANTS
39    // --------------------------------------------------------------------------------------------
40
41    /// The serialized size of a [`NonFungibleAsset`] in bytes.
42    ///
43    /// An account ID (15 bytes) plus a word (32 bytes) plus a callbacks flag (1 byte).
44    pub const SERIALIZED_SIZE: usize =
45        AccountId::SERIALIZED_SIZE + Word::SERIALIZED_SIZE + AssetCallbackFlag::SERIALIZED_SIZE;
46
47    // CONSTRUCTORS
48    // --------------------------------------------------------------------------------------------
49
50    /// Returns a non-fungible asset created from the specified asset details.
51    ///
52    /// # Errors
53    /// Returns an error if the provided faucet ID is not for a non-fungible asset faucet.
54    pub fn new(details: &NonFungibleAssetDetails) -> Result<Self, AssetError> {
55        let data_hash = Hasher::hash(details.asset_data());
56        Self::from_parts(details.faucet_id(), data_hash)
57    }
58
59    /// Return a non-fungible asset created from the specified faucet and using the provided
60    /// hash of the asset's data.
61    ///
62    /// Hash of the asset's data is expected to be computed from the binary representation of the
63    /// asset's data.
64    ///
65    /// # Errors
66    /// Returns an error if the provided faucet ID is not for a non-fungible asset faucet.
67    pub fn from_parts(faucet_id: AccountId, value: Word) -> Result<Self, AssetError> {
68        if !matches!(faucet_id.account_type(), AccountType::NonFungibleFaucet) {
69            return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id));
70        }
71
72        Ok(Self {
73            faucet_id,
74            value,
75            callbacks: AssetCallbackFlag::default(),
76        })
77    }
78
79    /// Creates a non-fungible asset from the provided key and value.
80    ///
81    /// # Errors
82    ///
83    /// Returns an error if:
84    /// - The provided key does not contain a valid faucet ID.
85    /// - The provided key's asset ID limbs are not equal to the provided value's first and second
86    ///   element.
87    /// - The faucet ID is not a non-fungible faucet ID.
88    pub fn from_key_value(key: AssetVaultKey, value: Word) -> Result<Self, AssetError> {
89        if key.asset_id().suffix() != value[0] || key.asset_id().prefix() != value[1] {
90            return Err(AssetError::NonFungibleAssetIdMustMatchValue {
91                asset_id: key.asset_id(),
92                value,
93            });
94        }
95
96        let mut asset = Self::from_parts(key.faucet_id(), value)?;
97        asset.callbacks = key.callback_flag();
98
99        Ok(asset)
100    }
101
102    /// Creates a non-fungible asset from the provided key and value.
103    ///
104    /// Prefer [`Self::from_key_value`] for more type safety.
105    ///
106    /// # Errors
107    ///
108    /// Returns an error if:
109    /// - The provided key does not contain a valid faucet ID.
110    /// - [`Self::from_key_value`] fails.
111    pub fn from_key_value_words(key: Word, value: Word) -> Result<Self, AssetError> {
112        let vault_key = AssetVaultKey::try_from(key)?;
113        Self::from_key_value(vault_key, value)
114    }
115
116    /// Returns a copy of this asset with the given [`AssetCallbackFlag`].
117    pub fn with_callbacks(mut self, callbacks: AssetCallbackFlag) -> Self {
118        self.callbacks = callbacks;
119        self
120    }
121
122    // ACCESSORS
123    // --------------------------------------------------------------------------------------------
124
125    /// Returns the vault key of the [`NonFungibleAsset`].
126    ///
127    /// See [`Asset`] docs for details on the key.
128    pub fn vault_key(&self) -> AssetVaultKey {
129        let asset_id_suffix = self.value[0];
130        let asset_id_prefix = self.value[1];
131        let asset_id = AssetId::new(asset_id_suffix, asset_id_prefix);
132
133        AssetVaultKey::new(asset_id, self.faucet_id, self.callbacks)
134            .expect("constructors should ensure account ID is of type non-fungible faucet")
135    }
136
137    /// Returns the ID of the faucet which issued this asset.
138    pub fn faucet_id(&self) -> AccountId {
139        self.faucet_id
140    }
141
142    /// Returns the [`AssetCallbackFlag`] of this asset.
143    pub fn callbacks(&self) -> AssetCallbackFlag {
144        self.callbacks
145    }
146
147    /// Returns the asset's key encoded to a [`Word`].
148    pub fn to_key_word(&self) -> Word {
149        self.vault_key().to_word()
150    }
151
152    /// Returns the asset's value encoded to a [`Word`].
153    pub fn to_value_word(&self) -> Word {
154        self.value
155    }
156}
157
158impl fmt::Display for NonFungibleAsset {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        // TODO: Replace with hex representation?
161        write!(f, "{self:?}")
162    }
163}
164
165impl From<NonFungibleAsset> for Asset {
166    fn from(asset: NonFungibleAsset) -> Self {
167        Asset::NonFungible(asset)
168    }
169}
170
171// SERIALIZATION
172// ================================================================================================
173
174impl Serializable for NonFungibleAsset {
175    fn write_into<W: ByteWriter>(&self, target: &mut W) {
176        // All assets should serialize their faucet ID at the first position to allow them to be
177        // easily distinguishable during deserialization.
178        target.write(self.faucet_id());
179        target.write(self.value);
180        target.write(self.callbacks);
181    }
182
183    fn get_size_hint(&self) -> usize {
184        self.faucet_id.get_size_hint() + self.value.get_size_hint() + self.callbacks.get_size_hint()
185    }
186}
187
188impl Deserializable for NonFungibleAsset {
189    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
190        let faucet_id: AccountId = source.read()?;
191
192        Self::deserialize_with_faucet_id(faucet_id, source)
193            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
194    }
195}
196
197impl NonFungibleAsset {
198    /// Deserializes a [`NonFungibleAsset`] from an [`AccountId`] and the remaining data from the
199    /// given `source`.
200    pub(super) fn deserialize_with_faucet_id<R: ByteReader>(
201        faucet_id: AccountId,
202        source: &mut R,
203    ) -> Result<Self, DeserializationError> {
204        let value: Word = source.read()?;
205        let callbacks: AssetCallbackFlag = source.read()?;
206
207        NonFungibleAsset::from_parts(faucet_id, value)
208            .map(|asset| asset.with_callbacks(callbacks))
209            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
210    }
211}
212
213// NON-FUNGIBLE ASSET DETAILS
214// ================================================================================================
215
216/// Details about a non-fungible asset.
217///
218/// Unlike [NonFungibleAsset] struct, this struct contains full details of a non-fungible asset.
219#[derive(Debug, Clone, PartialEq, Eq)]
220pub struct NonFungibleAssetDetails {
221    faucet_id: AccountId,
222    asset_data: Vec<u8>,
223}
224
225impl NonFungibleAssetDetails {
226    /// Returns asset details instantiated from the specified faucet ID and asset data.
227    ///
228    /// # Errors
229    /// Returns an error if the provided faucet ID is not for a non-fungible asset faucet.
230    pub fn new(faucet_id: AccountId, asset_data: Vec<u8>) -> Result<Self, AssetError> {
231        if !matches!(faucet_id.account_type(), AccountType::NonFungibleFaucet) {
232            return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id));
233        }
234
235        Ok(Self { faucet_id, asset_data })
236    }
237
238    /// Returns ID of the faucet which issued this asset.
239    pub fn faucet_id(&self) -> AccountId {
240        self.faucet_id
241    }
242
243    /// Returns asset data in binary format.
244    pub fn asset_data(&self) -> &[u8] {
245        &self.asset_data
246    }
247}
248
249// TESTS
250// ================================================================================================
251
252#[cfg(test)]
253mod tests {
254    use assert_matches::assert_matches;
255
256    use super::*;
257    use crate::Felt;
258    use crate::account::AccountId;
259    use crate::testing::account_id::{
260        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
261        ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
262        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
263        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
264    };
265
266    #[test]
267    fn fungible_asset_from_key_value_fails_on_invalid_asset_id() -> anyhow::Result<()> {
268        let invalid_key = AssetVaultKey::new_native(
269            AssetId::new(Felt::from(1u32), Felt::from(2u32)),
270            ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET.try_into()?,
271        )?;
272        let err =
273            NonFungibleAsset::from_key_value(invalid_key, Word::from([4, 5, 6, 7u32])).unwrap_err();
274
275        assert_matches!(err, AssetError::NonFungibleAssetIdMustMatchValue { .. });
276
277        Ok(())
278    }
279
280    #[test]
281    fn test_non_fungible_asset_serde() -> anyhow::Result<()> {
282        for non_fungible_account_id in [
283            ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
284            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
285            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
286        ] {
287            let account_id = AccountId::try_from(non_fungible_account_id).unwrap();
288            let details = NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]).unwrap();
289            let non_fungible_asset = NonFungibleAsset::new(&details).unwrap();
290            assert_eq!(
291                non_fungible_asset,
292                NonFungibleAsset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap()
293            );
294            assert_eq!(non_fungible_asset.to_bytes().len(), non_fungible_asset.get_size_hint());
295
296            assert_eq!(
297                non_fungible_asset,
298                NonFungibleAsset::from_key_value_words(
299                    non_fungible_asset.to_key_word(),
300                    non_fungible_asset.to_value_word()
301                )?
302            )
303        }
304
305        let account = AccountId::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET).unwrap();
306        let details = NonFungibleAssetDetails::new(account, vec![4, 5, 6, 7]).unwrap();
307        let asset = NonFungibleAsset::new(&details).unwrap();
308        let mut asset_bytes = asset.to_bytes();
309
310        let fungible_faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
311
312        // Set invalid faucet ID.
313        asset_bytes[0..AccountId::SERIALIZED_SIZE].copy_from_slice(&fungible_faucet_id.to_bytes());
314
315        let err = NonFungibleAsset::read_from_bytes(&asset_bytes).unwrap_err();
316        assert_matches!(err, DeserializationError::InvalidValue(msg) if msg.contains("must be of type NonFungibleFaucet"));
317
318        Ok(())
319    }
320
321    #[test]
322    fn test_vault_key_for_non_fungible_asset() {
323        let asset = NonFungibleAsset::mock(&[42]);
324
325        assert_eq!(asset.vault_key().faucet_id(), NonFungibleAsset::mock_issuer());
326        assert_eq!(asset.vault_key().asset_id().suffix(), asset.to_value_word()[0]);
327        assert_eq!(asset.vault_key().asset_id().prefix(), asset.to_value_word()[1]);
328    }
329}