use alloc::vec::Vec;
use core::fmt;
use super::vault::AssetId;
use super::{Asset, AssetComposition, AssetError, Word};
use crate::Hasher;
use crate::account::AccountId;
use crate::asset::vault::AssetClass;
use crate::utils::serde::{
ByteReader,
ByteWriter,
Deserializable,
DeserializationError,
Serializable,
};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct NonFungibleAsset {
faucet_id: AccountId,
value: Word,
}
impl NonFungibleAsset {
pub const SERIALIZED_SIZE: usize =
AssetComposition::SERIALIZED_SIZE + AccountId::SERIALIZED_SIZE + Word::SERIALIZED_SIZE;
pub fn new(details: &NonFungibleAssetDetails) -> Self {
let data_hash = Hasher::hash(details.asset_data());
Self::from_parts(details.faucet_id(), data_hash)
}
pub fn from_parts(faucet_id: AccountId, value: Word) -> Self {
Self { faucet_id, value }
}
pub fn from_id_and_value(id: AssetId, value: Word) -> Result<Self, AssetError> {
if !id.composition().is_none() {
return Err(AssetError::AssetCompositionMismatch {
faucet_id: id.faucet_id(),
expected: AssetComposition::None,
actual: id.composition(),
});
}
if id.asset_class().suffix() != value[0] || id.asset_class().prefix() != value[1] {
return Err(AssetError::NonFungibleAssetClassMustMatchValue {
asset_class: id.asset_class(),
value,
});
}
Ok(Self::from_parts(id.faucet_id(), value))
}
pub fn from_id_and_value_words(id: Word, value: Word) -> Result<Self, AssetError> {
let asset_id = AssetId::try_from(id)?;
Self::from_id_and_value(asset_id, value)
}
pub fn id(&self) -> AssetId {
let asset_class_suffix = self.value[0];
let asset_class_prefix = self.value[1];
let asset_class = AssetClass::new(asset_class_suffix, asset_class_prefix);
AssetId::new(asset_class, self.faucet_id, AssetComposition::None)
.expect("non-fungible composition is always valid")
}
pub fn faucet_id(&self) -> AccountId {
self.faucet_id
}
pub fn to_id_word(&self) -> Word {
self.id().to_word()
}
pub fn to_value_word(&self) -> Word {
self.value
}
}
impl fmt::Display for NonFungibleAsset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}
impl From<NonFungibleAsset> for Asset {
fn from(asset: NonFungibleAsset) -> Self {
Asset::NonFungible(asset)
}
}
impl Serializable for NonFungibleAsset {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
target.write(AssetComposition::None);
target.write(self.faucet_id());
target.write(self.value);
}
fn get_size_hint(&self) -> usize {
AssetComposition::SERIALIZED_SIZE
+ self.faucet_id.get_size_hint()
+ self.value.get_size_hint()
}
}
impl Deserializable for NonFungibleAsset {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let composition: AssetComposition = source.read()?;
if !composition.is_none() {
return Err(DeserializationError::InvalidValue(format!(
"expected non-fungible asset composition but found {composition:?}"
)));
}
NonFungibleAsset::deserialize_body(source)
}
}
impl NonFungibleAsset {
pub(super) fn deserialize_body<R: ByteReader>(
source: &mut R,
) -> Result<Self, DeserializationError> {
let faucet_id: AccountId = source.read()?;
let value: Word = source.read()?;
Ok(NonFungibleAsset::from_parts(faucet_id, value))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NonFungibleAssetDetails {
faucet_id: AccountId,
asset_data: Vec<u8>,
}
impl NonFungibleAssetDetails {
pub fn new(faucet_id: AccountId, asset_data: Vec<u8>) -> Self {
Self { faucet_id, asset_data }
}
pub fn faucet_id(&self) -> AccountId {
self.faucet_id
}
pub fn asset_data(&self) -> &[u8] {
&self.asset_data
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use super::*;
use crate::Felt;
use crate::account::AccountId;
use crate::asset::FungibleAsset;
use crate::testing::account_id::{
ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
};
#[test]
fn non_fungible_asset_from_id_and_value_words_fails_on_invalid_composition()
-> anyhow::Result<()> {
let asset = FungibleAsset::mock(20);
let err =
NonFungibleAsset::from_id_and_value_words(asset.to_id_word(), asset.to_value_word())
.unwrap_err();
assert_matches!(err, AssetError::AssetCompositionMismatch {
faucet_id: _, expected, actual,
} => {
assert_eq!(actual, AssetComposition::Fungible);
assert_eq!(expected, AssetComposition::None);
});
Ok(())
}
#[test]
fn non_fungible_asset_from_id_and_value_fails_on_invalid_asset_class() -> anyhow::Result<()> {
let invalid_id = AssetId::new(
AssetClass::new(Felt::from(1u32), Felt::from(2u32)),
ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET.try_into()?,
AssetComposition::None,
)?;
let err = NonFungibleAsset::from_id_and_value(invalid_id, Word::from([4, 5, 6, 7u32]))
.unwrap_err();
assert_matches!(err, AssetError::NonFungibleAssetClassMustMatchValue { .. });
Ok(())
}
#[test]
fn test_non_fungible_asset_serde() -> anyhow::Result<()> {
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 = NonFungibleAsset::new(&details);
assert_eq!(
non_fungible_asset,
NonFungibleAsset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap()
);
assert_eq!(non_fungible_asset.to_bytes().len(), non_fungible_asset.get_size_hint());
assert_eq!(
non_fungible_asset,
NonFungibleAsset::from_id_and_value_words(
non_fungible_asset.to_id_word(),
non_fungible_asset.to_value_word()
)?
)
}
let fungible_asset = FungibleAsset::mock(42);
let err = NonFungibleAsset::read_from_bytes(&fungible_asset.to_bytes()).unwrap_err();
assert_matches!(err, DeserializationError::InvalidValue(msg) => {
assert!(msg.contains("expected non-fungible asset composition but found Fungible"));
});
Ok(())
}
#[test]
fn test_asset_id_for_non_fungible_asset() {
let asset = NonFungibleAsset::mock(&[42]);
assert_eq!(asset.id().faucet_id(), NonFungibleAsset::mock_issuer());
assert_eq!(asset.id().asset_class().suffix(), asset.to_value_word()[0]);
assert_eq!(asset.id().asset_class().prefix(), asset.to_value_word()[1]);
}
}