Skip to main content

miden_protocol/asset/
nonfungible.rs

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