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)]
38pub struct NonFungibleAsset {
39 faucet_id: AccountId,
40 value: Word,
41}
42
43impl NonFungibleAsset {
44 pub const SERIALIZED_SIZE: usize =
51 AssetComposition::SERIALIZED_SIZE + AccountId::SERIALIZED_SIZE + Word::SERIALIZED_SIZE;
52
53 pub fn new(details: &NonFungibleAssetDetails) -> Self {
58 let data_hash = Hasher::hash(details.asset_data());
59 Self::from_parts(details.faucet_id(), data_hash)
60 }
61
62 pub fn from_parts(faucet_id: AccountId, value: Word) -> Self {
68 Self { faucet_id, value }
69 }
70
71 pub fn from_id_and_value(id: AssetId, value: Word) -> Result<Self, AssetError> {
81 if !id.composition().is_none() {
82 return Err(AssetError::AssetCompositionMismatch {
83 faucet_id: id.faucet_id(),
84 expected: AssetComposition::None,
85 actual: id.composition(),
86 });
87 }
88
89 if id.asset_class().suffix() != value[0] || id.asset_class().prefix() != value[1] {
90 return Err(AssetError::NonFungibleAssetClassMustMatchValue {
91 asset_class: id.asset_class(),
92 value,
93 });
94 }
95
96 Ok(Self::from_parts(id.faucet_id(), value))
97 }
98
99 pub fn from_id_and_value_words(id: Word, value: Word) -> Result<Self, AssetError> {
108 let asset_id = AssetId::try_from(id)?;
109 Self::from_id_and_value(asset_id, value)
110 }
111
112 pub fn id(&self) -> AssetId {
119 let asset_class_suffix = self.value[0];
120 let asset_class_prefix = self.value[1];
121 let asset_class = AssetClass::new(asset_class_suffix, asset_class_prefix);
122
123 AssetId::new(asset_class, self.faucet_id, AssetComposition::None)
124 .expect("non-fungible composition is always valid")
125 }
126
127 pub fn faucet_id(&self) -> AccountId {
129 self.faucet_id
130 }
131
132 pub fn to_id_word(&self) -> Word {
134 self.id().to_word()
135 }
136
137 pub fn to_value_word(&self) -> Word {
139 self.value
140 }
141}
142
143impl fmt::Display for NonFungibleAsset {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 write!(f, "{self:?}")
147 }
148}
149
150impl From<NonFungibleAsset> for Asset {
151 fn from(asset: NonFungibleAsset) -> Self {
152 Asset::new(asset.id(), asset.to_value_word())
153 .expect("non-fungible asset should be a valid asset")
154 }
155}
156
157impl Serializable for NonFungibleAsset {
161 fn write_into<W: ByteWriter>(&self, target: &mut W) {
162 target.write(AssetComposition::None);
164 target.write(self.faucet_id());
165 target.write(self.value);
166 }
167
168 fn get_size_hint(&self) -> usize {
169 AssetComposition::SERIALIZED_SIZE
170 + self.faucet_id.get_size_hint()
171 + self.value.get_size_hint()
172 }
173}
174
175impl Deserializable for NonFungibleAsset {
176 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
177 let composition: AssetComposition = source.read()?;
178 if !composition.is_none() {
179 return Err(DeserializationError::InvalidValue(format!(
180 "expected non-fungible asset composition but found {composition:?}"
181 )));
182 }
183
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}