Skip to main content

miden_protocol/asset/
nonfungible.rs

1use alloc::vec::Vec;
2use core::fmt;
3
4use super::vault::AssetId;
5use super::{Asset, AssetComposition, AssetError, Word};
6use crate::Hasher;
7use crate::account::AccountId;
8use crate::asset::vault::AssetClass;
9use crate::utils::serde::{
10    ByteReader,
11    ByteWriter,
12    Deserializable,
13    DeserializationError,
14    Serializable,
15};
16
17// NON-FUNGIBLE ASSET
18// ================================================================================================
19
20/// A commitment to a non-fungible asset.
21///
22/// See [`Asset`] for details on how it is constructed.
23///
24/// [`NonFungibleAsset`] itself does not contain the actual asset data. The container for this data
25/// is [`NonFungibleAssetDetails`].
26///
27/// Whether the asset triggers callbacks to the faucet is an immutable property of the faucet's
28/// [`AccountId`], see [`AccountId::asset_callback_flag`].
29#[derive(Debug, Copy, Clone, PartialEq, Eq)]
30pub struct NonFungibleAsset {
31    faucet_id: AccountId,
32    value: Word,
33}
34
35impl NonFungibleAsset {
36    // CONSTANTS
37    // --------------------------------------------------------------------------------------------
38
39    /// The serialized size of a [`NonFungibleAsset`] in bytes.
40    ///
41    /// A composition byte (u8) plus an account ID (15 bytes) plus a word (32 bytes).
42    pub const SERIALIZED_SIZE: usize =
43        AssetComposition::SERIALIZED_SIZE + AccountId::SERIALIZED_SIZE + Word::SERIALIZED_SIZE;
44
45    // CONSTRUCTORS
46    // --------------------------------------------------------------------------------------------
47
48    /// Returns a non-fungible asset created from the specified asset details.
49    pub fn new(details: &NonFungibleAssetDetails) -> Self {
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    pub fn from_parts(faucet_id: AccountId, value: Word) -> Self {
60        Self { faucet_id, value }
61    }
62
63    /// Creates a non-fungible asset from the provided ID and value.
64    ///
65    /// # Errors
66    ///
67    /// Returns an error if:
68    /// - The provided ID does not have [`AssetComposition::None`] set.
69    /// - The provided ID's asset class limbs are not equal to the provided value's first and second
70    ///   element.
71    /// - The faucet ID is not a non-fungible faucet ID.
72    pub fn from_id_and_value(id: AssetId, value: Word) -> Result<Self, AssetError> {
73        if !id.composition().is_none() {
74            return Err(AssetError::AssetCompositionMismatch {
75                faucet_id: id.faucet_id(),
76                expected: AssetComposition::None,
77                actual: id.composition(),
78            });
79        }
80
81        if id.asset_class().suffix() != value[0] || id.asset_class().prefix() != value[1] {
82            return Err(AssetError::NonFungibleAssetClassMustMatchValue {
83                asset_class: id.asset_class(),
84                value,
85            });
86        }
87
88        Ok(Self::from_parts(id.faucet_id(), value))
89    }
90
91    /// Creates a non-fungible asset from the provided ID and value.
92    ///
93    /// Prefer [`Self::from_id_and_value`] for more type safety.
94    ///
95    /// # Errors
96    ///
97    /// Returns an error if:
98    /// - [`Self::from_id_and_value`] fails.
99    pub fn from_id_and_value_words(id: Word, value: Word) -> Result<Self, AssetError> {
100        let asset_id = AssetId::try_from(id)?;
101        Self::from_id_and_value(asset_id, value)
102    }
103
104    // ACCESSORS
105    // --------------------------------------------------------------------------------------------
106
107    /// Returns the [`AssetId`] which uniquely identifies this [`NonFungibleAsset`].
108    ///
109    /// See [`Asset`] docs for details on the asset ID.
110    pub fn id(&self) -> AssetId {
111        let asset_class_suffix = self.value[0];
112        let asset_class_prefix = self.value[1];
113        let asset_class = AssetClass::new(asset_class_suffix, asset_class_prefix);
114
115        AssetId::new(asset_class, self.faucet_id, AssetComposition::None)
116            .expect("non-fungible composition is always valid")
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 [`AssetId`] encoded to a [`Word`].
125    pub fn to_id_word(&self) -> Word {
126        self.id().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        // Lead with the asset composition byte to distinguish asset types on the wire.
154        target.write(AssetComposition::None);
155        target.write(self.faucet_id());
156        target.write(self.value);
157    }
158
159    fn get_size_hint(&self) -> usize {
160        AssetComposition::SERIALIZED_SIZE
161            + self.faucet_id.get_size_hint()
162            + self.value.get_size_hint()
163    }
164}
165
166impl Deserializable for NonFungibleAsset {
167    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
168        let composition: AssetComposition = source.read()?;
169        if !composition.is_none() {
170            return Err(DeserializationError::InvalidValue(format!(
171                "expected non-fungible asset composition but found {composition:?}"
172            )));
173        }
174        NonFungibleAsset::deserialize_body(source)
175    }
176}
177
178impl NonFungibleAsset {
179    /// Reads the remaining body of a non-fungible asset, after the leading composition byte has
180    /// already been consumed.
181    pub(super) fn deserialize_body<R: ByteReader>(
182        source: &mut R,
183    ) -> Result<Self, DeserializationError> {
184        let faucet_id: AccountId = source.read()?;
185        let value: Word = source.read()?;
186
187        Ok(NonFungibleAsset::from_parts(faucet_id, value))
188    }
189}
190
191// NON-FUNGIBLE ASSET DETAILS
192// ================================================================================================
193
194/// Details about a non-fungible asset.
195///
196/// Unlike [NonFungibleAsset] struct, this struct contains full details of a non-fungible asset.
197#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct NonFungibleAssetDetails {
199    faucet_id: AccountId,
200    asset_data: Vec<u8>,
201}
202
203impl NonFungibleAssetDetails {
204    /// Returns asset details instantiated from the specified faucet ID and asset data.
205    pub fn new(faucet_id: AccountId, asset_data: Vec<u8>) -> Self {
206        Self { faucet_id, asset_data }
207    }
208
209    /// Returns ID of the faucet which issued this asset.
210    pub fn faucet_id(&self) -> AccountId {
211        self.faucet_id
212    }
213
214    /// Returns asset data in binary format.
215    pub fn asset_data(&self) -> &[u8] {
216        &self.asset_data
217    }
218}
219
220// TESTS
221// ================================================================================================
222
223#[cfg(test)]
224mod tests {
225    use assert_matches::assert_matches;
226
227    use super::*;
228    use crate::Felt;
229    use crate::account::AccountId;
230    use crate::asset::FungibleAsset;
231    use crate::testing::account_id::{
232        ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
233        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
234        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
235    };
236
237    #[test]
238    fn non_fungible_asset_from_id_and_value_words_fails_on_invalid_composition()
239    -> anyhow::Result<()> {
240        // Use a fungible asset's ID-value words where the the composition is set to `Fungible`.
241        let asset = FungibleAsset::mock(20);
242
243        let err =
244            NonFungibleAsset::from_id_and_value_words(asset.to_id_word(), asset.to_value_word())
245                .unwrap_err();
246        assert_matches!(err, AssetError::AssetCompositionMismatch {
247                faucet_id: _, expected, actual,
248            } => {
249                assert_eq!(actual, AssetComposition::Fungible);
250                assert_eq!(expected, AssetComposition::None);
251        });
252
253        Ok(())
254    }
255
256    #[test]
257    fn non_fungible_asset_from_id_and_value_fails_on_invalid_asset_class() -> anyhow::Result<()> {
258        let invalid_id = AssetId::new(
259            AssetClass::new(Felt::from(1u32), Felt::from(2u32)),
260            ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET.try_into()?,
261            AssetComposition::None,
262        )?;
263        let err = NonFungibleAsset::from_id_and_value(invalid_id, Word::from([4, 5, 6, 7u32]))
264            .unwrap_err();
265
266        assert_matches!(err, AssetError::NonFungibleAssetClassMustMatchValue { .. });
267
268        Ok(())
269    }
270
271    #[test]
272    fn test_non_fungible_asset_serde() -> anyhow::Result<()> {
273        for non_fungible_account_id in [
274            ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
275            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
276            ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
277        ] {
278            let account_id = AccountId::try_from(non_fungible_account_id).unwrap();
279            let details = NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]);
280            let non_fungible_asset = NonFungibleAsset::new(&details);
281            assert_eq!(
282                non_fungible_asset,
283                NonFungibleAsset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap()
284            );
285            assert_eq!(non_fungible_asset.to_bytes().len(), non_fungible_asset.get_size_hint());
286
287            assert_eq!(
288                non_fungible_asset,
289                NonFungibleAsset::from_id_and_value_words(
290                    non_fungible_asset.to_id_word(),
291                    non_fungible_asset.to_value_word()
292                )?
293            )
294        }
295
296        let fungible_asset = FungibleAsset::mock(42);
297        let err = NonFungibleAsset::read_from_bytes(&fungible_asset.to_bytes()).unwrap_err();
298        assert_matches!(err, DeserializationError::InvalidValue(msg) => {
299            assert!(msg.contains("expected non-fungible asset composition but found Fungible"));
300        });
301
302        Ok(())
303    }
304
305    #[test]
306    fn test_asset_id_for_non_fungible_asset() {
307        let asset = NonFungibleAsset::mock(&[42]);
308
309        assert_eq!(asset.id().faucet_id(), NonFungibleAsset::mock_issuer());
310        assert_eq!(asset.id().asset_class().suffix(), asset.to_value_word()[0]);
311        assert_eq!(asset.id().asset_class().prefix(), asset.to_value_word()[1]);
312    }
313}