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