use alloc::string::ToString;
use alloc::vec::Vec;
use core::fmt;
use super::vault::AssetVaultKey;
use super::{AccountType, Asset, AssetCallbackFlag, AssetError, Word};
use crate::Hasher;
use crate::account::AccountId;
use crate::asset::vault::AssetId;
use crate::utils::serde::{
ByteReader,
ByteWriter,
Deserializable,
DeserializationError,
Serializable,
};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct NonFungibleAsset {
faucet_id: AccountId,
value: Word,
callbacks: AssetCallbackFlag,
}
impl NonFungibleAsset {
pub const SERIALIZED_SIZE: usize =
AccountId::SERIALIZED_SIZE + Word::SERIALIZED_SIZE + AssetCallbackFlag::SERIALIZED_SIZE;
pub fn new(details: &NonFungibleAssetDetails) -> Result<Self, AssetError> {
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) -> Result<Self, AssetError> {
if !matches!(faucet_id.account_type(), AccountType::NonFungibleFaucet) {
return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id));
}
Ok(Self {
faucet_id,
value,
callbacks: AssetCallbackFlag::default(),
})
}
pub fn from_key_value(key: AssetVaultKey, value: Word) -> Result<Self, AssetError> {
if key.asset_id().suffix() != value[0] || key.asset_id().prefix() != value[1] {
return Err(AssetError::NonFungibleAssetIdMustMatchValue {
asset_id: key.asset_id(),
value,
});
}
let mut asset = Self::from_parts(key.faucet_id(), value)?;
asset.callbacks = key.callback_flag();
Ok(asset)
}
pub fn from_key_value_words(key: Word, value: Word) -> Result<Self, AssetError> {
let vault_key = AssetVaultKey::try_from(key)?;
Self::from_key_value(vault_key, value)
}
pub fn with_callbacks(mut self, callbacks: AssetCallbackFlag) -> Self {
self.callbacks = callbacks;
self
}
pub fn vault_key(&self) -> AssetVaultKey {
let asset_id_suffix = self.value[0];
let asset_id_prefix = self.value[1];
let asset_id = AssetId::new(asset_id_suffix, asset_id_prefix);
AssetVaultKey::new(asset_id, self.faucet_id, self.callbacks)
.expect("constructors should ensure account ID is of type non-fungible faucet")
}
pub fn faucet_id(&self) -> AccountId {
self.faucet_id
}
pub fn callbacks(&self) -> AssetCallbackFlag {
self.callbacks
}
pub fn to_key_word(&self) -> Word {
self.vault_key().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(self.faucet_id());
target.write(self.value);
target.write(self.callbacks);
}
fn get_size_hint(&self) -> usize {
self.faucet_id.get_size_hint() + self.value.get_size_hint() + self.callbacks.get_size_hint()
}
}
impl Deserializable for NonFungibleAsset {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let faucet_id: AccountId = source.read()?;
Self::deserialize_with_faucet_id(faucet_id, source)
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))
}
}
impl NonFungibleAsset {
pub(super) fn deserialize_with_faucet_id<R: ByteReader>(
faucet_id: AccountId,
source: &mut R,
) -> Result<Self, DeserializationError> {
let value: Word = source.read()?;
let callbacks: AssetCallbackFlag = source.read()?;
NonFungibleAsset::from_parts(faucet_id, value)
.map(|asset| asset.with_callbacks(callbacks))
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))
}
}
#[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>) -> Result<Self, AssetError> {
if !matches!(faucet_id.account_type(), AccountType::NonFungibleFaucet) {
return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id));
}
Ok(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::testing::account_id::{
ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
};
#[test]
fn fungible_asset_from_key_value_fails_on_invalid_asset_id() -> anyhow::Result<()> {
let invalid_key = AssetVaultKey::new_native(
AssetId::new(Felt::from(1u32), Felt::from(2u32)),
ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET.try_into()?,
)?;
let err =
NonFungibleAsset::from_key_value(invalid_key, Word::from([4, 5, 6, 7u32])).unwrap_err();
assert_matches!(err, AssetError::NonFungibleAssetIdMustMatchValue { .. });
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]).unwrap();
let non_fungible_asset = NonFungibleAsset::new(&details).unwrap();
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_key_value_words(
non_fungible_asset.to_key_word(),
non_fungible_asset.to_value_word()
)?
)
}
let account = AccountId::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET).unwrap();
let details = NonFungibleAssetDetails::new(account, vec![4, 5, 6, 7]).unwrap();
let asset = NonFungibleAsset::new(&details).unwrap();
let mut asset_bytes = asset.to_bytes();
let fungible_faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
asset_bytes[0..AccountId::SERIALIZED_SIZE].copy_from_slice(&fungible_faucet_id.to_bytes());
let err = NonFungibleAsset::read_from_bytes(&asset_bytes).unwrap_err();
assert_matches!(err, DeserializationError::InvalidValue(msg) if msg.contains("must be of type NonFungibleFaucet"));
Ok(())
}
#[test]
fn test_vault_key_for_non_fungible_asset() {
let asset = NonFungibleAsset::mock(&[42]);
assert_eq!(asset.vault_key().faucet_id(), NonFungibleAsset::mock_issuer());
assert_eq!(asset.vault_key().asset_id().suffix(), asset.to_value_word()[0]);
assert_eq!(asset.vault_key().asset_id().prefix(), asset.to_value_word()[1]);
}
}