miden_protocol/asset/
nonfungible.rs1use alloc::vec::Vec;
2use core::fmt;
3
4use super::vault::AssetId;
5use super::{Asset, AssetComposition, AssetError, Word};
6use crate::Hasher;
7use crate::account::AccountId;
8use crate::asset::vault::AssetClass;
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}
34
35impl NonFungibleAsset {
36 pub const SERIALIZED_SIZE: usize =
43 AssetComposition::SERIALIZED_SIZE + AccountId::SERIALIZED_SIZE + Word::SERIALIZED_SIZE;
44
45 pub fn new(details: &NonFungibleAssetDetails) -> Self {
50 let data_hash = Hasher::hash(details.asset_data());
51 Self::from_parts(details.faucet_id(), data_hash)
52 }
53
54 pub fn from_parts(faucet_id: AccountId, value: Word) -> Self {
60 Self { faucet_id, value }
61 }
62
63 pub fn from_id_and_value(id: AssetId, value: Word) -> Result<Self, AssetError> {
73 if !id.composition().is_none() {
74 return Err(AssetError::AssetCompositionMismatch {
75 faucet_id: id.faucet_id(),
76 expected: AssetComposition::None,
77 actual: id.composition(),
78 });
79 }
80
81 if id.asset_class().suffix() != value[0] || id.asset_class().prefix() != value[1] {
82 return Err(AssetError::NonFungibleAssetClassMustMatchValue {
83 asset_class: id.asset_class(),
84 value,
85 });
86 }
87
88 Ok(Self::from_parts(id.faucet_id(), value))
89 }
90
91 pub fn from_id_and_value_words(id: Word, value: Word) -> Result<Self, AssetError> {
100 let asset_id = AssetId::try_from(id)?;
101 Self::from_id_and_value(asset_id, value)
102 }
103
104 pub fn id(&self) -> AssetId {
111 let asset_class_suffix = self.value[0];
112 let asset_class_prefix = self.value[1];
113 let asset_class = AssetClass::new(asset_class_suffix, asset_class_prefix);
114
115 AssetId::new(asset_class, self.faucet_id, AssetComposition::None)
116 .expect("non-fungible composition is always valid")
117 }
118
119 pub fn faucet_id(&self) -> AccountId {
121 self.faucet_id
122 }
123
124 pub fn to_id_word(&self) -> Word {
126 self.id().to_word()
127 }
128
129 pub fn to_value_word(&self) -> Word {
131 self.value
132 }
133}
134
135impl fmt::Display for NonFungibleAsset {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 write!(f, "{self:?}")
139 }
140}
141
142impl From<NonFungibleAsset> for Asset {
143 fn from(asset: NonFungibleAsset) -> Self {
144 Asset::NonFungible(asset)
145 }
146}
147
148impl Serializable for NonFungibleAsset {
152 fn write_into<W: ByteWriter>(&self, target: &mut W) {
153 target.write(AssetComposition::None);
155 target.write(self.faucet_id());
156 target.write(self.value);
157 }
158
159 fn get_size_hint(&self) -> usize {
160 AssetComposition::SERIALIZED_SIZE
161 + self.faucet_id.get_size_hint()
162 + self.value.get_size_hint()
163 }
164}
165
166impl Deserializable for NonFungibleAsset {
167 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
168 let composition: AssetComposition = source.read()?;
169 if !composition.is_none() {
170 return Err(DeserializationError::InvalidValue(format!(
171 "expected non-fungible asset composition but found {composition:?}"
172 )));
173 }
174 NonFungibleAsset::deserialize_body(source)
175 }
176}
177
178impl NonFungibleAsset {
179 pub(super) fn deserialize_body<R: ByteReader>(
182 source: &mut R,
183 ) -> Result<Self, DeserializationError> {
184 let faucet_id: AccountId = source.read()?;
185 let value: Word = source.read()?;
186
187 Ok(NonFungibleAsset::from_parts(faucet_id, value))
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct NonFungibleAssetDetails {
199 faucet_id: AccountId,
200 asset_data: Vec<u8>,
201}
202
203impl NonFungibleAssetDetails {
204 pub fn new(faucet_id: AccountId, asset_data: Vec<u8>) -> Self {
206 Self { faucet_id, asset_data }
207 }
208
209 pub fn faucet_id(&self) -> AccountId {
211 self.faucet_id
212 }
213
214 pub fn asset_data(&self) -> &[u8] {
216 &self.asset_data
217 }
218}
219
220#[cfg(test)]
224mod tests {
225 use assert_matches::assert_matches;
226
227 use super::*;
228 use crate::Felt;
229 use crate::account::AccountId;
230 use crate::asset::FungibleAsset;
231 use crate::testing::account_id::{
232 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
233 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
234 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
235 };
236
237 #[test]
238 fn non_fungible_asset_from_id_and_value_words_fails_on_invalid_composition()
239 -> anyhow::Result<()> {
240 let asset = FungibleAsset::mock(20);
242
243 let err =
244 NonFungibleAsset::from_id_and_value_words(asset.to_id_word(), asset.to_value_word())
245 .unwrap_err();
246 assert_matches!(err, AssetError::AssetCompositionMismatch {
247 faucet_id: _, expected, actual,
248 } => {
249 assert_eq!(actual, AssetComposition::Fungible);
250 assert_eq!(expected, AssetComposition::None);
251 });
252
253 Ok(())
254 }
255
256 #[test]
257 fn non_fungible_asset_from_id_and_value_fails_on_invalid_asset_class() -> anyhow::Result<()> {
258 let invalid_id = AssetId::new(
259 AssetClass::new(Felt::from(1u32), Felt::from(2u32)),
260 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET.try_into()?,
261 AssetComposition::None,
262 )?;
263 let err = NonFungibleAsset::from_id_and_value(invalid_id, Word::from([4, 5, 6, 7u32]))
264 .unwrap_err();
265
266 assert_matches!(err, AssetError::NonFungibleAssetClassMustMatchValue { .. });
267
268 Ok(())
269 }
270
271 #[test]
272 fn test_non_fungible_asset_serde() -> anyhow::Result<()> {
273 for non_fungible_account_id in [
274 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
275 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
276 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
277 ] {
278 let account_id = AccountId::try_from(non_fungible_account_id).unwrap();
279 let details = NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]);
280 let non_fungible_asset = NonFungibleAsset::new(&details);
281 assert_eq!(
282 non_fungible_asset,
283 NonFungibleAsset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap()
284 );
285 assert_eq!(non_fungible_asset.to_bytes().len(), non_fungible_asset.get_size_hint());
286
287 assert_eq!(
288 non_fungible_asset,
289 NonFungibleAsset::from_id_and_value_words(
290 non_fungible_asset.to_id_word(),
291 non_fungible_asset.to_value_word()
292 )?
293 )
294 }
295
296 let fungible_asset = FungibleAsset::mock(42);
297 let err = NonFungibleAsset::read_from_bytes(&fungible_asset.to_bytes()).unwrap_err();
298 assert_matches!(err, DeserializationError::InvalidValue(msg) => {
299 assert!(msg.contains("expected non-fungible asset composition but found Fungible"));
300 });
301
302 Ok(())
303 }
304
305 #[test]
306 fn test_asset_id_for_non_fungible_asset() {
307 let asset = NonFungibleAsset::mock(&[42]);
308
309 assert_eq!(asset.id().faucet_id(), NonFungibleAsset::mock_issuer());
310 assert_eq!(asset.id().asset_class().suffix(), asset.to_value_word()[0]);
311 assert_eq!(asset.id().asset_class().prefix(), asset.to_value_word()[1]);
312 }
313}