miden_objects/asset/
fungible.rsuse alloc::{boxed::Box, string::ToString};
use core::fmt;
use vm_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable};
use vm_processor::DeserializationError;
use super::{is_not_a_non_fungible_asset, AccountType, Asset, AssetError, Felt, Word, ZERO};
use crate::account::{AccountId, AccountIdPrefix};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct FungibleAsset {
faucet_id: AccountId,
amount: u64,
}
impl FungibleAsset {
pub const MAX_AMOUNT: u64 = (1_u64 << 63) - 1;
pub const SERIALIZED_SIZE: usize = AccountId::SERIALIZED_SIZE + core::mem::size_of::<u64>();
pub const fn new(faucet_id: AccountId, amount: u64) -> Result<Self, AssetError> {
let asset = Self { faucet_id, amount };
asset.validate()
}
pub(crate) fn new_unchecked(value: Word) -> FungibleAsset {
FungibleAsset {
faucet_id: AccountId::new_unchecked([value[3], value[2]]),
amount: value[0].as_int(),
}
}
pub fn faucet_id(&self) -> AccountId {
self.faucet_id
}
pub fn faucet_id_prefix(&self) -> AccountIdPrefix {
self.faucet_id.prefix()
}
pub fn amount(&self) -> u64 {
self.amount
}
pub fn is_from_same_faucet(&self, other: &Self) -> bool {
self.faucet_id == other.faucet_id
}
pub fn vault_key(&self) -> Word {
Self::vault_key_from_faucet(self.faucet_id)
}
#[allow(clippy::should_implement_trait)]
pub fn add(self, other: Self) -> Result<Self, AssetError> {
if self.faucet_id != other.faucet_id {
return Err(AssetError::FungibleAssetInconsistentFaucetIds {
original_issuer: self.faucet_id,
other_issuer: other.faucet_id,
});
}
let amount = self
.amount
.checked_add(other.amount)
.expect("even MAX_AMOUNT + MAX_AMOUNT should not overflow u64");
if amount > Self::MAX_AMOUNT {
return Err(AssetError::FungibleAssetAmountTooBig(amount));
}
Ok(Self { faucet_id: self.faucet_id, amount })
}
pub fn sub(&mut self, amount: u64) -> Result<Self, AssetError> {
self.amount = self.amount.checked_sub(amount).ok_or(
AssetError::FungibleAssetAmountNotSufficient {
minuend: self.amount,
subtrahend: amount,
},
)?;
Ok(FungibleAsset { faucet_id: self.faucet_id, amount })
}
const fn validate(self) -> Result<Self, AssetError> {
let account_type = self.faucet_id.account_type();
if !matches!(account_type, AccountType::FungibleFaucet) {
return Err(AssetError::FungibleFaucetIdTypeMismatch(self.faucet_id));
}
if self.amount > Self::MAX_AMOUNT {
return Err(AssetError::FungibleAssetAmountTooBig(self.amount));
}
Ok(self)
}
pub(super) fn vault_key_from_faucet(faucet_id: AccountId) -> Word {
let mut key = Word::default();
key[2] = faucet_id.suffix();
key[3] = faucet_id.prefix().as_felt();
key
}
}
impl From<FungibleAsset> for Word {
fn from(asset: FungibleAsset) -> Self {
let mut result = Word::default();
result[0] = Felt::new(asset.amount);
result[2] = asset.faucet_id.suffix();
result[3] = asset.faucet_id.prefix().as_felt();
debug_assert!(is_not_a_non_fungible_asset(result));
result
}
}
impl From<FungibleAsset> for Asset {
fn from(asset: FungibleAsset) -> Self {
Asset::Fungible(asset)
}
}
impl TryFrom<Word> for FungibleAsset {
type Error = AssetError;
fn try_from(value: Word) -> Result<Self, Self::Error> {
if value[1] != ZERO {
return Err(AssetError::FungibleAssetExpectedZero(value));
}
let faucet_id = AccountId::try_from([value[3], value[2]])
.map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?;
let amount = value[0].as_int();
Self::new(faucet_id, amount)
}
}
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(self.faucet_id);
target.write(self.amount);
}
fn get_size_hint(&self) -> usize {
self.faucet_id.get_size_hint() + self.amount.get_size_hint()
}
}
impl Deserializable for FungibleAsset {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let faucet_id_prefix: AccountIdPrefix = source.read()?;
FungibleAsset::deserialize_with_faucet_id_prefix(faucet_id_prefix, source)
}
}
impl FungibleAsset {
pub(super) fn deserialize_with_faucet_id_prefix<R: ByteReader>(
faucet_id_prefix: AccountIdPrefix,
source: &mut R,
) -> Result<Self, DeserializationError> {
let suffix_bytes: [u8; 7] = source.read()?;
let prefix_bytes: [u8; 8] = faucet_id_prefix.into();
let mut id_bytes: [u8; 15] = [0; 15];
id_bytes[..8].copy_from_slice(&prefix_bytes);
id_bytes[8..].copy_from_slice(&suffix_bytes);
let faucet_id = AccountId::try_from(id_bytes)
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
let amount: u64 = source.read()?;
FungibleAsset::new(faucet_id, amount)
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
account::AccountId,
testing::account_id::{
ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN,
ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2,
ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN,
},
};
#[test]
fn test_fungible_asset_serde() {
for fungible_account_id in [
ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN,
ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN,
ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1,
ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2,
ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_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()
);
}
let account_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3).unwrap();
let asset = FungibleAsset::new(account_id, 50).unwrap();
let mut asset_bytes = asset.to_bytes();
assert_eq!(asset_bytes.len(), asset.get_size_hint());
assert_eq!(asset.get_size_hint(), FungibleAsset::SERIALIZED_SIZE);
let non_fungible_faucet_id =
AccountId::try_from(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN).unwrap();
asset_bytes[0..15].copy_from_slice(&non_fungible_faucet_id.to_bytes());
let err = FungibleAsset::read_from_bytes(&asset_bytes).unwrap_err();
assert!(matches!(err, DeserializationError::InvalidValue(_)));
}
}