1use alloc::string::ToString;
2use core::fmt;
3
4use super::errors::{AssetError, TokenSymbolError};
5use super::utils::serde::{
6 ByteReader,
7 ByteWriter,
8 Deserializable,
9 DeserializationError,
10 Serializable,
11};
12use super::{Felt, Word};
13use crate::account::AccountId;
14
15mod asset_amount;
16pub use asset_amount::AssetAmount;
17
18mod asset_value;
19pub use asset_value::AssetValue;
20
21mod fungible;
22
23pub use fungible::FungibleAsset;
24
25mod nonfungible;
26
27pub use nonfungible::{NonFungibleAsset, NonFungibleAssetDetails};
28
29mod token_symbol;
30pub use token_symbol::TokenSymbol;
31
32mod asset_callbacks;
33pub use asset_callbacks::AssetCallbacks;
34
35mod asset_composition;
36pub use asset_composition::AssetComposition;
37
38mod vault;
39pub use vault::{AssetClass, AssetId, AssetIdHash, AssetVault, AssetWitness, PartialVault};
40
41#[derive(Debug, Copy, Clone, PartialEq, Eq)]
82pub struct Asset {
83 id: AssetId,
84 value: AssetValue,
85}
86
87impl Asset {
88 pub fn new(id: AssetId, value: Word) -> Result<Self, AssetError> {
98 if id.composition().is_fungible() {
101 FungibleAsset::from_id_and_value(id, value)?;
102 }
103
104 Ok(Self { id, value: AssetValue::from_raw(value) })
106 }
107
108 pub fn from_id_and_value_words(id: Word, value: Word) -> Result<Self, AssetError> {
118 let asset_id = AssetId::try_from(id)?;
119 Self::new(asset_id, value)
120 }
121
122 pub fn is_same(&self, other: &Self) -> bool {
126 self.id() == other.id()
127 }
128
129 pub fn is_fungible(&self) -> bool {
131 self.id.composition().is_fungible()
132 }
133
134 pub fn is_non_fungible(&self) -> bool {
136 self.id.composition().is_none()
137 }
138
139 pub fn faucet_id(&self) -> AccountId {
141 self.id.faucet_id()
142 }
143
144 pub fn id(&self) -> AssetId {
146 self.id
147 }
148
149 pub fn value(&self) -> AssetValue {
151 self.value
152 }
153
154 pub fn to_id_word(&self) -> Word {
156 self.id().to_word()
157 }
158
159 pub fn to_value_word(&self) -> Word {
161 self.value.as_word()
162 }
163
164 pub fn as_elements(&self) -> [Felt; 8] {
169 let mut elements = [Felt::ZERO; 8];
170 elements[0..4].copy_from_slice(self.to_id_word().as_elements());
171 elements[4..8].copy_from_slice(self.to_value_word().as_elements());
172 elements
173 }
174
175 pub fn as_fungible(&self) -> Option<FungibleAsset> {
178 FungibleAsset::from_id_and_value(self.id, self.to_value_word()).ok()
179 }
180
181 pub fn unwrap_fungible(&self) -> FungibleAsset {
187 self.as_fungible().expect("the asset should be fungible")
188 }
189}
190
191impl fmt::Display for Asset {
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 write!(f, "Asset(id: {}, value: {})", self.id, self.value)
194 }
195}
196
197impl Serializable for Asset {
201 fn write_into<W: ByteWriter>(&self, target: &mut W) {
202 target.write(self.id);
203 target.write(self.value);
204 }
205
206 fn get_size_hint(&self) -> usize {
207 self.id.get_size_hint() + self.value.get_size_hint()
208 }
209}
210
211impl Deserializable for Asset {
212 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
213 let id: AssetId = source.read()?;
214 let value: AssetValue = source.read()?;
215
216 Asset::new(id, value.as_word())
217 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
218 }
219}
220
221#[cfg(test)]
225mod tests {
226
227 use assert_matches::assert_matches;
228 use miden_core::Word;
229 use miden_crypto::utils::{Deserializable, Serializable};
230
231 use super::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails};
232 use crate::Felt;
233 use crate::account::AccountId;
234 use crate::asset::{AssetClass, AssetComposition, AssetId};
235 use crate::errors::AssetError;
236 use crate::testing::account_id::{
237 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
238 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
239 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
240 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
241 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
242 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
243 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
244 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
245 };
246
247 pub(super) fn asset_metadata(id: AssetId) -> u8 {
249 (id.to_word()[2].as_canonical_u64() & AssetId::METADATA_BYTE_MASK as u64) as u8
250 }
251
252 pub(super) fn set_asset_metadata(id: AssetId, byte: u8) -> Word {
254 let mut id_word = id.to_word();
255 let raw = id_word[2].as_canonical_u64();
256 let new_raw = (raw & !(AssetId::METADATA_BYTE_MASK as u64)) | byte as u64;
257 id_word[2] =
258 Felt::try_from(new_raw).expect("clearing lower bits should produce a valid felt");
259 id_word
260 }
261
262 #[test]
264 fn test_asset_serde() -> anyhow::Result<()> {
265 for fungible_account_id in [
266 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
267 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
268 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
269 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
270 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
271 ] {
272 let account_id = AccountId::try_from(fungible_account_id).unwrap();
273 let fungible_asset: Asset = FungibleAsset::new(account_id, 10).unwrap().into();
274 assert_eq!(fungible_asset, Asset::read_from_bytes(&fungible_asset.to_bytes()).unwrap());
275 assert_eq!(
276 fungible_asset,
277 Asset::from_id_and_value_words(
278 fungible_asset.to_id_word(),
279 fungible_asset.to_value_word()
280 )?,
281 );
282 }
283
284 for non_fungible_account_id in [
285 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
286 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
287 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
288 ] {
289 let account_id = AccountId::try_from(non_fungible_account_id).unwrap();
290 let details = NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]);
291 let non_fungible_asset: Asset = NonFungibleAsset::new(&details).into();
292 assert_eq!(
293 non_fungible_asset,
294 Asset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap()
295 );
296 assert_eq!(
297 non_fungible_asset,
298 Asset::from_id_and_value_words(
299 non_fungible_asset.to_id_word(),
300 non_fungible_asset.to_value_word()
301 )?
302 );
303 }
304
305 Ok(())
306 }
307
308 #[test]
311 fn test_from_id_and_value_rejects_custom_composition() -> anyhow::Result<()> {
312 let err = AssetId::new(
313 AssetClass::default(),
314 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?,
315 AssetComposition::Custom,
316 )
317 .unwrap_err();
318
319 assert_matches!(err, AssetError::UnsupportedAssetComposition(AssetComposition::Custom));
320
321 Ok(())
322 }
323
324 #[test]
326 fn test_opaque_asset_roundtrip() -> anyhow::Result<()> {
327 let asset_id = AssetId::new(
328 AssetClass::new(Felt::from(1u32), Felt::from(2u32)),
329 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET.try_into()?,
330 AssetComposition::None,
331 )?;
332 let value = Word::from([7, 8, 9, 10u32]);
333 let asset = Asset::new(asset_id, value)?;
334
335 assert_eq!(asset.id(), asset_id);
336 assert_eq!(asset.to_value_word(), value);
337 assert_eq!(asset, Asset::read_from_bytes(&asset.to_bytes()).unwrap());
338 assert_eq!(asset.to_bytes().len(), asset.get_size_hint());
339 assert_eq!(asset, Asset::from_id_and_value_words(asset.to_id_word(), value)?);
340
341 Ok(())
342 }
343}