use alloc::{
collections::{btree_map::Entry, BTreeMap},
string::ToString,
vec::Vec,
};
use super::{
AccountDeltaError, ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
};
use crate::{
accounts::{AccountId, AccountType},
assets::{Asset, FungibleAsset, NonFungibleAsset},
};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct AccountVaultDelta {
fungible: FungibleAssetDelta,
non_fungible: NonFungibleAssetDelta,
}
impl AccountVaultDelta {
pub const fn new(fungible: FungibleAssetDelta, non_fungible: NonFungibleAssetDelta) -> Self {
Self { fungible, non_fungible }
}
pub fn fungible(&self) -> &FungibleAssetDelta {
&self.fungible
}
pub fn non_fungible(&self) -> &NonFungibleAssetDelta {
&self.non_fungible
}
pub fn is_empty(&self) -> bool {
self.fungible.is_empty() && self.non_fungible.is_empty()
}
pub fn add_asset(&mut self, asset: Asset) -> Result<(), AccountDeltaError> {
match asset {
Asset::Fungible(asset) => self.fungible.add(asset),
Asset::NonFungible(asset) => self.non_fungible.add(asset),
}
}
pub fn remove_asset(&mut self, asset: Asset) -> Result<(), AccountDeltaError> {
match asset {
Asset::Fungible(asset) => self.fungible.remove(asset),
Asset::NonFungible(asset) => self.non_fungible.remove(asset),
}
}
pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
self.non_fungible.merge(other.non_fungible)?;
self.fungible.merge(other.fungible)
}
}
#[cfg(any(feature = "testing", test))]
impl AccountVaultDelta {
pub fn from_iters(
added_assets: impl IntoIterator<Item = crate::assets::Asset>,
removed_assets: impl IntoIterator<Item = crate::assets::Asset>,
) -> Self {
use crate::assets::Asset;
let mut fungible = FungibleAssetDelta::default();
let mut non_fungible = NonFungibleAssetDelta::default();
for asset in added_assets {
match asset {
Asset::Fungible(asset) => {
fungible.add(asset).unwrap();
},
Asset::NonFungible(asset) => {
non_fungible.add(asset).unwrap();
},
}
}
for asset in removed_assets {
match asset {
Asset::Fungible(asset) => {
fungible.remove(asset).unwrap();
},
Asset::NonFungible(asset) => {
non_fungible.remove(asset).unwrap();
},
}
}
Self { fungible, non_fungible }
}
pub fn added_assets(&self) -> impl Iterator<Item = crate::assets::Asset> + '_ {
use crate::assets::{Asset, FungibleAsset, NonFungibleAsset};
self.fungible
.0
.iter()
.filter(|&(_, &value)| value >= 0)
.map(|(&faucet_id, &diff)| {
Asset::Fungible(FungibleAsset::new(faucet_id, diff.unsigned_abs()).unwrap())
})
.chain(self.non_fungible.filter_by_action(NonFungibleDeltaAction::Add).map(|key| {
Asset::NonFungible(unsafe { NonFungibleAsset::new_unchecked(key.into()) })
}))
}
pub fn removed_assets(&self) -> impl Iterator<Item = crate::assets::Asset> + '_ {
use crate::assets::{Asset, FungibleAsset, NonFungibleAsset};
self.fungible
.0
.iter()
.filter(|&(_, &value)| value < 0)
.map(|(&faucet_id, &diff)| {
Asset::Fungible(FungibleAsset::new(faucet_id, diff.unsigned_abs()).unwrap())
})
.chain(self.non_fungible.filter_by_action(NonFungibleDeltaAction::Remove).map(|key| {
Asset::NonFungible(unsafe { NonFungibleAsset::new_unchecked(key.into()) })
}))
}
}
impl Serializable for AccountVaultDelta {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
target.write(&self.fungible);
target.write(&self.non_fungible);
}
}
impl Deserializable for AccountVaultDelta {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let fungible = source.read()?;
let non_fungible = source.read()?;
Ok(Self::new(fungible, non_fungible))
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct FungibleAssetDelta(BTreeMap<AccountId, i64>);
impl FungibleAssetDelta {
pub fn new(map: BTreeMap<AccountId, i64>) -> Result<Self, AccountDeltaError> {
let delta = Self(map);
delta.validate()?;
Ok(delta)
}
pub fn add(&mut self, asset: FungibleAsset) -> Result<(), AccountDeltaError> {
let amount: i64 = asset.amount().try_into().expect("Amount it too high");
self.add_delta(asset.faucet_id(), amount)
}
pub fn remove(&mut self, asset: FungibleAsset) -> Result<(), AccountDeltaError> {
let amount: i64 = asset.amount().try_into().expect("Amount it too high");
self.add_delta(asset.faucet_id(), -amount)
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&AccountId, &i64)> {
self.0.iter()
}
pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
for (&faucet_id, &amount) in other.0.iter() {
self.add_delta(faucet_id, amount)?;
}
Ok(())
}
fn add_delta(&mut self, faucet_id: AccountId, delta: i64) -> Result<(), AccountDeltaError> {
match self.0.entry(faucet_id) {
Entry::Vacant(entry) => {
entry.insert(delta);
},
Entry::Occupied(mut entry) => {
let old = *entry.get();
let new = old.checked_add(delta).ok_or(
AccountDeltaError::FungibleAssetDeltaOverflow {
faucet_id,
this: old,
other: delta,
},
)?;
if new == 0 {
entry.remove();
} else {
*entry.get_mut() = new;
}
},
}
Ok(())
}
fn validate(&self) -> Result<(), AccountDeltaError> {
for faucet_id in self.0.keys() {
if !matches!(faucet_id.account_type(), AccountType::FungibleFaucet) {
return Err(AccountDeltaError::NotAFungibleFaucetId(*faucet_id));
}
}
Ok(())
}
}
impl Serializable for FungibleAssetDelta {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
target.write_usize(self.0.len());
target.write_many(self.0.iter().map(|(&faucet_id, &delta)| (faucet_id, delta as u64)));
}
}
impl Deserializable for FungibleAssetDelta {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let num_fungible_assets = source.read_usize()?;
let map = source
.read_many::<(AccountId, u64)>(num_fungible_assets)?
.into_iter()
.map(|(account_id, delta_as_u64)| (account_id, delta_as_u64 as i64))
.collect();
Self::new(map).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct NonFungibleAssetDelta(BTreeMap<NonFungibleAsset, NonFungibleDeltaAction>);
impl NonFungibleAssetDelta {
pub const fn new(map: BTreeMap<NonFungibleAsset, NonFungibleDeltaAction>) -> Self {
Self(map)
}
pub fn add(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> {
self.apply_action(asset, NonFungibleDeltaAction::Add)
}
pub fn remove(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> {
self.apply_action(asset, NonFungibleDeltaAction::Remove)
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&NonFungibleAsset, &NonFungibleDeltaAction)> {
self.0.iter()
}
pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
for (&key, &action) in other.0.iter() {
self.apply_action(key, action)?;
}
Ok(())
}
fn apply_action(
&mut self,
asset: NonFungibleAsset,
action: NonFungibleDeltaAction,
) -> Result<(), AccountDeltaError> {
match self.0.entry(asset) {
Entry::Vacant(entry) => {
entry.insert(action);
},
Entry::Occupied(entry) => {
let previous = *entry.get();
if previous == action {
return Err(AccountDeltaError::DuplicateNonFungibleVaultUpdate(asset));
}
entry.remove();
},
}
Ok(())
}
fn filter_by_action(
&self,
action: NonFungibleDeltaAction,
) -> impl Iterator<Item = NonFungibleAsset> + '_ {
self.0
.iter()
.filter(move |&(_, cur_action)| cur_action == &action)
.map(|(key, _)| *key)
}
}
impl Serializable for NonFungibleAssetDelta {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
let added: Vec<_> = self.filter_by_action(NonFungibleDeltaAction::Add).collect();
let removed: Vec<_> = self.filter_by_action(NonFungibleDeltaAction::Remove).collect();
target.write_usize(added.len());
target.write_many(added.iter());
target.write_usize(removed.len());
target.write_many(removed.iter());
}
}
impl Deserializable for NonFungibleAssetDelta {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let mut map = BTreeMap::new();
let num_added = source.read_usize()?;
for _ in 0..num_added {
let added_asset = source.read()?;
map.insert(added_asset, NonFungibleDeltaAction::Add);
}
let num_removed = source.read_usize()?;
for _ in 0..num_removed {
let removed_asset = source.read()?;
map.insert(removed_asset, NonFungibleDeltaAction::Remove);
}
Ok(Self::new(map))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NonFungibleDeltaAction {
Add,
Remove,
}
#[cfg(test)]
mod tests {
use super::{AccountVaultDelta, Deserializable, Serializable};
use crate::{
accounts::{
account_id::testing::{
ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN,
ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN,
},
AccountId,
},
assets::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails},
testing::storage::build_assets,
};
#[test]
fn test_serde_account_vault() {
let (asset_0, asset_1) = build_assets();
let delta = AccountVaultDelta::from_iters([asset_0], [asset_1]);
let serialized = delta.to_bytes();
let deserialized = AccountVaultDelta::read_from_bytes(&serialized).unwrap();
assert_eq!(deserialized, delta);
}
#[test]
fn test_is_empty_account_vault() {
let faucet = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN).unwrap();
let asset: Asset = FungibleAsset::new(faucet, 123).unwrap().into();
assert!(AccountVaultDelta::default().is_empty());
assert!(!AccountVaultDelta::from_iters([asset], []).is_empty());
assert!(!AccountVaultDelta::from_iters([], [asset]).is_empty());
}
#[rstest::rstest]
#[case::pos_pos(50, 50, Some(100))]
#[case::neg_neg(-50, -50, Some(-100))]
#[case::empty_pos(0, 50, Some(50))]
#[case::empty_neg(0, -50, Some(-50))]
#[case::nullify_pos_neg(100, -100, Some(0))]
#[case::nullify_neg_pos(-100, 100, Some(0))]
#[case::overflow(FungibleAsset::MAX_AMOUNT as i64, FungibleAsset::MAX_AMOUNT as i64, None)]
#[case::underflow(-(FungibleAsset::MAX_AMOUNT as i64), -(FungibleAsset::MAX_AMOUNT as i64), None)]
#[test]
fn merge_fungible_aggregation(#[case] x: i64, #[case] y: i64, #[case] expected: Option<i64>) {
fn create_delta_with_fungible(account_id: AccountId, amount: i64) -> AccountVaultDelta {
let asset = FungibleAsset::new(account_id, amount.unsigned_abs()).unwrap().into();
match amount {
0 => AccountVaultDelta::default(),
x if x.is_positive() => AccountVaultDelta::from_iters([asset], []),
_ => AccountVaultDelta::from_iters([], [asset]),
}
}
let account_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN).unwrap();
let mut delta_x = create_delta_with_fungible(account_id, x);
let delta_y = create_delta_with_fungible(account_id, y);
let result = delta_x.merge(delta_y);
if let Some(expected) = expected {
let expected = create_delta_with_fungible(account_id, expected);
assert_eq!(result.map(|_| delta_x).unwrap(), expected);
} else {
assert!(result.is_err());
}
}
#[rstest::rstest]
#[case::empty_removed(None, Some(false), Ok(Some(false)))]
#[case::empty_added(None, Some(true), Ok(Some(true)))]
#[case::add_remove(Some(true), Some(false), Ok(None))]
#[case::remove_add(Some(false), Some(true), Ok(None))]
#[case::double_add(Some(true), Some(true), Err(()))]
#[case::double_remove(Some(false), Some(false), Err(()))]
#[test]
fn merge_non_fungible_aggregation(
#[case] x: Option<bool>,
#[case] y: Option<bool>,
#[case] expected: Result<Option<bool>, ()>,
) {
fn create_delta_with_non_fungible(
account_id: AccountId,
added: Option<bool>,
) -> AccountVaultDelta {
let asset: Asset = NonFungibleAsset::new(
&NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]).unwrap(),
)
.unwrap()
.into();
match added {
Some(true) => AccountVaultDelta::from_iters([asset], []),
Some(false) => AccountVaultDelta::from_iters([], [asset]),
None => AccountVaultDelta::default(),
}
}
let account_id = AccountId::try_from(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN).unwrap();
let mut delta_x = create_delta_with_non_fungible(account_id, x);
let delta_y = create_delta_with_non_fungible(account_id, y);
let result = delta_x.merge(delta_y);
if let Ok(expected) = expected {
let expected = create_delta_with_non_fungible(account_id, expected);
assert_eq!(result.map(|_| delta_x).unwrap(), expected);
} else {
assert!(result.is_err());
}
}
}