use alloc::string::ToString;
use core::fmt;
use super::vault::AssetVaultKey;
use super::{Asset, AssetAmount, AssetCallbackFlag, AssetComposition, AssetError, Word};
use crate::Felt;
use crate::account::AccountId;
use crate::asset::AssetId;
use crate::utils::serde::{
ByteReader,
ByteWriter,
Deserializable,
DeserializationError,
Serializable,
};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct FungibleAsset {
faucet_id: AccountId,
amount: AssetAmount,
callbacks: AssetCallbackFlag,
}
impl FungibleAsset {
pub const MAX_AMOUNT: AssetAmount = AssetAmount::MAX;
pub const SERIALIZED_SIZE: usize = AssetComposition::SERIALIZED_SIZE
+ AccountId::SERIALIZED_SIZE
+ core::mem::size_of::<u64>()
+ AssetCallbackFlag::SERIALIZED_SIZE;
pub fn new(faucet_id: AccountId, amount: u64) -> Result<Self, AssetError> {
let amount = AssetAmount::new(amount)?;
Ok(Self {
faucet_id,
amount,
callbacks: AssetCallbackFlag::default(),
})
}
pub fn from_key_value(key: AssetVaultKey, value: Word) -> Result<Self, AssetError> {
if !key.composition().is_fungible() {
return Err(AssetError::AssetCompositionMismatch {
faucet_id: key.faucet_id(),
expected: AssetComposition::Fungible,
actual: key.composition(),
});
}
if !key.asset_id().is_empty() {
return Err(AssetError::FungibleAssetIdMustBeZero(key.asset_id()));
}
if value[1] != Felt::ZERO || value[2] != Felt::ZERO || value[3] != Felt::ZERO {
return Err(AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(value));
}
let mut asset = Self::new(key.faucet_id(), value[0].as_canonical_u64())?;
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 faucet_id(&self) -> AccountId {
self.faucet_id
}
pub fn amount(&self) -> AssetAmount {
self.amount
}
pub fn is_same(&self, other: &Self) -> bool {
self.vault_key() == other.vault_key()
}
pub fn callbacks(&self) -> AssetCallbackFlag {
self.callbacks
}
pub fn vault_key(&self) -> AssetVaultKey {
AssetVaultKey::new(
AssetId::default(),
self.faucet_id,
AssetComposition::Fungible,
self.callbacks,
)
.expect("default asset id should be valid for fungible composition")
}
pub fn to_key_word(&self) -> Word {
self.vault_key().to_word()
}
pub fn to_value_word(&self) -> Word {
Word::new([Felt::from(self.amount), Felt::ZERO, Felt::ZERO, Felt::ZERO])
}
#[allow(clippy::should_implement_trait)]
pub fn add(self, other: Self) -> Result<Self, AssetError> {
if !self.is_same(&other) {
return Err(AssetError::FungibleAssetInconsistentVaultKeys {
original_key: self.vault_key(),
other_key: other.vault_key(),
});
}
let amount = (self.amount + other.amount)?;
Ok(Self {
faucet_id: self.faucet_id,
amount,
callbacks: self.callbacks,
})
}
#[allow(clippy::should_implement_trait)]
pub fn sub(self, other: Self) -> Result<Self, AssetError> {
if !self.is_same(&other) {
return Err(AssetError::FungibleAssetInconsistentVaultKeys {
original_key: self.vault_key(),
other_key: other.vault_key(),
});
}
let amount = (self.amount - other.amount)?;
Ok(FungibleAsset {
faucet_id: self.faucet_id,
amount,
callbacks: self.callbacks,
})
}
}
impl From<FungibleAsset> for Asset {
fn from(asset: FungibleAsset) -> Self {
Asset::Fungible(asset)
}
}
impl fmt::Display for FungibleAsset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}
impl Serializable for FungibleAsset {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
target.write(AssetComposition::Fungible);
target.write(self.faucet_id);
target.write(self.amount.as_u64());
target.write(self.callbacks);
}
fn get_size_hint(&self) -> usize {
AssetComposition::SERIALIZED_SIZE
+ self.faucet_id.get_size_hint()
+ self.amount.as_u64().get_size_hint()
+ self.callbacks.get_size_hint()
}
}
impl Deserializable for FungibleAsset {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let composition: AssetComposition = source.read()?;
if !composition.is_fungible() {
return Err(DeserializationError::InvalidValue(format!(
"expected fungible asset composition but found {composition:?}"
)));
}
FungibleAsset::deserialize_body(source)
}
}
impl FungibleAsset {
pub(super) fn deserialize_body<R: ByteReader>(
source: &mut R,
) -> Result<Self, DeserializationError> {
let faucet_id: AccountId = source.read()?;
let amount: u64 = source.read()?;
let callbacks = source.read()?;
let asset = FungibleAsset::new(faucet_id, amount)
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))?
.with_callbacks(callbacks);
Ok(asset)
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use super::*;
use crate::account::AccountId;
use crate::asset::NonFungibleAsset;
use crate::asset::tests::set_asset_metadata;
use crate::testing::account_id::{
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,
};
#[test]
fn fungible_asset_from_key_value_words_fails_on_invalid_composition() -> anyhow::Result<()> {
let asset_key =
set_asset_metadata(FungibleAsset::mock(25).vault_key(), AssetComposition::None.as_u8());
let err =
FungibleAsset::from_key_value_words(asset_key, FungibleAsset::mock(5).to_value_word())
.unwrap_err();
assert_matches!(err, AssetError::AssetCompositionMismatch {
faucet_id: _, expected, actual: _
} => {
assert_eq!(expected, AssetComposition::Fungible);
});
Ok(())
}
#[test]
fn fungible_asset_from_key_value_words_fails_on_invalid_asset_id() -> anyhow::Result<()> {
let faucet_id: AccountId = ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?;
let mut asset_key = AssetVaultKey::new(
AssetId::default(),
faucet_id,
AssetComposition::Fungible,
AssetCallbackFlag::Disabled,
)?
.to_word();
asset_key[0] = Felt::from(1u32);
asset_key[1] = Felt::from(2u32);
let err =
FungibleAsset::from_key_value_words(asset_key, FungibleAsset::mock(5).to_value_word())
.unwrap_err();
assert_matches!(err, AssetError::FungibleAssetIdMustBeZero(_));
Ok(())
}
#[test]
fn fungible_asset_from_key_value_fails_on_invalid_value() -> anyhow::Result<()> {
let asset = FungibleAsset::mock(42);
let mut invalid_value = asset.to_value_word();
invalid_value[2] = Felt::from(5u32);
let err = FungibleAsset::from_key_value(asset.vault_key(), invalid_value).unwrap_err();
assert_matches!(err, AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(_));
Ok(())
}
#[test]
fn test_fungible_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 = FungibleAsset::new(account_id, 10).unwrap();
assert_eq!(
fungible_asset,
FungibleAsset::read_from_bytes(&fungible_asset.to_bytes()).unwrap()
);
assert_eq!(fungible_asset.to_bytes().len(), fungible_asset.get_size_hint());
assert_eq!(
fungible_asset,
FungibleAsset::from_key_value_words(
fungible_asset.to_key_word(),
fungible_asset.to_value_word()
)?
)
}
let non_fungible_asset = NonFungibleAsset::mock(&[4]);
let err = FungibleAsset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap_err();
assert_matches!(err, DeserializationError::InvalidValue(msg) => {
assert!(msg.contains("expected fungible asset composition but found None"));
});
Ok(())
}
#[test]
fn test_vault_key_for_fungible_asset() {
let asset = FungibleAsset::mock(34);
assert_eq!(asset.vault_key().faucet_id(), FungibleAsset::mock_issuer());
assert_eq!(asset.vault_key().asset_id().prefix().as_canonical_u64(), 0);
assert_eq!(asset.vault_key().asset_id().suffix().as_canonical_u64(), 0);
}
}