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