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::account::{AccountId, AccountType};
15use crate::asset::{Asset, AssetVaultKey, FungibleAsset, NonFungibleAsset};
16use crate::{Felt, ONE, ZERO};
17
18const DOMAIN_ASSET: Felt = Felt::new(1);
23
24#[derive(Clone, Debug, Default, PartialEq, Eq)]
31pub struct AccountVaultDelta {
32 fungible: FungibleAssetDelta,
33 non_fungible: NonFungibleAssetDelta,
34}
35
36impl AccountVaultDelta {
37 pub const fn new(fungible: FungibleAssetDelta, non_fungible: NonFungibleAssetDelta) -> Self {
43 Self { fungible, non_fungible }
44 }
45
46 pub fn fungible(&self) -> &FungibleAssetDelta {
48 &self.fungible
49 }
50
51 pub fn non_fungible(&self) -> &NonFungibleAssetDelta {
53 &self.non_fungible
54 }
55
56 pub fn is_empty(&self) -> bool {
58 self.fungible.is_empty() && self.non_fungible.is_empty()
59 }
60
61 pub fn add_asset(&mut self, asset: Asset) -> Result<(), AccountDeltaError> {
63 match asset {
64 Asset::Fungible(asset) => self.fungible.add(asset),
65 Asset::NonFungible(asset) => self.non_fungible.add(asset),
66 }
67 }
68
69 pub fn remove_asset(&mut self, asset: Asset) -> Result<(), AccountDeltaError> {
71 match asset {
72 Asset::Fungible(asset) => self.fungible.remove(asset),
73 Asset::NonFungible(asset) => self.non_fungible.remove(asset),
74 }
75 }
76
77 pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
84 self.non_fungible.merge(other.non_fungible)?;
85 self.fungible.merge(other.fungible)
86 }
87
88 pub(super) fn append_delta_elements(&self, elements: &mut Vec<Felt>) {
91 self.fungible().append_delta_elements(elements);
92 self.non_fungible().append_delta_elements(elements);
93 }
94}
95
96#[cfg(any(feature = "testing", test))]
97impl AccountVaultDelta {
98 pub fn from_iters(
100 added_assets: impl IntoIterator<Item = crate::asset::Asset>,
101 removed_assets: impl IntoIterator<Item = crate::asset::Asset>,
102 ) -> Self {
103 let mut fungible = FungibleAssetDelta::default();
104 let mut non_fungible = NonFungibleAssetDelta::default();
105
106 for asset in added_assets {
107 match asset {
108 Asset::Fungible(asset) => {
109 fungible.add(asset).unwrap();
110 },
111 Asset::NonFungible(asset) => {
112 non_fungible.add(asset).unwrap();
113 },
114 }
115 }
116
117 for asset in removed_assets {
118 match asset {
119 Asset::Fungible(asset) => {
120 fungible.remove(asset).unwrap();
121 },
122 Asset::NonFungible(asset) => {
123 non_fungible.remove(asset).unwrap();
124 },
125 }
126 }
127
128 Self { fungible, non_fungible }
129 }
130
131 pub fn added_assets(&self) -> impl Iterator<Item = crate::asset::Asset> + '_ {
133 self.fungible
134 .0
135 .iter()
136 .filter(|&(_, &value)| value >= 0)
137 .map(|(&faucet_id, &diff)| {
138 Asset::Fungible(FungibleAsset::new(faucet_id, diff.unsigned_abs()).unwrap())
139 })
140 .chain(
141 self.non_fungible
142 .filter_by_action(NonFungibleDeltaAction::Add)
143 .map(Asset::NonFungible),
144 )
145 }
146
147 pub fn removed_assets(&self) -> impl Iterator<Item = crate::asset::Asset> + '_ {
149 self.fungible
150 .0
151 .iter()
152 .filter(|&(_, &value)| value < 0)
153 .map(|(&faucet_id, &diff)| {
154 Asset::Fungible(FungibleAsset::new(faucet_id, diff.unsigned_abs()).unwrap())
155 })
156 .chain(
157 self.non_fungible
158 .filter_by_action(NonFungibleDeltaAction::Remove)
159 .map(Asset::NonFungible),
160 )
161 }
162}
163
164impl Serializable for AccountVaultDelta {
165 fn write_into<W: ByteWriter>(&self, target: &mut W) {
166 target.write(&self.fungible);
167 target.write(&self.non_fungible);
168 }
169
170 fn get_size_hint(&self) -> usize {
171 self.fungible.get_size_hint() + self.non_fungible.get_size_hint()
172 }
173}
174
175impl Deserializable for AccountVaultDelta {
176 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
177 let fungible = source.read()?;
178 let non_fungible = source.read()?;
179
180 Ok(Self::new(fungible, non_fungible))
181 }
182}
183
184#[derive(Clone, Debug, Default, PartialEq, Eq)]
189pub struct FungibleAssetDelta(BTreeMap<AccountId, i64>);
190
191impl FungibleAssetDelta {
192 pub fn new(map: BTreeMap<AccountId, i64>) -> Result<Self, AccountDeltaError> {
197 let delta = Self(map);
198 delta.validate()?;
199
200 Ok(delta)
201 }
202
203 pub fn add(&mut self, asset: FungibleAsset) -> Result<(), AccountDeltaError> {
208 let amount: i64 = asset.amount().try_into().expect("Amount it too high");
209 self.add_delta(asset.faucet_id(), amount)
210 }
211
212 pub fn remove(&mut self, asset: FungibleAsset) -> Result<(), AccountDeltaError> {
217 let amount: i64 = asset.amount().try_into().expect("Amount it too high");
218 self.add_delta(asset.faucet_id(), -amount)
219 }
220
221 pub fn amount(&self, faucet_id: &AccountId) -> Option<i64> {
223 self.0.get(faucet_id).copied()
224 }
225
226 pub fn num_assets(&self) -> usize {
228 self.0.len()
229 }
230
231 pub fn is_empty(&self) -> bool {
233 self.0.is_empty()
234 }
235
236 pub fn iter(&self) -> impl Iterator<Item = (&AccountId, &i64)> {
238 self.0.iter()
239 }
240
241 pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
248 for (&faucet_id, &amount) in other.0.iter() {
254 self.add_delta(faucet_id, amount)?;
255 }
256
257 Ok(())
258 }
259
260 fn add_delta(&mut self, faucet_id: AccountId, delta: i64) -> Result<(), AccountDeltaError> {
269 match self.0.entry(faucet_id) {
270 Entry::Vacant(entry) => {
271 if delta != 0 {
273 entry.insert(delta);
274 }
275 },
276 Entry::Occupied(mut entry) => {
277 let old = *entry.get();
278 let new = old.checked_add(delta).ok_or(
279 AccountDeltaError::FungibleAssetDeltaOverflow {
280 faucet_id,
281 current: old,
282 delta,
283 },
284 )?;
285
286 if new == 0 {
287 entry.remove();
288 } else {
289 *entry.get_mut() = new;
290 }
291 },
292 }
293
294 Ok(())
295 }
296
297 fn validate(&self) -> Result<(), AccountDeltaError> {
302 for faucet_id in self.0.keys() {
303 if !matches!(faucet_id.account_type(), AccountType::FungibleFaucet) {
304 return Err(AccountDeltaError::NotAFungibleFaucetId(*faucet_id));
305 }
306 }
307
308 Ok(())
309 }
310
311 pub(super) fn append_delta_elements(&self, elements: &mut Vec<Felt>) {
322 for (faucet_id, amount_delta) in self.iter() {
323 debug_assert_ne!(
326 *amount_delta, 0,
327 "fungible asset iterator should never yield amount deltas of 0"
328 );
329
330 let was_added = if *amount_delta > 0 { ONE } else { ZERO };
331 let amount_delta = Felt::try_from(amount_delta.unsigned_abs())
332 .expect("amount delta should be less than i64::MAX");
333
334 elements.extend_from_slice(&[
335 DOMAIN_ASSET,
336 was_added,
337 faucet_id.suffix(),
338 faucet_id.prefix().as_felt(),
339 ]);
340 elements.extend_from_slice(&[amount_delta, ZERO, ZERO, ZERO]);
341 }
342 }
343}
344
345impl Serializable for FungibleAssetDelta {
346 fn write_into<W: ByteWriter>(&self, target: &mut W) {
347 target.write_usize(self.0.len());
348 target.write_many(self.0.iter().map(|(&faucet_id, &delta)| (faucet_id, delta as u64)));
352 }
353
354 fn get_size_hint(&self) -> usize {
355 self.0.len().get_size_hint() + self.0.len() * FungibleAsset::SERIALIZED_SIZE
356 }
357}
358
359impl Deserializable for FungibleAssetDelta {
360 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
361 let num_fungible_assets = source.read_usize()?;
362 let map = source
366 .read_many_iter::<(AccountId, u64)>(num_fungible_assets)?
367 .map(|result| {
368 result.map(|(account_id, delta_as_u64)| (account_id, delta_as_u64 as i64))
369 })
370 .collect::<Result<_, _>>()?;
371
372 Self::new(map).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
373 }
374}
375
376#[derive(Clone, Debug, Default, PartialEq, Eq)]
384pub struct NonFungibleAssetDelta(
385 BTreeMap<AssetVaultKey, (NonFungibleAsset, NonFungibleDeltaAction)>,
386);
387
388impl NonFungibleAssetDelta {
389 pub const fn new(
391 map: BTreeMap<AssetVaultKey, (NonFungibleAsset, NonFungibleDeltaAction)>,
392 ) -> Self {
393 Self(map)
394 }
395
396 pub fn add(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> {
401 self.apply_action(asset, NonFungibleDeltaAction::Add)
402 }
403
404 pub fn remove(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> {
409 self.apply_action(asset, NonFungibleDeltaAction::Remove)
410 }
411
412 pub fn num_assets(&self) -> usize {
414 self.0.len()
415 }
416
417 pub fn is_empty(&self) -> bool {
419 self.0.is_empty()
420 }
421
422 pub fn iter(&self) -> impl Iterator<Item = (&NonFungibleAsset, &NonFungibleDeltaAction)> {
424 self.0
425 .iter()
426 .map(|(_key, (non_fungible_asset, delta_action))| (non_fungible_asset, delta_action))
427 }
428
429 pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
436 for (&asset, &action) in other.iter() {
438 self.apply_action(asset, action)?;
439 }
440
441 Ok(())
442 }
443
444 fn apply_action(
453 &mut self,
454 asset: NonFungibleAsset,
455 action: NonFungibleDeltaAction,
456 ) -> Result<(), AccountDeltaError> {
457 match self.0.entry(asset.vault_key()) {
458 Entry::Vacant(entry) => {
459 entry.insert((asset, action));
460 },
461 Entry::Occupied(entry) => {
462 let (_prev_asset, previous_action) = *entry.get();
463 if previous_action == action {
464 return Err(AccountDeltaError::DuplicateNonFungibleVaultUpdate(asset));
466 }
467 entry.remove();
469 },
470 }
471
472 Ok(())
473 }
474
475 fn filter_by_action(
477 &self,
478 action: NonFungibleDeltaAction,
479 ) -> impl Iterator<Item = NonFungibleAsset> + '_ {
480 self.0
481 .iter()
482 .filter(move |&(_, (_asset, cur_action))| cur_action == &action)
483 .map(|(_key, (asset, _action))| *asset)
484 }
485
486 pub(super) fn append_delta_elements(&self, elements: &mut Vec<Felt>) {
489 for (asset, action) in self.iter() {
490 let was_added = match action {
491 NonFungibleDeltaAction::Remove => ZERO,
492 NonFungibleDeltaAction::Add => ONE,
493 };
494
495 elements.extend_from_slice(&[
496 DOMAIN_ASSET,
497 was_added,
498 asset.faucet_id().suffix(),
499 asset.faucet_id().prefix().as_felt(),
500 ]);
501 elements.extend_from_slice(asset.to_value_word().as_elements());
502 }
503 }
504}
505
506impl Serializable for NonFungibleAssetDelta {
507 fn write_into<W: ByteWriter>(&self, target: &mut W) {
508 let added: Vec<_> = self.filter_by_action(NonFungibleDeltaAction::Add).collect();
509 let removed: Vec<_> = self.filter_by_action(NonFungibleDeltaAction::Remove).collect();
510
511 target.write_usize(added.len());
512 target.write_many(added.iter());
513
514 target.write_usize(removed.len());
515 target.write_many(removed.iter());
516 }
517
518 fn get_size_hint(&self) -> usize {
519 let added = self.filter_by_action(NonFungibleDeltaAction::Add).count();
520 let removed = self.filter_by_action(NonFungibleDeltaAction::Remove).count();
521
522 added.get_size_hint()
523 + removed.get_size_hint()
524 + added * NonFungibleAsset::SERIALIZED_SIZE
525 + removed * NonFungibleAsset::SERIALIZED_SIZE
526 }
527}
528
529impl Deserializable for NonFungibleAssetDelta {
530 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
531 let mut map = BTreeMap::new();
532
533 let num_added = source.read_usize()?;
534 for _ in 0..num_added {
535 let added_asset: NonFungibleAsset = source.read()?;
536 map.insert(added_asset.vault_key(), (added_asset, NonFungibleDeltaAction::Add));
537 }
538
539 let num_removed = source.read_usize()?;
540 for _ in 0..num_removed {
541 let removed_asset: NonFungibleAsset = source.read()?;
542 map.insert(removed_asset.vault_key(), (removed_asset, NonFungibleDeltaAction::Remove));
543 }
544
545 Ok(Self::new(map))
546 }
547}
548
549#[derive(Clone, Copy, Debug, PartialEq, Eq)]
550pub enum NonFungibleDeltaAction {
551 Add,
552 Remove,
553}
554
555#[cfg(test)]
559mod tests {
560 use super::{AccountVaultDelta, Deserializable, Serializable};
561 use crate::account::AccountId;
562 use crate::asset::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails};
563 use crate::testing::account_id::{
564 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
565 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
566 };
567
568 #[test]
569 fn test_serde_account_vault() {
570 let asset_0 = FungibleAsset::mock(100);
571 let asset_1 = NonFungibleAsset::mock(&[10, 21, 32, 43]);
572 let delta = AccountVaultDelta::from_iters([asset_0], [asset_1]);
573
574 let serialized = delta.to_bytes();
575 let deserialized = AccountVaultDelta::read_from_bytes(&serialized).unwrap();
576 assert_eq!(deserialized, delta);
577 }
578
579 #[test]
580 fn test_is_empty_account_vault() {
581 let faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
582 let asset: Asset = FungibleAsset::new(faucet, 123).unwrap().into();
583
584 assert!(AccountVaultDelta::default().is_empty());
585 assert!(!AccountVaultDelta::from_iters([asset], []).is_empty());
586 assert!(!AccountVaultDelta::from_iters([], [asset]).is_empty());
587 }
588
589 #[rstest::rstest]
590 #[case::pos_pos(50, 50, Some(100))]
591 #[case::neg_neg(-50, -50, Some(-100))]
592 #[case::empty_pos(0, 50, Some(50))]
593 #[case::empty_neg(0, -50, Some(-50))]
594 #[case::nullify_pos_neg(100, -100, Some(0))]
595 #[case::nullify_neg_pos(-100, 100, Some(0))]
596 #[case::overflow(FungibleAsset::MAX_AMOUNT as i64, FungibleAsset::MAX_AMOUNT as i64, None)]
597 #[case::underflow(-(FungibleAsset::MAX_AMOUNT as i64), -(FungibleAsset::MAX_AMOUNT as i64), None)]
598 #[test]
599 fn merge_fungible_aggregation(#[case] x: i64, #[case] y: i64, #[case] expected: Option<i64>) {
600 fn create_delta_with_fungible(account_id: AccountId, amount: i64) -> AccountVaultDelta {
603 let asset = FungibleAsset::new(account_id, amount.unsigned_abs()).unwrap().into();
604 match amount {
605 0 => AccountVaultDelta::default(),
606 x if x.is_positive() => AccountVaultDelta::from_iters([asset], []),
607 _ => AccountVaultDelta::from_iters([], [asset]),
608 }
609 }
610
611 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
612
613 let mut delta_x = create_delta_with_fungible(account_id, x);
614 let delta_y = create_delta_with_fungible(account_id, y);
615
616 let result = delta_x.merge(delta_y);
617
618 if let Some(expected) = expected {
620 let expected = create_delta_with_fungible(account_id, expected);
621 assert_eq!(result.map(|_| delta_x).unwrap(), expected);
622 } else {
623 assert!(result.is_err());
624 }
625 }
626
627 #[rstest::rstest]
628 #[case::empty_removed(None, Some(false), Ok(Some(false)))]
629 #[case::empty_added(None, Some(true), Ok(Some(true)))]
630 #[case::add_remove(Some(true), Some(false), Ok(None))]
631 #[case::remove_add(Some(false), Some(true), Ok(None))]
632 #[case::double_add(Some(true), Some(true), Err(()))]
633 #[case::double_remove(Some(false), Some(false), Err(()))]
634 #[test]
635 fn merge_non_fungible_aggregation(
636 #[case] x: Option<bool>,
637 #[case] y: Option<bool>,
638 #[case] expected: Result<Option<bool>, ()>,
639 ) {
640 fn create_delta_with_non_fungible(
643 account_id: AccountId,
644 added: Option<bool>,
645 ) -> AccountVaultDelta {
646 let asset: Asset = NonFungibleAsset::new(
647 &NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]).unwrap(),
648 )
649 .unwrap()
650 .into();
651
652 match added {
653 Some(true) => AccountVaultDelta::from_iters([asset], []),
654 Some(false) => AccountVaultDelta::from_iters([], [asset]),
655 None => AccountVaultDelta::default(),
656 }
657 }
658
659 let account_id = NonFungibleAsset::mock_issuer();
660
661 let mut delta_x = create_delta_with_non_fungible(account_id, x);
662 let delta_y = create_delta_with_non_fungible(account_id, y);
663
664 let result = delta_x.merge(delta_y);
665
666 if let Ok(expected) = expected {
667 let expected = create_delta_with_non_fungible(account_id, expected);
668 assert_eq!(result.map(|_| delta_x).unwrap(), expected);
669 } else {
670 assert!(result.is_err());
671 }
672 }
673}