use alloc::string::ToString;
use core::fmt;
use super::errors::{AssetError, TokenSymbolError};
use super::utils::serde::{
ByteReader,
ByteWriter,
Deserializable,
DeserializationError,
Serializable,
};
use super::{Felt, Word};
use crate::account::AccountId;
mod asset_amount;
pub use asset_amount::AssetAmount;
mod asset_value;
pub use asset_value::AssetValue;
mod fungible;
pub use fungible::FungibleAsset;
mod nonfungible;
pub use nonfungible::{NonFungibleAsset, NonFungibleAssetDetails};
mod token_symbol;
pub use token_symbol::TokenSymbol;
mod asset_callbacks;
pub use asset_callbacks::AssetCallbacks;
mod asset_composition;
pub use asset_composition::AssetComposition;
mod vault;
pub use vault::{AssetClass, AssetId, AssetIdHash, AssetVault, AssetWitness, PartialVault};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Asset {
id: AssetId,
value: AssetValue,
}
impl Asset {
pub fn new(id: AssetId, value: Word) -> Result<Self, AssetError> {
if id.composition().is_fungible() {
FungibleAsset::from_id_and_value(id, value)?;
}
Ok(Self { id, value: AssetValue::from_raw(value) })
}
pub fn from_id_and_value_words(id: Word, value: Word) -> Result<Self, AssetError> {
let asset_id = AssetId::try_from(id)?;
Self::new(asset_id, value)
}
pub fn is_same(&self, other: &Self) -> bool {
self.id() == other.id()
}
pub fn is_fungible(&self) -> bool {
self.id.composition().is_fungible()
}
pub fn is_non_fungible(&self) -> bool {
self.id.composition().is_none()
}
pub fn faucet_id(&self) -> AccountId {
self.id.faucet_id()
}
pub fn id(&self) -> AssetId {
self.id
}
pub fn value(&self) -> AssetValue {
self.value
}
pub fn to_id_word(&self) -> Word {
self.id().to_word()
}
pub fn to_value_word(&self) -> Word {
self.value.as_word()
}
pub fn as_elements(&self) -> [Felt; 8] {
let mut elements = [Felt::ZERO; 8];
elements[0..4].copy_from_slice(self.to_id_word().as_elements());
elements[4..8].copy_from_slice(self.to_value_word().as_elements());
elements
}
pub fn as_fungible(&self) -> Option<FungibleAsset> {
FungibleAsset::from_id_and_value(self.id, self.to_value_word()).ok()
}
pub fn unwrap_fungible(&self) -> FungibleAsset {
self.as_fungible().expect("the asset should be fungible")
}
}
impl fmt::Display for Asset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Asset(id: {}, value: {})", self.id, self.value)
}
}
impl Serializable for Asset {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
target.write(self.id);
target.write(self.value);
}
fn get_size_hint(&self) -> usize {
self.id.get_size_hint() + self.value.get_size_hint()
}
}
impl Deserializable for Asset {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let id: AssetId = source.read()?;
let value: AssetValue = source.read()?;
Asset::new(id, value.as_word())
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use miden_core::Word;
use miden_crypto::utils::{Deserializable, Serializable};
use super::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails};
use crate::Felt;
use crate::account::AccountId;
use crate::asset::{AssetClass, AssetComposition, AssetId};
use crate::errors::AssetError;
use crate::testing::account_id::{
ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
};
pub(super) fn asset_metadata(id: AssetId) -> u8 {
(id.to_word()[2].as_canonical_u64() & AssetId::METADATA_BYTE_MASK as u64) as u8
}
pub(super) fn set_asset_metadata(id: AssetId, byte: u8) -> Word {
let mut id_word = id.to_word();
let raw = id_word[2].as_canonical_u64();
let new_raw = (raw & !(AssetId::METADATA_BYTE_MASK as u64)) | byte as u64;
id_word[2] =
Felt::try_from(new_raw).expect("clearing lower bits should produce a valid felt");
id_word
}
#[test]
fn test_asset_serde() -> anyhow::Result<()> {
for fungible_account_id in [
ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
] {
let account_id = AccountId::try_from(fungible_account_id).unwrap();
let fungible_asset: Asset = FungibleAsset::new(account_id, 10).unwrap().into();
assert_eq!(fungible_asset, Asset::read_from_bytes(&fungible_asset.to_bytes()).unwrap());
assert_eq!(
fungible_asset,
Asset::from_id_and_value_words(
fungible_asset.to_id_word(),
fungible_asset.to_value_word()
)?,
);
}
for non_fungible_account_id in [
ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
] {
let account_id = AccountId::try_from(non_fungible_account_id).unwrap();
let details = NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]);
let non_fungible_asset: Asset = NonFungibleAsset::new(&details).into();
assert_eq!(
non_fungible_asset,
Asset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap()
);
assert_eq!(
non_fungible_asset,
Asset::from_id_and_value_words(
non_fungible_asset.to_id_word(),
non_fungible_asset.to_value_word()
)?
);
}
Ok(())
}
#[test]
fn test_from_id_and_value_rejects_custom_composition() -> anyhow::Result<()> {
let err = AssetId::new(
AssetClass::default(),
ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?,
AssetComposition::Custom,
)
.unwrap_err();
assert_matches!(err, AssetError::UnsupportedAssetComposition(AssetComposition::Custom));
Ok(())
}
#[test]
fn test_opaque_asset_roundtrip() -> anyhow::Result<()> {
let asset_id = AssetId::new(
AssetClass::new(Felt::from(1u32), Felt::from(2u32)),
ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET.try_into()?,
AssetComposition::None,
)?;
let value = Word::from([7, 8, 9, 10u32]);
let asset = Asset::new(asset_id, value)?;
assert_eq!(asset.id(), asset_id);
assert_eq!(asset.to_value_word(), value);
assert_eq!(asset, Asset::read_from_bytes(&asset.to_bytes()).unwrap());
assert_eq!(asset.to_bytes().len(), asset.get_size_hint());
assert_eq!(asset, Asset::from_id_and_value_words(asset.to_id_word(), value)?);
Ok(())
}
}