1use alloc::{
2 collections::{btree_map::Entry, BTreeMap},
3 string::ToString,
4 vec::Vec,
5};
6
7use super::{
8 AccountDeltaError, ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
9};
10use crate::{
11 account::{AccountId, AccountType},
12 asset::{Asset, AssetVault, FungibleAsset, NonFungibleAsset},
13};
14#[derive(Clone, Debug, Default, PartialEq, Eq)]
24pub struct AccountVaultDelta {
25 fungible: FungibleAssetDelta,
26 non_fungible: NonFungibleAssetDelta,
27}
28
29impl AccountVaultDelta {
30 pub const fn new(fungible: FungibleAssetDelta, non_fungible: NonFungibleAssetDelta) -> Self {
36 Self { fungible, non_fungible }
37 }
38
39 pub fn fungible(&self) -> &FungibleAssetDelta {
41 &self.fungible
42 }
43
44 pub fn non_fungible(&self) -> &NonFungibleAssetDelta {
46 &self.non_fungible
47 }
48
49 pub fn is_empty(&self) -> bool {
51 self.fungible.is_empty() && self.non_fungible.is_empty()
52 }
53
54 pub fn add_asset(&mut self, asset: Asset) -> Result<(), AccountDeltaError> {
56 match asset {
57 Asset::Fungible(asset) => self.fungible.add(asset),
58 Asset::NonFungible(asset) => self.non_fungible.add(asset),
59 }
60 }
61
62 pub fn remove_asset(&mut self, asset: Asset) -> Result<(), AccountDeltaError> {
64 match asset {
65 Asset::Fungible(asset) => self.fungible.remove(asset),
66 Asset::NonFungible(asset) => self.non_fungible.remove(asset),
67 }
68 }
69
70 pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
77 self.non_fungible.merge(other.non_fungible)?;
78 self.fungible.merge(other.fungible)
79 }
80}
81
82#[cfg(any(feature = "testing", test))]
83impl AccountVaultDelta {
84 pub fn from_iters(
86 added_assets: impl IntoIterator<Item = crate::asset::Asset>,
87 removed_assets: impl IntoIterator<Item = crate::asset::Asset>,
88 ) -> Self {
89 use crate::asset::Asset;
90
91 let mut fungible = FungibleAssetDelta::default();
92 let mut non_fungible = NonFungibleAssetDelta::default();
93
94 for asset in added_assets {
95 match asset {
96 Asset::Fungible(asset) => {
97 fungible.add(asset).unwrap();
98 },
99 Asset::NonFungible(asset) => {
100 non_fungible.add(asset).unwrap();
101 },
102 }
103 }
104
105 for asset in removed_assets {
106 match asset {
107 Asset::Fungible(asset) => {
108 fungible.remove(asset).unwrap();
109 },
110 Asset::NonFungible(asset) => {
111 non_fungible.remove(asset).unwrap();
112 },
113 }
114 }
115
116 Self { fungible, non_fungible }
117 }
118
119 pub fn added_assets(&self) -> impl Iterator<Item = crate::asset::Asset> + '_ {
121 use crate::asset::{Asset, FungibleAsset, NonFungibleAsset};
122 self.fungible
123 .0
124 .iter()
125 .filter(|&(_, &value)| value >= 0)
126 .map(|(&faucet_id, &diff)| {
127 Asset::Fungible(FungibleAsset::new(faucet_id, diff.unsigned_abs()).unwrap())
128 })
129 .chain(self.non_fungible.filter_by_action(NonFungibleDeltaAction::Add).map(|key| {
130 Asset::NonFungible(unsafe { NonFungibleAsset::new_unchecked(key.into()) })
131 }))
132 }
133
134 pub fn removed_assets(&self) -> impl Iterator<Item = crate::asset::Asset> + '_ {
136 use crate::asset::{Asset, FungibleAsset, NonFungibleAsset};
137 self.fungible
138 .0
139 .iter()
140 .filter(|&(_, &value)| value < 0)
141 .map(|(&faucet_id, &diff)| {
142 Asset::Fungible(FungibleAsset::new(faucet_id, diff.unsigned_abs()).unwrap())
143 })
144 .chain(self.non_fungible.filter_by_action(NonFungibleDeltaAction::Remove).map(|key| {
145 Asset::NonFungible(unsafe { NonFungibleAsset::new_unchecked(key.into()) })
146 }))
147 }
148}
149
150impl From<&AssetVault> for AccountVaultDelta {
151 fn from(vault: &AssetVault) -> Self {
152 let mut fungible = BTreeMap::new();
153 let mut non_fungible = BTreeMap::new();
154
155 for asset in vault.assets() {
156 match asset {
157 Asset::Fungible(asset) => {
158 fungible.insert(
159 asset.faucet_id(),
160 asset
161 .amount()
162 .try_into()
163 .expect("asset amount should be at most i64::MAX by construction"),
164 );
165 },
166 Asset::NonFungible(asset) => {
167 non_fungible.insert(asset, NonFungibleDeltaAction::Add);
168 },
169 }
170 }
171
172 Self {
173 fungible: FungibleAssetDelta(fungible),
174 non_fungible: NonFungibleAssetDelta::new(non_fungible),
175 }
176 }
177}
178
179impl Serializable for AccountVaultDelta {
180 fn write_into<W: ByteWriter>(&self, target: &mut W) {
181 target.write(&self.fungible);
182 target.write(&self.non_fungible);
183 }
184
185 fn get_size_hint(&self) -> usize {
186 self.fungible.get_size_hint() + self.non_fungible.get_size_hint()
187 }
188}
189
190impl Deserializable for AccountVaultDelta {
191 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
192 let fungible = source.read()?;
193 let non_fungible = source.read()?;
194
195 Ok(Self::new(fungible, non_fungible))
196 }
197}
198
199#[derive(Clone, Debug, Default, PartialEq, Eq)]
204pub struct FungibleAssetDelta(BTreeMap<AccountId, i64>);
205
206impl FungibleAssetDelta {
207 pub fn new(map: BTreeMap<AccountId, i64>) -> Result<Self, AccountDeltaError> {
212 let delta = Self(map);
213 delta.validate()?;
214
215 Ok(delta)
216 }
217
218 pub fn add(&mut self, asset: FungibleAsset) -> Result<(), AccountDeltaError> {
223 let amount: i64 = asset.amount().try_into().expect("Amount it too high");
224 self.add_delta(asset.faucet_id(), amount)
225 }
226
227 pub fn remove(&mut self, asset: FungibleAsset) -> Result<(), AccountDeltaError> {
232 let amount: i64 = asset.amount().try_into().expect("Amount it too high");
233 self.add_delta(asset.faucet_id(), -amount)
234 }
235
236 pub fn num_assets(&self) -> usize {
238 self.0.len()
239 }
240
241 pub fn is_empty(&self) -> bool {
243 self.0.is_empty()
244 }
245
246 pub fn iter(&self) -> impl Iterator<Item = (&AccountId, &i64)> {
248 self.0.iter()
249 }
250
251 pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
258 for (&faucet_id, &amount) in other.0.iter() {
264 self.add_delta(faucet_id, amount)?;
265 }
266
267 Ok(())
268 }
269
270 fn add_delta(&mut self, faucet_id: AccountId, delta: i64) -> Result<(), AccountDeltaError> {
279 match self.0.entry(faucet_id) {
280 Entry::Vacant(entry) => {
281 entry.insert(delta);
282 },
283 Entry::Occupied(mut entry) => {
284 let old = *entry.get();
285 let new = old.checked_add(delta).ok_or(
286 AccountDeltaError::FungibleAssetDeltaOverflow {
287 faucet_id,
288 current: old,
289 delta,
290 },
291 )?;
292
293 if new == 0 {
294 entry.remove();
295 } else {
296 *entry.get_mut() = new;
297 }
298 },
299 }
300
301 Ok(())
302 }
303
304 fn validate(&self) -> Result<(), AccountDeltaError> {
309 for faucet_id in self.0.keys() {
310 if !matches!(faucet_id.account_type(), AccountType::FungibleFaucet) {
311 return Err(AccountDeltaError::NotAFungibleFaucetId(*faucet_id));
312 }
313 }
314
315 Ok(())
316 }
317}
318
319impl Serializable for FungibleAssetDelta {
320 fn write_into<W: ByteWriter>(&self, target: &mut W) {
321 target.write_usize(self.0.len());
322 target.write_many(self.0.iter().map(|(&faucet_id, &delta)| (faucet_id, delta as u64)));
326 }
327
328 fn get_size_hint(&self) -> usize {
329 self.0.len().get_size_hint() + self.0.len() * FungibleAsset::SERIALIZED_SIZE
330 }
331}
332
333impl Deserializable for FungibleAssetDelta {
334 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
335 let num_fungible_assets = source.read_usize()?;
336 let map = source
340 .read_many::<(AccountId, u64)>(num_fungible_assets)?
341 .into_iter()
342 .map(|(account_id, delta_as_u64)| (account_id, delta_as_u64 as i64))
343 .collect();
344
345 Self::new(map).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
346 }
347}
348
349#[derive(Clone, Debug, Default, PartialEq, Eq)]
354pub struct NonFungibleAssetDelta(BTreeMap<NonFungibleAsset, NonFungibleDeltaAction>);
355
356impl NonFungibleAssetDelta {
357 pub const fn new(map: BTreeMap<NonFungibleAsset, NonFungibleDeltaAction>) -> Self {
359 Self(map)
360 }
361
362 pub fn add(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> {
367 self.apply_action(asset, NonFungibleDeltaAction::Add)
368 }
369
370 pub fn remove(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> {
375 self.apply_action(asset, NonFungibleDeltaAction::Remove)
376 }
377
378 pub fn num_assets(&self) -> usize {
380 self.0.len()
381 }
382
383 pub fn is_empty(&self) -> bool {
385 self.0.is_empty()
386 }
387
388 pub fn iter(&self) -> impl Iterator<Item = (&NonFungibleAsset, &NonFungibleDeltaAction)> {
390 self.0.iter()
391 }
392
393 pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
400 for (&key, &action) in other.0.iter() {
402 self.apply_action(key, action)?;
403 }
404
405 Ok(())
406 }
407
408 fn apply_action(
417 &mut self,
418 asset: NonFungibleAsset,
419 action: NonFungibleDeltaAction,
420 ) -> Result<(), AccountDeltaError> {
421 match self.0.entry(asset) {
422 Entry::Vacant(entry) => {
423 entry.insert(action);
424 },
425 Entry::Occupied(entry) => {
426 let previous = *entry.get();
427 if previous == action {
428 return Err(AccountDeltaError::DuplicateNonFungibleVaultUpdate(asset));
430 }
431 entry.remove();
433 },
434 }
435
436 Ok(())
437 }
438
439 fn filter_by_action(
441 &self,
442 action: NonFungibleDeltaAction,
443 ) -> impl Iterator<Item = NonFungibleAsset> + '_ {
444 self.0
445 .iter()
446 .filter(move |&(_, cur_action)| cur_action == &action)
447 .map(|(key, _)| *key)
448 }
449}
450
451impl Serializable for NonFungibleAssetDelta {
452 fn write_into<W: ByteWriter>(&self, target: &mut W) {
453 let added: Vec<_> = self.filter_by_action(NonFungibleDeltaAction::Add).collect();
454 let removed: Vec<_> = self.filter_by_action(NonFungibleDeltaAction::Remove).collect();
455
456 target.write_usize(added.len());
457 target.write_many(added.iter());
458
459 target.write_usize(removed.len());
460 target.write_many(removed.iter());
461 }
462
463 fn get_size_hint(&self) -> usize {
464 let added = self.filter_by_action(NonFungibleDeltaAction::Add).count();
465 let removed = self.filter_by_action(NonFungibleDeltaAction::Remove).count();
466
467 added.get_size_hint()
468 + removed.get_size_hint()
469 + added * NonFungibleAsset::SERIALIZED_SIZE
470 + removed * NonFungibleAsset::SERIALIZED_SIZE
471 }
472}
473
474impl Deserializable for NonFungibleAssetDelta {
475 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
476 let mut map = BTreeMap::new();
477
478 let num_added = source.read_usize()?;
479 for _ in 0..num_added {
480 let added_asset = source.read()?;
481 map.insert(added_asset, NonFungibleDeltaAction::Add);
482 }
483
484 let num_removed = source.read_usize()?;
485 for _ in 0..num_removed {
486 let removed_asset = source.read()?;
487 map.insert(removed_asset, NonFungibleDeltaAction::Remove);
488 }
489
490 Ok(Self::new(map))
491 }
492}
493
494#[derive(Clone, Copy, Debug, PartialEq, Eq)]
495pub enum NonFungibleDeltaAction {
496 Add,
497 Remove,
498}
499
500#[cfg(test)]
504mod tests {
505 use super::{AccountVaultDelta, Deserializable, Serializable};
506 use crate::{
507 account::{AccountId, AccountIdPrefix},
508 asset::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails},
509 testing::account_id::{
510 ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN,
511 },
512 };
513
514 #[test]
515 fn test_serde_account_vault() {
516 let asset_0 = FungibleAsset::mock(100);
517 let asset_1 = NonFungibleAsset::mock(&[10, 21, 32, 43]);
518 let delta = AccountVaultDelta::from_iters([asset_0], [asset_1]);
519
520 let serialized = delta.to_bytes();
521 let deserialized = AccountVaultDelta::read_from_bytes(&serialized).unwrap();
522 assert_eq!(deserialized, delta);
523 }
524
525 #[test]
526 fn test_is_empty_account_vault() {
527 let faucet = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN).unwrap();
528 let asset: Asset = FungibleAsset::new(faucet, 123).unwrap().into();
529
530 assert!(AccountVaultDelta::default().is_empty());
531 assert!(!AccountVaultDelta::from_iters([asset], []).is_empty());
532 assert!(!AccountVaultDelta::from_iters([], [asset]).is_empty());
533 }
534
535 #[rstest::rstest]
536 #[case::pos_pos(50, 50, Some(100))]
537 #[case::neg_neg(-50, -50, Some(-100))]
538 #[case::empty_pos(0, 50, Some(50))]
539 #[case::empty_neg(0, -50, Some(-50))]
540 #[case::nullify_pos_neg(100, -100, Some(0))]
541 #[case::nullify_neg_pos(-100, 100, Some(0))]
542 #[case::overflow(FungibleAsset::MAX_AMOUNT as i64, FungibleAsset::MAX_AMOUNT as i64, None)]
543 #[case::underflow(-(FungibleAsset::MAX_AMOUNT as i64), -(FungibleAsset::MAX_AMOUNT as i64), None)]
544 #[test]
545 fn merge_fungible_aggregation(#[case] x: i64, #[case] y: i64, #[case] expected: Option<i64>) {
546 fn create_delta_with_fungible(account_id: AccountId, amount: i64) -> AccountVaultDelta {
549 let asset = FungibleAsset::new(account_id, amount.unsigned_abs()).unwrap().into();
550 match amount {
551 0 => AccountVaultDelta::default(),
552 x if x.is_positive() => AccountVaultDelta::from_iters([asset], []),
553 _ => AccountVaultDelta::from_iters([], [asset]),
554 }
555 }
556
557 let account_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN).unwrap();
558
559 let mut delta_x = create_delta_with_fungible(account_id, x);
560 let delta_y = create_delta_with_fungible(account_id, y);
561
562 let result = delta_x.merge(delta_y);
563
564 if let Some(expected) = expected {
566 let expected = create_delta_with_fungible(account_id, expected);
567 assert_eq!(result.map(|_| delta_x).unwrap(), expected);
568 } else {
569 assert!(result.is_err());
570 }
571 }
572
573 #[rstest::rstest]
574 #[case::empty_removed(None, Some(false), Ok(Some(false)))]
575 #[case::empty_added(None, Some(true), Ok(Some(true)))]
576 #[case::add_remove(Some(true), Some(false), Ok(None))]
577 #[case::remove_add(Some(false), Some(true), Ok(None))]
578 #[case::double_add(Some(true), Some(true), Err(()))]
579 #[case::double_remove(Some(false), Some(false), Err(()))]
580 #[test]
581 fn merge_non_fungible_aggregation(
582 #[case] x: Option<bool>,
583 #[case] y: Option<bool>,
584 #[case] expected: Result<Option<bool>, ()>,
585 ) {
586 fn create_delta_with_non_fungible(
589 account_id_prefix: AccountIdPrefix,
590 added: Option<bool>,
591 ) -> AccountVaultDelta {
592 let asset: Asset = NonFungibleAsset::new(
593 &NonFungibleAssetDetails::new(account_id_prefix, vec![1, 2, 3]).unwrap(),
594 )
595 .unwrap()
596 .into();
597
598 match added {
599 Some(true) => AccountVaultDelta::from_iters([asset], []),
600 Some(false) => AccountVaultDelta::from_iters([], [asset]),
601 None => AccountVaultDelta::default(),
602 }
603 }
604
605 let account_id = NonFungibleAsset::mock_issuer().prefix();
606
607 let mut delta_x = create_delta_with_non_fungible(account_id, x);
608 let delta_y = create_delta_with_non_fungible(account_id, y);
609
610 let result = delta_x.merge(delta_y);
611
612 if let Ok(expected) = expected {
613 let expected = create_delta_with_non_fungible(account_id, expected);
614 assert_eq!(result.map(|_| delta_x).unwrap(), expected);
615 } else {
616 assert!(result.is_err());
617 }
618 }
619}