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