miden_protocol/asset/
nonfungible.rs1use alloc::string::ToString;
2use alloc::vec::Vec;
3use core::fmt;
4
5use super::vault::AssetVaultKey;
6use super::{AccountType, Asset, 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)]
28pub struct NonFungibleAsset {
29 faucet_id: AccountId,
30 value: Word,
31}
32
33impl NonFungibleAsset {
34 pub const SERIALIZED_SIZE: usize = AccountId::SERIALIZED_SIZE + Word::SERIALIZED_SIZE;
41
42 pub fn new(details: &NonFungibleAssetDetails) -> Result<Self, AssetError> {
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) -> Result<Self, AssetError> {
63 if !matches!(faucet_id.account_type(), AccountType::NonFungibleFaucet) {
64 return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id));
65 }
66
67 Ok(Self { faucet_id, value })
68 }
69
70 pub fn from_key_value(key: AssetVaultKey, value: Word) -> Result<Self, AssetError> {
80 if key.asset_id().suffix() != value[0] || key.asset_id().prefix() != value[1] {
81 return Err(AssetError::NonFungibleAssetIdMustMatchValue {
82 asset_id: key.asset_id(),
83 value,
84 });
85 }
86
87 Self::from_parts(key.faucet_id(), value)
88 }
89
90 pub fn from_key_value_words(key: Word, value: Word) -> Result<Self, AssetError> {
100 let vault_key = AssetVaultKey::try_from(key)?;
101 Self::from_key_value(vault_key, value)
102 }
103
104 pub fn vault_key(&self) -> AssetVaultKey {
111 let asset_id_suffix = self.value[0];
112 let asset_id_prefix = self.value[1];
113 let asset_id = AssetId::new(asset_id_suffix, asset_id_prefix);
114
115 AssetVaultKey::new(asset_id, self.faucet_id)
116 .expect("constructors should ensure account ID is of type non-fungible faucet")
117 }
118
119 pub fn faucet_id(&self) -> AccountId {
121 self.faucet_id
122 }
123
124 pub fn to_key_word(&self) -> Word {
126 self.vault_key().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(self.faucet_id());
156 target.write(self.value);
157 }
158
159 fn get_size_hint(&self) -> usize {
160 Self::SERIALIZED_SIZE
161 }
162}
163
164impl Deserializable for NonFungibleAsset {
165 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
166 let faucet_id: AccountId = source.read()?;
167
168 Self::deserialize_with_faucet_id(faucet_id, source)
169 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
170 }
171}
172
173impl NonFungibleAsset {
174 pub(super) fn deserialize_with_faucet_id<R: ByteReader>(
177 faucet_id: AccountId,
178 source: &mut R,
179 ) -> Result<Self, DeserializationError> {
180 let value: Word = source.read()?;
181
182 NonFungibleAsset::from_parts(faucet_id, value)
183 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct NonFungibleAssetDetails {
195 faucet_id: AccountId,
196 asset_data: Vec<u8>,
197}
198
199impl NonFungibleAssetDetails {
200 pub fn new(faucet_id: AccountId, asset_data: Vec<u8>) -> Result<Self, AssetError> {
205 if !matches!(faucet_id.account_type(), AccountType::NonFungibleFaucet) {
206 return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id));
207 }
208
209 Ok(Self { faucet_id, asset_data })
210 }
211
212 pub fn faucet_id(&self) -> AccountId {
214 self.faucet_id
215 }
216
217 pub fn asset_data(&self) -> &[u8] {
219 &self.asset_data
220 }
221}
222
223#[cfg(test)]
227mod tests {
228 use assert_matches::assert_matches;
229
230 use super::*;
231 use crate::Felt;
232 use crate::account::AccountId;
233 use crate::testing::account_id::{
234 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
235 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
236 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
237 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
238 };
239
240 #[test]
241 fn fungible_asset_from_key_value_fails_on_invalid_asset_id() -> anyhow::Result<()> {
242 let invalid_key = AssetVaultKey::new(
243 AssetId::new(Felt::from(1u32), Felt::from(2u32)),
244 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET.try_into()?,
245 )?;
246 let err =
247 NonFungibleAsset::from_key_value(invalid_key, Word::from([4, 5, 6, 7u32])).unwrap_err();
248
249 assert_matches!(err, AssetError::NonFungibleAssetIdMustMatchValue { .. });
250
251 Ok(())
252 }
253
254 #[test]
255 fn test_non_fungible_asset_serde() -> anyhow::Result<()> {
256 for non_fungible_account_id in [
257 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
258 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
259 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
260 ] {
261 let account_id = AccountId::try_from(non_fungible_account_id).unwrap();
262 let details = NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]).unwrap();
263 let non_fungible_asset = NonFungibleAsset::new(&details).unwrap();
264 assert_eq!(
265 non_fungible_asset,
266 NonFungibleAsset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap()
267 );
268 assert_eq!(non_fungible_asset.to_bytes().len(), non_fungible_asset.get_size_hint());
269
270 assert_eq!(
271 non_fungible_asset,
272 NonFungibleAsset::from_key_value_words(
273 non_fungible_asset.to_key_word(),
274 non_fungible_asset.to_value_word()
275 )?
276 )
277 }
278
279 let account = AccountId::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET).unwrap();
280 let details = NonFungibleAssetDetails::new(account, vec![4, 5, 6, 7]).unwrap();
281 let asset = NonFungibleAsset::new(&details).unwrap();
282 let mut asset_bytes = asset.to_bytes();
283
284 let fungible_faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
285
286 asset_bytes[0..AccountId::SERIALIZED_SIZE].copy_from_slice(&fungible_faucet_id.to_bytes());
288
289 let err = NonFungibleAsset::read_from_bytes(&asset_bytes).unwrap_err();
290 assert_matches!(err, DeserializationError::InvalidValue(msg) if msg.contains("must be of type NonFungibleFaucet"));
291
292 Ok(())
293 }
294
295 #[test]
296 fn test_vault_key_for_non_fungible_asset() {
297 let asset = NonFungibleAsset::mock(&[42]);
298
299 assert_eq!(asset.vault_key().faucet_id(), NonFungibleAsset::mock_issuer());
300 assert_eq!(asset.vault_key().asset_id().suffix(), asset.to_value_word()[0]);
301 assert_eq!(asset.vault_key().asset_id().prefix(), asset.to_value_word()[1]);
302 }
303}