miden_objects/asset/
nonfungible.rs1use alloc::boxed::Box;
2use alloc::string::ToString;
3use alloc::vec::Vec;
4use core::fmt;
5
6use super::vault::AssetVaultKey;
7use super::{AccountIdPrefix, AccountType, Asset, AssetError, Felt, Hasher, Word};
8use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable};
9use crate::{FieldElement, WORD_SIZE};
10
11const FAUCET_ID_POS_BE: usize = 3;
13
14#[derive(Debug, Copy, Clone, PartialEq, Eq)]
30pub struct NonFungibleAsset(Word);
31
32impl PartialOrd for NonFungibleAsset {
33 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
34 Some(self.cmp(other))
35 }
36}
37
38impl Ord for NonFungibleAsset {
39 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
40 self.0.cmp(&other.0)
41 }
42}
43
44impl NonFungibleAsset {
45 pub const SERIALIZED_SIZE: usize = Felt::ELEMENT_BYTES * WORD_SIZE;
52
53 pub fn new(details: &NonFungibleAssetDetails) -> Result<Self, AssetError> {
61 let data_hash = Hasher::hash(details.asset_data());
62 Self::from_parts(details.faucet_id(), data_hash)
63 }
64
65 pub fn from_parts(faucet_id: AccountIdPrefix, mut data_hash: Word) -> Result<Self, AssetError> {
74 if !matches!(faucet_id.account_type(), AccountType::NonFungibleFaucet) {
75 return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id));
76 }
77
78 data_hash[FAUCET_ID_POS_BE] = Felt::from(faucet_id);
79
80 Ok(Self(data_hash))
81 }
82
83 pub unsafe fn new_unchecked(value: Word) -> NonFungibleAsset {
89 NonFungibleAsset(value)
90 }
91
92 pub fn vault_key(&self) -> AssetVaultKey {
111 let mut vault_key = self.0;
112
113 vault_key.swap(0, FAUCET_ID_POS_BE);
115
116 vault_key[3] =
118 AccountIdPrefix::clear_fungible_bit(self.faucet_id_prefix().version(), vault_key[3]);
119
120 AssetVaultKey::new_unchecked(vault_key)
121 }
122
123 pub fn faucet_id_prefix(&self) -> AccountIdPrefix {
125 AccountIdPrefix::new_unchecked(self.0[FAUCET_ID_POS_BE])
126 }
127
128 fn validate(&self) -> Result<(), AssetError> {
137 let faucet_id = AccountIdPrefix::try_from(self.0[FAUCET_ID_POS_BE])
138 .map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?;
139
140 let account_type = faucet_id.account_type();
141 if !matches!(account_type, AccountType::NonFungibleFaucet) {
142 return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id));
143 }
144
145 Ok(())
146 }
147}
148
149impl From<NonFungibleAsset> for Word {
150 fn from(asset: NonFungibleAsset) -> Self {
151 asset.0
152 }
153}
154
155impl From<NonFungibleAsset> for Asset {
156 fn from(asset: NonFungibleAsset) -> Self {
157 Asset::NonFungible(asset)
158 }
159}
160
161impl TryFrom<Word> for NonFungibleAsset {
162 type Error = AssetError;
163
164 fn try_from(value: Word) -> Result<Self, Self::Error> {
165 let asset = Self(value);
166 asset.validate()?;
167 Ok(asset)
168 }
169}
170
171impl fmt::Display for NonFungibleAsset {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 write!(f, "{self:?}")
174 }
175}
176
177impl Serializable for NonFungibleAsset {
181 fn write_into<W: ByteWriter>(&self, target: &mut W) {
182 target.write(self.faucet_id_prefix());
185 target.write(self.0[2]);
186 target.write(self.0[1]);
187 target.write(self.0[0]);
188 }
189
190 fn get_size_hint(&self) -> usize {
191 Self::SERIALIZED_SIZE
192 }
193}
194
195impl Deserializable for NonFungibleAsset {
196 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
197 let faucet_id_prefix: AccountIdPrefix = source.read()?;
198
199 Self::deserialize_with_faucet_id_prefix(faucet_id_prefix, source)
200 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
201 }
202}
203
204impl NonFungibleAsset {
205 pub(super) fn deserialize_with_faucet_id_prefix<R: ByteReader>(
208 faucet_id_prefix: AccountIdPrefix,
209 source: &mut R,
210 ) -> Result<Self, DeserializationError> {
211 let hash_2: Felt = source.read()?;
212 let hash_1: Felt = source.read()?;
213 let hash_0: Felt = source.read()?;
214
215 NonFungibleAsset::from_parts(
218 faucet_id_prefix,
219 Word::from([hash_0, hash_1, hash_2, Felt::ZERO]),
220 )
221 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
222 }
223}
224
225#[derive(Debug, Clone, PartialEq, Eq)]
232pub struct NonFungibleAssetDetails {
233 faucet_id: AccountIdPrefix,
234 asset_data: Vec<u8>,
235}
236
237impl NonFungibleAssetDetails {
238 pub fn new(faucet_id: AccountIdPrefix, asset_data: Vec<u8>) -> Result<Self, AssetError> {
243 if !matches!(faucet_id.account_type(), AccountType::NonFungibleFaucet) {
244 return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id));
245 }
246
247 Ok(Self { faucet_id, asset_data })
248 }
249
250 pub fn faucet_id(&self) -> AccountIdPrefix {
252 self.faucet_id
253 }
254
255 pub fn asset_data(&self) -> &[u8] {
257 &self.asset_data
258 }
259}
260
261#[cfg(test)]
265mod tests {
266 use assert_matches::assert_matches;
267
268 use super::*;
269 use crate::account::AccountId;
270 use crate::testing::account_id::{
271 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
272 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
273 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
274 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
275 };
276
277 #[test]
278 fn test_non_fungible_asset_serde() {
279 for non_fungible_account_id in [
280 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
281 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
282 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
283 ] {
284 let account_id = AccountId::try_from(non_fungible_account_id).unwrap();
285 let details = NonFungibleAssetDetails::new(account_id.prefix(), vec![1, 2, 3]).unwrap();
286 let non_fungible_asset = NonFungibleAsset::new(&details).unwrap();
287 assert_eq!(
288 non_fungible_asset,
289 NonFungibleAsset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap()
290 );
291 }
292
293 let account = AccountId::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET).unwrap();
294 let details = NonFungibleAssetDetails::new(account.prefix(), vec![4, 5, 6, 7]).unwrap();
295 let asset = NonFungibleAsset::new(&details).unwrap();
296 let mut asset_bytes = asset.to_bytes();
297
298 let fungible_faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
299
300 asset_bytes[0..8].copy_from_slice(&fungible_faucet_id.prefix().to_bytes());
302
303 let err = NonFungibleAsset::read_from_bytes(&asset_bytes).unwrap_err();
304 assert_matches!(err, DeserializationError::InvalidValue(msg) if msg.contains("must be of type NonFungibleFaucet"));
305 }
306}