1use super::errors::{AssetError, TokenSymbolError};
2use super::utils::serde::{
3 ByteReader,
4 ByteWriter,
5 Deserializable,
6 DeserializationError,
7 Serializable,
8};
9use super::{Felt, Word};
10use crate::account::AccountId;
11
12mod asset_amount;
13pub use asset_amount::AssetAmount;
14
15mod fungible;
16
17pub use fungible::FungibleAsset;
18
19mod nonfungible;
20
21pub use nonfungible::{NonFungibleAsset, NonFungibleAssetDetails};
22
23mod token_symbol;
24pub use token_symbol::TokenSymbol;
25
26mod asset_callbacks;
27pub use asset_callbacks::AssetCallbacks;
28
29mod asset_composition;
30pub use asset_composition::AssetComposition;
31
32mod vault;
33pub use vault::{AssetClass, AssetId, AssetIdHash, AssetVault, AssetWitness, PartialVault};
34
35#[derive(Debug, Copy, Clone, PartialEq, Eq)]
94pub enum Asset {
95 Fungible(FungibleAsset),
96 NonFungible(NonFungibleAsset),
97}
98
99impl Asset {
100 pub fn from_id_and_value(id: AssetId, value: Word) -> Result<Self, AssetError> {
107 match id.composition() {
108 AssetComposition::Fungible => {
109 FungibleAsset::from_id_and_value(id, value).map(Asset::Fungible)
110 },
111 AssetComposition::None => {
112 NonFungibleAsset::from_id_and_value(id, value).map(Asset::NonFungible)
113 },
114 AssetComposition::Custom => {
115 Err(AssetError::UnsupportedAssetComposition(AssetComposition::Custom))
116 },
117 }
118 }
119
120 pub fn from_id_and_value_words(id: Word, value: Word) -> Result<Self, AssetError> {
130 let asset_id = AssetId::try_from(id)?;
131 Self::from_id_and_value(asset_id, value)
132 }
133
134 pub fn is_same(&self, other: &Self) -> bool {
138 self.id() == other.id()
139 }
140
141 pub fn is_fungible(&self) -> bool {
143 matches!(self, Self::Fungible(_))
144 }
145
146 pub fn is_non_fungible(&self) -> bool {
148 matches!(self, Self::NonFungible(_))
149 }
150
151 pub fn faucet_id(&self) -> AccountId {
153 match self {
154 Self::Fungible(asset) => asset.faucet_id(),
155 Self::NonFungible(asset) => asset.faucet_id(),
156 }
157 }
158
159 pub fn id(&self) -> AssetId {
161 match self {
162 Self::Fungible(asset) => asset.id(),
163 Self::NonFungible(asset) => asset.id(),
164 }
165 }
166
167 pub fn to_id_word(&self) -> Word {
169 self.id().to_word()
170 }
171
172 pub fn to_value_word(&self) -> Word {
174 match self {
175 Asset::Fungible(fungible_asset) => fungible_asset.to_value_word(),
176 Asset::NonFungible(non_fungible_asset) => non_fungible_asset.to_value_word(),
177 }
178 }
179
180 pub fn as_elements(&self) -> [Felt; 8] {
185 let mut elements = [Felt::ZERO; 8];
186 elements[0..4].copy_from_slice(self.to_id_word().as_elements());
187 elements[4..8].copy_from_slice(self.to_value_word().as_elements());
188 elements
189 }
190
191 pub fn unwrap_fungible(&self) -> FungibleAsset {
197 match self {
198 Asset::Fungible(asset) => *asset,
199 Asset::NonFungible(_) => panic!("the asset is non-fungible"),
200 }
201 }
202
203 pub fn unwrap_non_fungible(&self) -> NonFungibleAsset {
209 match self {
210 Asset::Fungible(_) => panic!("the asset is fungible"),
211 Asset::NonFungible(asset) => *asset,
212 }
213 }
214}
215
216impl Serializable for Asset {
220 fn write_into<W: ByteWriter>(&self, target: &mut W) {
221 match self {
222 Asset::Fungible(fungible_asset) => fungible_asset.write_into(target),
223 Asset::NonFungible(non_fungible_asset) => non_fungible_asset.write_into(target),
224 }
225 }
226
227 fn get_size_hint(&self) -> usize {
228 match self {
229 Asset::Fungible(fungible_asset) => fungible_asset.get_size_hint(),
230 Asset::NonFungible(non_fungible_asset) => non_fungible_asset.get_size_hint(),
231 }
232 }
233}
234
235impl Deserializable for Asset {
236 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
237 let composition: AssetComposition = source.read()?;
240 match composition {
241 AssetComposition::Fungible => FungibleAsset::deserialize_body(source).map(Asset::from),
242 AssetComposition::None => NonFungibleAsset::deserialize_body(source).map(Asset::from),
243 AssetComposition::Custom => Err(DeserializationError::InvalidValue(
244 "Custom asset composition is not supported".into(),
245 )),
246 }
247 }
248}
249
250#[cfg(test)]
254mod tests {
255
256 use assert_matches::assert_matches;
257 use miden_core::Word;
258 use miden_crypto::utils::{Deserializable, Serializable};
259
260 use super::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails};
261 use crate::Felt;
262 use crate::account::AccountId;
263 use crate::asset::{AssetClass, AssetComposition, AssetId};
264 use crate::errors::AssetError;
265 use crate::testing::account_id::{
266 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
267 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
268 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
269 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
270 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
271 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
272 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
273 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
274 };
275
276 pub(super) fn asset_metadata(id: AssetId) -> u8 {
278 (id.to_word()[2].as_canonical_u64() & AssetId::METADATA_BYTE_MASK as u64) as u8
279 }
280
281 pub(super) fn set_asset_metadata(id: AssetId, byte: u8) -> Word {
283 let mut id_word = id.to_word();
284 let raw = id_word[2].as_canonical_u64();
285 let new_raw = (raw & !(AssetId::METADATA_BYTE_MASK as u64)) | byte as u64;
286 id_word[2] =
287 Felt::try_from(new_raw).expect("clearing lower bits should produce a valid felt");
288 id_word
289 }
290
291 #[test]
293 fn test_asset_serde() -> anyhow::Result<()> {
294 for fungible_account_id in [
295 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
296 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
297 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
298 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
299 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
300 ] {
301 let account_id = AccountId::try_from(fungible_account_id).unwrap();
302 let fungible_asset: Asset = FungibleAsset::new(account_id, 10).unwrap().into();
303 assert_eq!(fungible_asset, Asset::read_from_bytes(&fungible_asset.to_bytes()).unwrap());
304 assert_eq!(
305 fungible_asset,
306 Asset::from_id_and_value_words(
307 fungible_asset.to_id_word(),
308 fungible_asset.to_value_word()
309 )?,
310 );
311 }
312
313 for non_fungible_account_id in [
314 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
315 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
316 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
317 ] {
318 let account_id = AccountId::try_from(non_fungible_account_id).unwrap();
319 let details = NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]);
320 let non_fungible_asset: Asset = NonFungibleAsset::new(&details).into();
321 assert_eq!(
322 non_fungible_asset,
323 Asset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap()
324 );
325 assert_eq!(
326 non_fungible_asset,
327 Asset::from_id_and_value_words(
328 non_fungible_asset.to_id_word(),
329 non_fungible_asset.to_value_word()
330 )?
331 );
332 }
333
334 Ok(())
335 }
336
337 #[test]
340 fn test_composition_byte_is_serialized_first() {
341 let fungible_bytes = FungibleAsset::mock(300).to_bytes();
342 assert_eq!(fungible_bytes[0], AssetComposition::Fungible.as_u8());
343
344 let non_fungible_bytes = NonFungibleAsset::mock(&[0xaa, 0xbb]).to_bytes();
345 assert_eq!(non_fungible_bytes[0], AssetComposition::None.as_u8());
346 }
347
348 #[test]
351 fn test_from_id_and_value_rejects_custom_composition() -> anyhow::Result<()> {
352 let err = AssetId::new(
353 AssetClass::default(),
354 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?,
355 AssetComposition::Custom,
356 )
357 .unwrap_err();
358
359 assert_matches!(err, AssetError::UnsupportedAssetComposition(AssetComposition::Custom));
360
361 Ok(())
362 }
363}