use alloc::collections::BTreeMap;
use alloc::collections::btree_map::Entry;
use alloc::string::ToString;
use alloc::vec::Vec;
use super::{
AccountDeltaError,
ByteReader,
ByteWriter,
Deserializable,
DeserializationError,
Serializable,
};
use crate::account::delta::AssetDeltaOperation;
use crate::asset::{Asset, AssetId};
use crate::{Felt, Word};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AssetDelta {
delta_op: AssetDeltaOperation,
asset: Asset,
}
impl AssetDelta {
pub fn new(delta_op: AssetDeltaOperation, asset: Asset) -> Self {
Self { delta_op, asset }
}
pub fn delta_op(&self) -> AssetDeltaOperation {
self.delta_op
}
pub fn asset(&self) -> Asset {
self.asset
}
pub fn asset_id(&self) -> AssetId {
self.asset.id()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct AccountVaultDelta {
delta: BTreeMap<AssetId, AssetDelta>,
}
impl AccountVaultDelta {
pub(in crate::account) const DOMAIN: Felt = Felt::new_unchecked(1);
pub const MAX_ASSETS_PER_DELTA_OP: u16 = 1024;
pub fn new(
asset_deltas: impl IntoIterator<Item = AssetDelta>,
) -> Result<Self, AccountDeltaError> {
let mut delta = BTreeMap::new();
let mut num_added_assets = 0usize;
let mut num_removed_assets = 0usize;
for asset_delta in asset_deltas {
match asset_delta.delta_op() {
AssetDeltaOperation::Add => num_added_assets += 1,
AssetDeltaOperation::Remove => num_removed_assets += 1,
}
match delta.entry(asset_delta.asset_id()) {
Entry::Vacant(entry) => {
entry.insert(asset_delta);
},
Entry::Occupied(_) => {
return Err(AccountDeltaError::DuplicateAssetDelta(asset_delta.asset_id()));
},
}
}
Self::validate_asset_count(AssetDeltaOperation::Add, num_added_assets)?;
Self::validate_asset_count(AssetDeltaOperation::Remove, num_removed_assets)?;
Ok(Self { delta })
}
pub fn is_empty(&self) -> bool {
self.delta.is_empty()
}
pub fn num_assets(&self) -> usize {
self.delta.len()
}
pub fn iter(&self) -> impl Iterator<Item = &AssetDelta> {
self.delta.values()
}
pub fn added_assets(&self) -> impl Iterator<Item = Asset> + '_ {
self.filter_by_op(AssetDeltaOperation::Add)
}
pub fn removed_assets(&self) -> impl Iterator<Item = Asset> + '_ {
self.filter_by_op(AssetDeltaOperation::Remove)
}
pub(super) fn append_delta_elements(&self, elements: &mut Vec<Felt>) {
self.append_asset_section(AssetDeltaOperation::Add, elements);
self.append_asset_section(AssetDeltaOperation::Remove, elements);
}
fn num_assets_by_op(&self, delta_op: AssetDeltaOperation) -> u16 {
let num_assets = self.filter_by_op(delta_op).count();
u16::try_from(num_assets).expect("number of changed assets is validated on construction")
}
fn num_added_assets(&self) -> u16 {
self.num_assets_by_op(AssetDeltaOperation::Add)
}
fn num_removed_assets(&self) -> u16 {
self.num_assets_by_op(AssetDeltaOperation::Remove)
}
fn validate_asset_count(
delta_op: AssetDeltaOperation,
num_ops: usize,
) -> Result<(), AccountDeltaError> {
if num_ops > usize::from(Self::MAX_ASSETS_PER_DELTA_OP) {
return Err(AccountDeltaError::TooManyVaultAssetDeltas { delta_op, num_ops });
}
Ok(())
}
fn filter_by_op(&self, delta_op: AssetDeltaOperation) -> impl Iterator<Item = Asset> + '_ {
self.delta
.values()
.filter(move |asset_delta| asset_delta.delta_op() == delta_op)
.map(AssetDelta::asset)
}
fn append_asset_section(&self, delta_op: AssetDeltaOperation, elements: &mut Vec<Felt>) {
let mut num_changed_assets = 0;
for asset in self.filter_by_op(delta_op) {
elements.extend_from_slice(&asset.as_elements());
num_changed_assets += 1;
}
if num_changed_assets != 0 {
let num_changed_assets = Felt::try_from(num_changed_assets as u64)
.expect("number of changed assets should not exceed max representable felt");
elements.extend_from_slice(&[
Self::DOMAIN,
Felt::from(delta_op.as_u8()),
num_changed_assets,
Felt::ZERO,
]);
elements.extend_from_slice(Word::empty().as_elements());
}
}
}
impl Serializable for AccountVaultDelta {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
target.write(self.num_added_assets());
target.write_many(self.added_assets());
target.write(self.num_removed_assets());
target.write_many(self.removed_assets());
}
fn get_size_hint(&self) -> usize {
let added_size: usize = self.added_assets().map(|asset| asset.get_size_hint()).sum();
let removed_size: usize = self.removed_assets().map(|asset| asset.get_size_hint()).sum();
2 * 0u16.get_size_hint() + added_size + removed_size
}
}
impl Deserializable for AccountVaultDelta {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let num_added_assets: u16 = source.read()?;
if num_added_assets > Self::MAX_ASSETS_PER_DELTA_OP {
return Err(DeserializationError::InvalidValue(
AccountDeltaError::TooManyVaultAssetDeltas {
delta_op: AssetDeltaOperation::Add,
num_ops: usize::from(num_added_assets),
}
.to_string(),
));
}
let mut asset_deltas = Vec::new();
for asset in source.read_many_iter::<Asset>(usize::from(num_added_assets))? {
asset_deltas.push(AssetDelta::new(AssetDeltaOperation::Add, asset?));
}
let num_removed_assets: u16 = source.read()?;
if num_removed_assets > Self::MAX_ASSETS_PER_DELTA_OP {
return Err(DeserializationError::InvalidValue(
AccountDeltaError::TooManyVaultAssetDeltas {
delta_op: AssetDeltaOperation::Remove,
num_ops: usize::from(num_removed_assets),
}
.to_string(),
));
}
for asset in source.read_many_iter::<Asset>(usize::from(num_removed_assets))? {
asset_deltas.push(AssetDelta::new(AssetDeltaOperation::Remove, asset?));
}
Self::new(asset_deltas).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
}
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use alloc::vec::Vec;
use assert_matches::assert_matches;
use rstest::rstest;
use super::{AccountVaultDelta, Deserializable, DeserializationError, Serializable};
use crate::account::delta::AssetDeltaOperation;
use crate::account::{AccountId, AssetDelta};
use crate::asset::{Asset, FungibleAsset, NonFungibleAsset};
use crate::errors::AccountDeltaError;
use crate::utils::serde::ByteWriter;
#[test]
fn account_vault_delta_serde() -> anyhow::Result<()> {
let empty_delta = AccountVaultDelta::default();
assert!(empty_delta.is_empty());
let serialized = empty_delta.to_bytes();
assert_eq!(AccountVaultDelta::read_from_bytes(&serialized)?, empty_delta);
assert_eq!(empty_delta.get_size_hint(), serialized.len());
let delta = AccountVaultDelta::from_iters(
[FungibleAsset::mock(100), NonFungibleAsset::mock(&[10, 21, 32, 43])],
[NonFungibleAsset::mock(&[54, 65])],
);
assert!(!delta.is_empty());
let serialized = delta.to_bytes();
assert_eq!(AccountVaultDelta::read_from_bytes(&serialized)?, delta);
assert_eq!(delta.get_size_hint(), serialized.len());
Ok(())
}
fn generate_asset_deltas(delta_op: AssetDeltaOperation, num_deltas: usize) -> Vec<AssetDelta> {
(0..num_deltas)
.map(|_| {
let asset =
FungibleAsset::new(AccountId::builder().build_with_seed(rand::random()), 42)
.unwrap();
AssetDelta::new(delta_op, Asset::from(asset))
})
.collect::<Vec<_>>()
}
#[rstest]
#[case::add(AssetDeltaOperation::Add)]
#[case::remove(AssetDeltaOperation::Remove)]
fn account_vault_delta_accepts_max_num_changed_assets(
#[case] expected_delta_op: AssetDeltaOperation,
) -> anyhow::Result<()> {
let asset_deltas = generate_asset_deltas(
expected_delta_op,
usize::from(AccountVaultDelta::MAX_ASSETS_PER_DELTA_OP),
);
AccountVaultDelta::new(asset_deltas)?;
Ok(())
}
#[rstest]
#[case::add(AssetDeltaOperation::Add)]
#[case::remove(AssetDeltaOperation::Remove)]
fn account_vault_delta_rejects_more_than_max_num_changed_assets(
#[case] expected_delta_op: AssetDeltaOperation,
) -> anyhow::Result<()> {
let expected_num_ops = usize::from(AccountVaultDelta::MAX_ASSETS_PER_DELTA_OP) + 1;
let asset_deltas = generate_asset_deltas(expected_delta_op, expected_num_ops);
let err = AccountVaultDelta::new(asset_deltas).unwrap_err();
assert_matches!(err, AccountDeltaError::TooManyVaultAssetDeltas { delta_op, num_ops } => {
assert_eq!(delta_op, expected_delta_op);
assert_eq!(num_ops, expected_num_ops);
});
Ok(())
}
#[test]
fn account_vault_delta_rejects_duplicate_asset() -> anyhow::Result<()> {
let asset = NonFungibleAsset::mock(&[10, 21, 32, 43]);
let asset_deltas = [
AssetDelta::new(AssetDeltaOperation::Add, asset),
AssetDelta::new(AssetDeltaOperation::Remove, asset),
];
let err = AccountVaultDelta::new(asset_deltas).unwrap_err();
assert_matches!(err, AccountDeltaError::DuplicateAssetDelta(asset_id) => {
assert_eq!(asset_id, asset.id());
});
Ok(())
}
#[test]
fn account_vault_delta_deserialization_rejects_duplicate_asset() -> anyhow::Result<()> {
let asset = NonFungibleAsset::mock(&[10, 21, 32, 43]);
let mut bytes = Vec::new();
bytes.write(1u16);
bytes.write(asset);
bytes.write(1u16);
bytes.write(asset);
let error = AccountVaultDelta::read_from_bytes(&bytes)
.expect_err("delta with a duplicate asset should not deserialize");
let expected = AccountDeltaError::DuplicateAssetDelta(asset.id()).to_string();
assert_matches!(error, DeserializationError::InvalidValue(message) if message == expected);
Ok(())
}
}