1use alloc::collections::BTreeMap;
2use alloc::collections::btree_map::Entry;
3use alloc::string::ToString;
4use alloc::vec::Vec;
5
6use super::{
7 AccountDeltaError,
8 ByteReader,
9 ByteWriter,
10 Deserializable,
11 DeserializationError,
12 Serializable,
13};
14use crate::asset::{Asset, AssetVaultKey, FungibleAsset, NonFungibleAsset};
15use crate::{Felt, ONE, ZERO};
16
17const DOMAIN_ASSET: Felt = Felt::ONE;
22
23#[derive(Clone, Debug, Default, PartialEq, Eq)]
30pub struct AccountVaultDelta {
31 fungible: FungibleAssetDelta,
32 non_fungible: NonFungibleAssetDelta,
33}
34
35impl AccountVaultDelta {
36 pub const fn new(fungible: FungibleAssetDelta, non_fungible: NonFungibleAssetDelta) -> Self {
42 Self { fungible, non_fungible }
43 }
44
45 pub fn fungible(&self) -> &FungibleAssetDelta {
47 &self.fungible
48 }
49
50 pub fn non_fungible(&self) -> &NonFungibleAssetDelta {
52 &self.non_fungible
53 }
54
55 pub fn is_empty(&self) -> bool {
57 self.fungible.is_empty() && self.non_fungible.is_empty()
58 }
59
60 pub fn add_asset(&mut self, asset: Asset) -> Result<(), AccountDeltaError> {
62 match asset {
63 Asset::Fungible(asset) => self.fungible.add(asset),
64 Asset::NonFungible(asset) => self.non_fungible.add(asset),
65 }
66 }
67
68 pub fn remove_asset(&mut self, asset: Asset) -> Result<(), AccountDeltaError> {
70 match asset {
71 Asset::Fungible(asset) => self.fungible.remove(asset),
72 Asset::NonFungible(asset) => self.non_fungible.remove(asset),
73 }
74 }
75
76 pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
83 self.non_fungible.merge(other.non_fungible)?;
84 self.fungible.merge(other.fungible)
85 }
86
87 pub(super) fn append_delta_elements(&self, elements: &mut Vec<Felt>) {
90 self.fungible().append_delta_elements(elements);
91 self.non_fungible().append_delta_elements(elements);
92 }
93}
94
95#[cfg(any(feature = "testing", test))]
96impl AccountVaultDelta {
97 pub fn from_iters(
99 added_assets: impl IntoIterator<Item = crate::asset::Asset>,
100 removed_assets: impl IntoIterator<Item = crate::asset::Asset>,
101 ) -> Self {
102 let mut fungible = FungibleAssetDelta::default();
103 let mut non_fungible = NonFungibleAssetDelta::default();
104
105 for asset in added_assets {
106 match asset {
107 Asset::Fungible(asset) => {
108 fungible.add(asset).unwrap();
109 },
110 Asset::NonFungible(asset) => {
111 non_fungible.add(asset).unwrap();
112 },
113 }
114 }
115
116 for asset in removed_assets {
117 match asset {
118 Asset::Fungible(asset) => {
119 fungible.remove(asset).unwrap();
120 },
121 Asset::NonFungible(asset) => {
122 non_fungible.remove(asset).unwrap();
123 },
124 }
125 }
126
127 Self { fungible, non_fungible }
128 }
129
130 pub fn added_assets(&self) -> impl Iterator<Item = crate::asset::Asset> + '_ {
132 self.fungible
133 .0
134 .iter()
135 .filter(|&(_, &value)| value >= 0)
136 .map(|(vault_key, &diff)| {
137 Asset::Fungible(
138 FungibleAsset::new(vault_key.faucet_id(), diff.unsigned_abs())
139 .unwrap()
140 .with_callbacks(vault_key.callback_flag()),
141 )
142 })
143 .chain(
144 self.non_fungible
145 .filter_by_action(NonFungibleDeltaAction::Add)
146 .map(Asset::NonFungible),
147 )
148 }
149
150 pub fn removed_assets(&self) -> impl Iterator<Item = crate::asset::Asset> + '_ {
152 self.fungible
153 .0
154 .iter()
155 .filter(|&(_, &value)| value < 0)
156 .map(|(vault_key, &diff)| {
157 Asset::Fungible(
158 FungibleAsset::new(vault_key.faucet_id(), diff.unsigned_abs())
159 .unwrap()
160 .with_callbacks(vault_key.callback_flag()),
161 )
162 })
163 .chain(
164 self.non_fungible
165 .filter_by_action(NonFungibleDeltaAction::Remove)
166 .map(Asset::NonFungible),
167 )
168 }
169}
170
171impl Serializable for AccountVaultDelta {
172 fn write_into<W: ByteWriter>(&self, target: &mut W) {
173 target.write(&self.fungible);
174 target.write(&self.non_fungible);
175 }
176
177 fn get_size_hint(&self) -> usize {
178 self.fungible.get_size_hint() + self.non_fungible.get_size_hint()
179 }
180}
181
182impl Deserializable for AccountVaultDelta {
183 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
184 let fungible = source.read()?;
185 let non_fungible = source.read()?;
186
187 Ok(Self::new(fungible, non_fungible))
188 }
189}
190
191#[derive(Clone, Debug, Default, PartialEq, Eq)]
199pub struct FungibleAssetDelta(BTreeMap<AssetVaultKey, i64>);
200
201impl FungibleAssetDelta {
202 pub fn new(map: BTreeMap<AssetVaultKey, i64>) -> Result<Self, AccountDeltaError> {
207 Self::validate(&map)?;
208
209 Ok(Self(map))
210 }
211
212 pub fn add(&mut self, asset: FungibleAsset) -> Result<(), AccountDeltaError> {
217 let amount: i64 = asset.amount().as_i64();
218 self.add_delta(asset.vault_key(), amount)
219 }
220
221 pub fn remove(&mut self, asset: FungibleAsset) -> Result<(), AccountDeltaError> {
226 let amount: i64 = asset.amount().as_i64();
227 self.add_delta(asset.vault_key(), -amount)
228 }
229
230 pub fn amount(&self, vault_key: &AssetVaultKey) -> Option<i64> {
232 self.0.get(vault_key).copied()
233 }
234
235 pub fn num_assets(&self) -> usize {
237 self.0.len()
238 }
239
240 pub fn is_empty(&self) -> bool {
242 self.0.is_empty()
243 }
244
245 pub fn iter(&self) -> impl Iterator<Item = (&AssetVaultKey, &i64)> {
247 self.0.iter()
248 }
249
250 pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
257 for (&vault_key, &amount) in other.0.iter() {
263 self.add_delta(vault_key, amount)?;
264 }
265
266 Ok(())
267 }
268
269 fn add_delta(&mut self, vault_key: AssetVaultKey, delta: i64) -> Result<(), AccountDeltaError> {
278 match self.0.entry(vault_key) {
279 Entry::Vacant(entry) => {
280 if delta != 0 {
282 entry.insert(delta);
283 }
284 },
285 Entry::Occupied(mut entry) => {
286 let old = *entry.get();
287 let new = old.checked_add(delta).ok_or(
288 AccountDeltaError::FungibleAssetDeltaOverflow {
289 faucet_id: vault_key.faucet_id(),
290 current: old,
291 delta,
292 },
293 )?;
294
295 if new == 0 {
296 entry.remove();
297 } else {
298 *entry.get_mut() = new;
299 }
300 },
301 }
302
303 Ok(())
304 }
305
306 fn validate(map: &BTreeMap<AssetVaultKey, i64>) -> Result<(), AccountDeltaError> {
311 for vault_key in map.keys() {
312 if !vault_key.composition().is_fungible() {
313 return Err(AccountDeltaError::NotAFungibleFaucetId(vault_key.faucet_id()));
314 }
315 }
316
317 Ok(())
318 }
319
320 pub(super) fn append_delta_elements(&self, elements: &mut Vec<Felt>) {
331 for (vault_key, amount_delta) in self.iter() {
332 debug_assert_ne!(
335 *amount_delta, 0,
336 "fungible asset iterator should never yield amount deltas of 0"
337 );
338
339 let was_added = if *amount_delta > 0 { ONE } else { ZERO };
340 let amount_delta = Felt::try_from(amount_delta.unsigned_abs())
341 .expect("amount delta should be less than i64::MAX");
342
343 let key_word = vault_key.to_word();
344 elements.extend_from_slice(&[
345 DOMAIN_ASSET,
346 was_added,
347 key_word[2], key_word[3], ]);
350 elements.extend_from_slice(&[amount_delta, ZERO, ZERO, ZERO]);
351 }
352 }
353}
354
355impl Serializable for FungibleAssetDelta {
356 fn write_into<W: ByteWriter>(&self, target: &mut W) {
357 target.write_usize(self.0.len());
358 target.write_many(self.0.iter().map(|(vault_key, &delta)| (*vault_key, delta as u64)));
363 }
364
365 fn get_size_hint(&self) -> usize {
366 const ENTRY_SIZE: usize = AssetVaultKey::SERIALIZED_SIZE + core::mem::size_of::<u64>();
367 self.0.len().get_size_hint() + self.0.len() * ENTRY_SIZE
368 }
369}
370
371impl Deserializable for FungibleAssetDelta {
372 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
373 let num_fungible_assets = source.read_usize()?;
374 let map = source
378 .read_many_iter::<(AssetVaultKey, u64)>(num_fungible_assets)?
379 .map(|result| result.map(|(vault_key, delta_as_u64)| (vault_key, delta_as_u64 as i64)))
380 .collect::<Result<_, _>>()?;
381
382 Self::new(map).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
383 }
384}
385
386#[derive(Clone, Debug, Default, PartialEq, Eq)]
394pub struct NonFungibleAssetDelta(
395 BTreeMap<AssetVaultKey, (NonFungibleAsset, NonFungibleDeltaAction)>,
396);
397
398impl NonFungibleAssetDelta {
399 pub const fn new(
401 map: BTreeMap<AssetVaultKey, (NonFungibleAsset, NonFungibleDeltaAction)>,
402 ) -> Self {
403 Self(map)
404 }
405
406 pub fn add(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> {
411 self.apply_action(asset, NonFungibleDeltaAction::Add)
412 }
413
414 pub fn remove(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> {
419 self.apply_action(asset, NonFungibleDeltaAction::Remove)
420 }
421
422 pub fn num_assets(&self) -> usize {
424 self.0.len()
425 }
426
427 pub fn is_empty(&self) -> bool {
429 self.0.is_empty()
430 }
431
432 pub fn iter(&self) -> impl Iterator<Item = (&NonFungibleAsset, &NonFungibleDeltaAction)> {
434 self.0
435 .iter()
436 .map(|(_key, (non_fungible_asset, delta_action))| (non_fungible_asset, delta_action))
437 }
438
439 pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
446 for (&asset, &action) in other.iter() {
448 self.apply_action(asset, action)?;
449 }
450
451 Ok(())
452 }
453
454 fn apply_action(
463 &mut self,
464 asset: NonFungibleAsset,
465 action: NonFungibleDeltaAction,
466 ) -> Result<(), AccountDeltaError> {
467 match self.0.entry(asset.vault_key()) {
468 Entry::Vacant(entry) => {
469 entry.insert((asset, action));
470 },
471 Entry::Occupied(entry) => {
472 let (_prev_asset, previous_action) = *entry.get();
473 if previous_action == action {
474 return Err(AccountDeltaError::DuplicateNonFungibleVaultUpdate(asset));
476 }
477 entry.remove();
479 },
480 }
481
482 Ok(())
483 }
484
485 fn filter_by_action(
487 &self,
488 action: NonFungibleDeltaAction,
489 ) -> impl Iterator<Item = NonFungibleAsset> + '_ {
490 self.0
491 .iter()
492 .filter(move |&(_, (_asset, cur_action))| cur_action == &action)
493 .map(|(_key, (asset, _action))| *asset)
494 }
495
496 pub(super) fn append_delta_elements(&self, elements: &mut Vec<Felt>) {
499 for (asset, action) in self.iter() {
500 let was_added = match action {
501 NonFungibleDeltaAction::Remove => ZERO,
502 NonFungibleDeltaAction::Add => ONE,
503 };
504
505 let key_word = asset.vault_key().to_word();
506 elements.extend_from_slice(&[
507 DOMAIN_ASSET,
508 was_added,
509 key_word[2], key_word[3], ]);
512 elements.extend_from_slice(asset.to_value_word().as_elements());
513 }
514 }
515}
516
517impl Serializable for NonFungibleAssetDelta {
518 fn write_into<W: ByteWriter>(&self, target: &mut W) {
519 let added: Vec<_> = self.filter_by_action(NonFungibleDeltaAction::Add).collect();
520 let removed: Vec<_> = self.filter_by_action(NonFungibleDeltaAction::Remove).collect();
521
522 target.write_usize(added.len());
523 target.write_many(added.iter());
524
525 target.write_usize(removed.len());
526 target.write_many(removed.iter());
527 }
528
529 fn get_size_hint(&self) -> usize {
530 let added = self.filter_by_action(NonFungibleDeltaAction::Add).count();
531 let removed = self.filter_by_action(NonFungibleDeltaAction::Remove).count();
532
533 added.get_size_hint()
534 + removed.get_size_hint()
535 + added * NonFungibleAsset::SERIALIZED_SIZE
536 + removed * NonFungibleAsset::SERIALIZED_SIZE
537 }
538}
539
540impl Deserializable for NonFungibleAssetDelta {
541 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
542 let mut map = BTreeMap::new();
543
544 let num_added = source.read_usize()?;
545 for _ in 0..num_added {
546 let added_asset: NonFungibleAsset = source.read()?;
547 map.insert(added_asset.vault_key(), (added_asset, NonFungibleDeltaAction::Add));
548 }
549
550 let num_removed = source.read_usize()?;
551 for _ in 0..num_removed {
552 let removed_asset: NonFungibleAsset = source.read()?;
553 map.insert(removed_asset.vault_key(), (removed_asset, NonFungibleDeltaAction::Remove));
554 }
555
556 Ok(Self::new(map))
557 }
558}
559
560#[derive(Clone, Copy, Debug, PartialEq, Eq)]
561pub enum NonFungibleDeltaAction {
562 Add,
563 Remove,
564}
565
566#[cfg(test)]
570mod tests {
571 use super::{AccountVaultDelta, Deserializable, Serializable};
572 use crate::account::AccountId;
573 use crate::asset::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails};
574 use crate::testing::account_id::{
575 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
576 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
577 };
578
579 #[test]
580 fn test_serde_account_vault() {
581 let asset_0 = FungibleAsset::mock(100);
582 let asset_1 = NonFungibleAsset::mock(&[10, 21, 32, 43]);
583 let delta = AccountVaultDelta::from_iters([asset_0], [asset_1]);
584
585 let serialized = delta.to_bytes();
586 let deserialized = AccountVaultDelta::read_from_bytes(&serialized).unwrap();
587 assert_eq!(deserialized, delta);
588 }
589
590 #[test]
591 fn test_is_empty_account_vault() {
592 let faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
593 let asset: Asset = FungibleAsset::new(faucet, 123).unwrap().into();
594
595 assert!(AccountVaultDelta::default().is_empty());
596 assert!(!AccountVaultDelta::from_iters([asset], []).is_empty());
597 assert!(!AccountVaultDelta::from_iters([], [asset]).is_empty());
598 }
599
600 #[rstest::rstest]
601 #[case::pos_pos(50, 50, Some(100))]
602 #[case::neg_neg(-50, -50, Some(-100))]
603 #[case::empty_pos(0, 50, Some(50))]
604 #[case::empty_neg(0, -50, Some(-50))]
605 #[case::nullify_pos_neg(100, -100, Some(0))]
606 #[case::nullify_neg_pos(-100, 100, Some(0))]
607 #[case::overflow(FungibleAsset::MAX_AMOUNT.as_i64(), FungibleAsset::MAX_AMOUNT.as_i64(), None)]
608 #[case::underflow(-(FungibleAsset::MAX_AMOUNT.as_i64()), -(FungibleAsset::MAX_AMOUNT.as_i64()), None)]
609 #[test]
610 fn merge_fungible_aggregation(#[case] x: i64, #[case] y: i64, #[case] expected: Option<i64>) {
611 fn create_delta_with_fungible(account_id: AccountId, amount: i64) -> AccountVaultDelta {
614 let asset = FungibleAsset::new(account_id, amount.unsigned_abs()).unwrap().into();
615 match amount {
616 0 => AccountVaultDelta::default(),
617 x if x.is_positive() => AccountVaultDelta::from_iters([asset], []),
618 _ => AccountVaultDelta::from_iters([], [asset]),
619 }
620 }
621
622 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
623
624 let mut delta_x = create_delta_with_fungible(account_id, x);
625 let delta_y = create_delta_with_fungible(account_id, y);
626
627 let result = delta_x.merge(delta_y);
628
629 if let Some(expected) = expected {
631 let expected = create_delta_with_fungible(account_id, expected);
632 assert_eq!(result.map(|_| delta_x).unwrap(), expected);
633 } else {
634 assert!(result.is_err());
635 }
636 }
637
638 #[rstest::rstest]
639 #[case::empty_removed(None, Some(false), Ok(Some(false)))]
640 #[case::empty_added(None, Some(true), Ok(Some(true)))]
641 #[case::add_remove(Some(true), Some(false), Ok(None))]
642 #[case::remove_add(Some(false), Some(true), Ok(None))]
643 #[case::double_add(Some(true), Some(true), Err(()))]
644 #[case::double_remove(Some(false), Some(false), Err(()))]
645 #[test]
646 fn merge_non_fungible_aggregation(
647 #[case] x: Option<bool>,
648 #[case] y: Option<bool>,
649 #[case] expected: Result<Option<bool>, ()>,
650 ) {
651 fn create_delta_with_non_fungible(
654 account_id: AccountId,
655 added: Option<bool>,
656 ) -> AccountVaultDelta {
657 let asset: Asset =
658 NonFungibleAsset::new(&NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]))
659 .into();
660
661 match added {
662 Some(true) => AccountVaultDelta::from_iters([asset], []),
663 Some(false) => AccountVaultDelta::from_iters([], [asset]),
664 None => AccountVaultDelta::default(),
665 }
666 }
667
668 let account_id = NonFungibleAsset::mock_issuer();
669
670 let mut delta_x = create_delta_with_non_fungible(account_id, x);
671 let delta_y = create_delta_with_non_fungible(account_id, y);
672
673 let result = delta_x.merge(delta_y);
674
675 if let Ok(expected) = expected {
676 let expected = create_delta_with_non_fungible(account_id, expected);
677 assert_eq!(result.map(|_| delta_x).unwrap(), expected);
678 } else {
679 assert!(result.is_err());
680 }
681 }
682}