miden_protocol/asset/
nonfungible.rs1use alloc::string::ToString;
2use alloc::vec::Vec;
3use core::fmt;
4
5use super::vault::AssetVaultKey;
6use super::{AccountType, Asset, AssetCallbackFlag, AssetError, Word};
7use crate::Hasher;
8use crate::account::AccountId;
9use crate::asset::vault::AssetId;
10use crate::utils::serde::{
11 ByteReader,
12 ByteWriter,
13 Deserializable,
14 DeserializationError,
15 Serializable,
16};
17
18#[derive(Debug, Copy, Clone, PartialEq, Eq)]
31pub struct NonFungibleAsset {
32 faucet_id: AccountId,
33 value: Word,
34 callbacks: AssetCallbackFlag,
35}
36
37impl NonFungibleAsset {
38 pub const SERIALIZED_SIZE: usize =
45 AccountId::SERIALIZED_SIZE + Word::SERIALIZED_SIZE + AssetCallbackFlag::SERIALIZED_SIZE;
46
47 pub fn new(details: &NonFungibleAssetDetails) -> Result<Self, AssetError> {
55 let data_hash = Hasher::hash(details.asset_data());
56 Self::from_parts(details.faucet_id(), data_hash)
57 }
58
59 pub fn from_parts(faucet_id: AccountId, value: Word) -> Result<Self, AssetError> {
68 if !matches!(faucet_id.account_type(), AccountType::NonFungibleFaucet) {
69 return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id));
70 }
71
72 Ok(Self {
73 faucet_id,
74 value,
75 callbacks: AssetCallbackFlag::default(),
76 })
77 }
78
79 pub fn from_key_value(key: AssetVaultKey, value: Word) -> Result<Self, AssetError> {
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 pub fn from_key_value_words(key: Word, value: Word) -> Result<Self, AssetError> {
112 let vault_key = AssetVaultKey::try_from(key)?;
113 Self::from_key_value(vault_key, value)
114 }
115
116 pub fn with_callbacks(mut self, callbacks: AssetCallbackFlag) -> Self {
118 self.callbacks = callbacks;
119 self
120 }
121
122 pub fn vault_key(&self) -> AssetVaultKey {
129 let asset_id_suffix = self.value[0];
130 let asset_id_prefix = self.value[1];
131 let asset_id = AssetId::new(asset_id_suffix, asset_id_prefix);
132
133 AssetVaultKey::new(asset_id, self.faucet_id, self.callbacks)
134 .expect("constructors should ensure account ID is of type non-fungible faucet")
135 }
136
137 pub fn faucet_id(&self) -> AccountId {
139 self.faucet_id
140 }
141
142 pub fn callbacks(&self) -> AssetCallbackFlag {
144 self.callbacks
145 }
146
147 pub fn to_key_word(&self) -> Word {
149 self.vault_key().to_word()
150 }
151
152 pub fn to_value_word(&self) -> Word {
154 self.value
155 }
156}
157
158impl fmt::Display for NonFungibleAsset {
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 write!(f, "{self:?}")
162 }
163}
164
165impl From<NonFungibleAsset> for Asset {
166 fn from(asset: NonFungibleAsset) -> Self {
167 Asset::NonFungible(asset)
168 }
169}
170
171impl Serializable for NonFungibleAsset {
175 fn write_into<W: ByteWriter>(&self, target: &mut W) {
176 target.write(self.faucet_id());
179 target.write(self.value);
180 target.write(self.callbacks);
181 }
182
183 fn get_size_hint(&self) -> usize {
184 self.faucet_id.get_size_hint() + self.value.get_size_hint() + self.callbacks.get_size_hint()
185 }
186}
187
188impl Deserializable for NonFungibleAsset {
189 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
190 let faucet_id: AccountId = source.read()?;
191
192 Self::deserialize_with_faucet_id(faucet_id, source)
193 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
194 }
195}
196
197impl NonFungibleAsset {
198 pub(super) fn deserialize_with_faucet_id<R: ByteReader>(
201 faucet_id: AccountId,
202 source: &mut R,
203 ) -> Result<Self, DeserializationError> {
204 let value: Word = source.read()?;
205 let callbacks: AssetCallbackFlag = source.read()?;
206
207 NonFungibleAsset::from_parts(faucet_id, value)
208 .map(|asset| asset.with_callbacks(callbacks))
209 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
210 }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
220pub struct NonFungibleAssetDetails {
221 faucet_id: AccountId,
222 asset_data: Vec<u8>,
223}
224
225impl NonFungibleAssetDetails {
226 pub fn new(faucet_id: AccountId, asset_data: Vec<u8>) -> Result<Self, AssetError> {
231 if !matches!(faucet_id.account_type(), AccountType::NonFungibleFaucet) {
232 return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id));
233 }
234
235 Ok(Self { faucet_id, asset_data })
236 }
237
238 pub fn faucet_id(&self) -> AccountId {
240 self.faucet_id
241 }
242
243 pub fn asset_data(&self) -> &[u8] {
245 &self.asset_data
246 }
247}
248
249#[cfg(test)]
253mod tests {
254 use assert_matches::assert_matches;
255
256 use super::*;
257 use crate::Felt;
258 use crate::account::AccountId;
259 use crate::testing::account_id::{
260 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
261 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
262 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
263 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
264 };
265
266 #[test]
267 fn fungible_asset_from_key_value_fails_on_invalid_asset_id() -> anyhow::Result<()> {
268 let invalid_key = AssetVaultKey::new_native(
269 AssetId::new(Felt::from(1u32), Felt::from(2u32)),
270 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET.try_into()?,
271 )?;
272 let err =
273 NonFungibleAsset::from_key_value(invalid_key, Word::from([4, 5, 6, 7u32])).unwrap_err();
274
275 assert_matches!(err, AssetError::NonFungibleAssetIdMustMatchValue { .. });
276
277 Ok(())
278 }
279
280 #[test]
281 fn test_non_fungible_asset_serde() -> anyhow::Result<()> {
282 for non_fungible_account_id in [
283 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
284 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
285 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
286 ] {
287 let account_id = AccountId::try_from(non_fungible_account_id).unwrap();
288 let details = NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]).unwrap();
289 let non_fungible_asset = NonFungibleAsset::new(&details).unwrap();
290 assert_eq!(
291 non_fungible_asset,
292 NonFungibleAsset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap()
293 );
294 assert_eq!(non_fungible_asset.to_bytes().len(), non_fungible_asset.get_size_hint());
295
296 assert_eq!(
297 non_fungible_asset,
298 NonFungibleAsset::from_key_value_words(
299 non_fungible_asset.to_key_word(),
300 non_fungible_asset.to_value_word()
301 )?
302 )
303 }
304
305 let account = AccountId::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET).unwrap();
306 let details = NonFungibleAssetDetails::new(account, vec![4, 5, 6, 7]).unwrap();
307 let asset = NonFungibleAsset::new(&details).unwrap();
308 let mut asset_bytes = asset.to_bytes();
309
310 let fungible_faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
311
312 asset_bytes[0..AccountId::SERIALIZED_SIZE].copy_from_slice(&fungible_faucet_id.to_bytes());
314
315 let err = NonFungibleAsset::read_from_bytes(&asset_bytes).unwrap_err();
316 assert_matches!(err, DeserializationError::InvalidValue(msg) if msg.contains("must be of type NonFungibleFaucet"));
317
318 Ok(())
319 }
320
321 #[test]
322 fn test_vault_key_for_non_fungible_asset() {
323 let asset = NonFungibleAsset::mock(&[42]);
324
325 assert_eq!(asset.vault_key().faucet_id(), NonFungibleAsset::mock_issuer());
326 assert_eq!(asset.vault_key().asset_id().suffix(), asset.to_value_word()[0]);
327 assert_eq!(asset.vault_key().asset_id().prefix(), asset.to_value_word()[1]);
328 }
329}