miden_protocol/asset/
nonfungible.rs1use 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#[derive(Debug, Copy, Clone, PartialEq, Eq)]
30pub struct NonFungibleAsset {
31 faucet_id: AccountId,
32 value: Word,
33 callbacks: AssetCallbackFlag,
34}
35
36impl NonFungibleAsset {
37 pub const SERIALIZED_SIZE: usize = AssetComposition::SERIALIZED_SIZE
45 + AccountId::SERIALIZED_SIZE
46 + Word::SERIALIZED_SIZE
47 + AssetCallbackFlag::SERIALIZED_SIZE;
48
49 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 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 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 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 pub fn with_callbacks(mut self, callbacks: AssetCallbackFlag) -> Self {
117 self.callbacks = callbacks;
118 self
119 }
120
121 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 pub fn faucet_id(&self) -> AccountId {
138 self.faucet_id
139 }
140
141 pub fn callbacks(&self) -> AssetCallbackFlag {
143 self.callbacks
144 }
145
146 pub fn to_key_word(&self) -> Word {
148 self.vault_key().to_word()
149 }
150
151 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 write!(f, "{self:?}")
161 }
162}
163
164impl From<NonFungibleAsset> for Asset {
165 fn from(asset: NonFungibleAsset) -> Self {
166 Asset::NonFungible(asset)
167 }
168}
169
170impl Serializable for NonFungibleAsset {
174 fn write_into<W: ByteWriter>(&self, target: &mut W) {
175 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 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#[derive(Debug, Clone, PartialEq, Eq)]
223pub struct NonFungibleAssetDetails {
224 faucet_id: AccountId,
225 asset_data: Vec<u8>,
226}
227
228impl NonFungibleAssetDetails {
229 pub fn new(faucet_id: AccountId, asset_data: Vec<u8>) -> Self {
231 Self { faucet_id, asset_data }
232 }
233
234 pub fn faucet_id(&self) -> AccountId {
236 self.faucet_id
237 }
238
239 pub fn asset_data(&self) -> &[u8] {
241 &self.asset_data
242 }
243}
244
245#[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 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}