1use alloc::{
2 collections::{BTreeMap, btree_map::Entry},
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 amount(&self, faucet_id: &AccountId) -> Option<i64> {
238 self.0.get(faucet_id).copied()
239 }
240
241 pub fn num_assets(&self) -> usize {
243 self.0.len()
244 }
245
246 pub fn is_empty(&self) -> bool {
248 self.0.is_empty()
249 }
250
251 pub fn iter(&self) -> impl Iterator<Item = (&AccountId, &i64)> {
253 self.0.iter()
254 }
255
256 pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
263 for (&faucet_id, &amount) in other.0.iter() {
269 self.add_delta(faucet_id, amount)?;
270 }
271
272 Ok(())
273 }
274
275 fn add_delta(&mut self, faucet_id: AccountId, delta: i64) -> Result<(), AccountDeltaError> {
284 match self.0.entry(faucet_id) {
285 Entry::Vacant(entry) => {
286 entry.insert(delta);
287 },
288 Entry::Occupied(mut entry) => {
289 let old = *entry.get();
290 let new = old.checked_add(delta).ok_or(
291 AccountDeltaError::FungibleAssetDeltaOverflow {
292 faucet_id,
293 current: old,
294 delta,
295 },
296 )?;
297
298 if new == 0 {
299 entry.remove();
300 } else {
301 *entry.get_mut() = new;
302 }
303 },
304 }
305
306 Ok(())
307 }
308
309 fn validate(&self) -> Result<(), AccountDeltaError> {
314 for faucet_id in self.0.keys() {
315 if !matches!(faucet_id.account_type(), AccountType::FungibleFaucet) {
316 return Err(AccountDeltaError::NotAFungibleFaucetId(*faucet_id));
317 }
318 }
319
320 Ok(())
321 }
322}
323
324impl Serializable for FungibleAssetDelta {
325 fn write_into<W: ByteWriter>(&self, target: &mut W) {
326 target.write_usize(self.0.len());
327 target.write_many(self.0.iter().map(|(&faucet_id, &delta)| (faucet_id, delta as u64)));
331 }
332
333 fn get_size_hint(&self) -> usize {
334 self.0.len().get_size_hint() + self.0.len() * FungibleAsset::SERIALIZED_SIZE
335 }
336}
337
338impl Deserializable for FungibleAssetDelta {
339 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
340 let num_fungible_assets = source.read_usize()?;
341 let map = source
345 .read_many::<(AccountId, u64)>(num_fungible_assets)?
346 .into_iter()
347 .map(|(account_id, delta_as_u64)| (account_id, delta_as_u64 as i64))
348 .collect();
349
350 Self::new(map).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
351 }
352}
353
354#[derive(Clone, Debug, Default, PartialEq, Eq)]
359pub struct NonFungibleAssetDelta(BTreeMap<NonFungibleAsset, NonFungibleDeltaAction>);
360
361impl NonFungibleAssetDelta {
362 pub const fn new(map: BTreeMap<NonFungibleAsset, NonFungibleDeltaAction>) -> Self {
364 Self(map)
365 }
366
367 pub fn add(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> {
372 self.apply_action(asset, NonFungibleDeltaAction::Add)
373 }
374
375 pub fn remove(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> {
380 self.apply_action(asset, NonFungibleDeltaAction::Remove)
381 }
382
383 pub fn num_assets(&self) -> usize {
385 self.0.len()
386 }
387
388 pub fn is_empty(&self) -> bool {
390 self.0.is_empty()
391 }
392
393 pub fn iter(&self) -> impl Iterator<Item = (&NonFungibleAsset, &NonFungibleDeltaAction)> {
395 self.0.iter()
396 }
397
398 pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
405 for (&key, &action) in other.0.iter() {
407 self.apply_action(key, action)?;
408 }
409
410 Ok(())
411 }
412
413 fn apply_action(
422 &mut self,
423 asset: NonFungibleAsset,
424 action: NonFungibleDeltaAction,
425 ) -> Result<(), AccountDeltaError> {
426 match self.0.entry(asset) {
427 Entry::Vacant(entry) => {
428 entry.insert(action);
429 },
430 Entry::Occupied(entry) => {
431 let previous = *entry.get();
432 if previous == action {
433 return Err(AccountDeltaError::DuplicateNonFungibleVaultUpdate(asset));
435 }
436 entry.remove();
438 },
439 }
440
441 Ok(())
442 }
443
444 fn filter_by_action(
446 &self,
447 action: NonFungibleDeltaAction,
448 ) -> impl Iterator<Item = NonFungibleAsset> + '_ {
449 self.0
450 .iter()
451 .filter(move |&(_, cur_action)| cur_action == &action)
452 .map(|(key, _)| *key)
453 }
454}
455
456impl Serializable for NonFungibleAssetDelta {
457 fn write_into<W: ByteWriter>(&self, target: &mut W) {
458 let added: Vec<_> = self.filter_by_action(NonFungibleDeltaAction::Add).collect();
459 let removed: Vec<_> = self.filter_by_action(NonFungibleDeltaAction::Remove).collect();
460
461 target.write_usize(added.len());
462 target.write_many(added.iter());
463
464 target.write_usize(removed.len());
465 target.write_many(removed.iter());
466 }
467
468 fn get_size_hint(&self) -> usize {
469 let added = self.filter_by_action(NonFungibleDeltaAction::Add).count();
470 let removed = self.filter_by_action(NonFungibleDeltaAction::Remove).count();
471
472 added.get_size_hint()
473 + removed.get_size_hint()
474 + added * NonFungibleAsset::SERIALIZED_SIZE
475 + removed * NonFungibleAsset::SERIALIZED_SIZE
476 }
477}
478
479impl Deserializable for NonFungibleAssetDelta {
480 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
481 let mut map = BTreeMap::new();
482
483 let num_added = source.read_usize()?;
484 for _ in 0..num_added {
485 let added_asset = source.read()?;
486 map.insert(added_asset, NonFungibleDeltaAction::Add);
487 }
488
489 let num_removed = source.read_usize()?;
490 for _ in 0..num_removed {
491 let removed_asset = source.read()?;
492 map.insert(removed_asset, NonFungibleDeltaAction::Remove);
493 }
494
495 Ok(Self::new(map))
496 }
497}
498
499#[derive(Clone, Copy, Debug, PartialEq, Eq)]
500pub enum NonFungibleDeltaAction {
501 Add,
502 Remove,
503}
504
505#[cfg(test)]
509mod tests {
510 use super::{AccountVaultDelta, Deserializable, Serializable};
511 use crate::{
512 account::{AccountId, AccountIdPrefix},
513 asset::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails},
514 testing::account_id::{
515 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
516 },
517 };
518
519 #[test]
520 fn test_serde_account_vault() {
521 let asset_0 = FungibleAsset::mock(100);
522 let asset_1 = NonFungibleAsset::mock(&[10, 21, 32, 43]);
523 let delta = AccountVaultDelta::from_iters([asset_0], [asset_1]);
524
525 let serialized = delta.to_bytes();
526 let deserialized = AccountVaultDelta::read_from_bytes(&serialized).unwrap();
527 assert_eq!(deserialized, delta);
528 }
529
530 #[test]
531 fn test_is_empty_account_vault() {
532 let faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
533 let asset: Asset = FungibleAsset::new(faucet, 123).unwrap().into();
534
535 assert!(AccountVaultDelta::default().is_empty());
536 assert!(!AccountVaultDelta::from_iters([asset], []).is_empty());
537 assert!(!AccountVaultDelta::from_iters([], [asset]).is_empty());
538 }
539
540 #[rstest::rstest]
541 #[case::pos_pos(50, 50, Some(100))]
542 #[case::neg_neg(-50, -50, Some(-100))]
543 #[case::empty_pos(0, 50, Some(50))]
544 #[case::empty_neg(0, -50, Some(-50))]
545 #[case::nullify_pos_neg(100, -100, Some(0))]
546 #[case::nullify_neg_pos(-100, 100, Some(0))]
547 #[case::overflow(FungibleAsset::MAX_AMOUNT as i64, FungibleAsset::MAX_AMOUNT as i64, None)]
548 #[case::underflow(-(FungibleAsset::MAX_AMOUNT as i64), -(FungibleAsset::MAX_AMOUNT as i64), None)]
549 #[test]
550 fn merge_fungible_aggregation(#[case] x: i64, #[case] y: i64, #[case] expected: Option<i64>) {
551 fn create_delta_with_fungible(account_id: AccountId, amount: i64) -> AccountVaultDelta {
554 let asset = FungibleAsset::new(account_id, amount.unsigned_abs()).unwrap().into();
555 match amount {
556 0 => AccountVaultDelta::default(),
557 x if x.is_positive() => AccountVaultDelta::from_iters([asset], []),
558 _ => AccountVaultDelta::from_iters([], [asset]),
559 }
560 }
561
562 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
563
564 let mut delta_x = create_delta_with_fungible(account_id, x);
565 let delta_y = create_delta_with_fungible(account_id, y);
566
567 let result = delta_x.merge(delta_y);
568
569 if let Some(expected) = expected {
571 let expected = create_delta_with_fungible(account_id, expected);
572 assert_eq!(result.map(|_| delta_x).unwrap(), expected);
573 } else {
574 assert!(result.is_err());
575 }
576 }
577
578 #[rstest::rstest]
579 #[case::empty_removed(None, Some(false), Ok(Some(false)))]
580 #[case::empty_added(None, Some(true), Ok(Some(true)))]
581 #[case::add_remove(Some(true), Some(false), Ok(None))]
582 #[case::remove_add(Some(false), Some(true), Ok(None))]
583 #[case::double_add(Some(true), Some(true), Err(()))]
584 #[case::double_remove(Some(false), Some(false), Err(()))]
585 #[test]
586 fn merge_non_fungible_aggregation(
587 #[case] x: Option<bool>,
588 #[case] y: Option<bool>,
589 #[case] expected: Result<Option<bool>, ()>,
590 ) {
591 fn create_delta_with_non_fungible(
594 account_id_prefix: AccountIdPrefix,
595 added: Option<bool>,
596 ) -> AccountVaultDelta {
597 let asset: Asset = NonFungibleAsset::new(
598 &NonFungibleAssetDetails::new(account_id_prefix, vec![1, 2, 3]).unwrap(),
599 )
600 .unwrap()
601 .into();
602
603 match added {
604 Some(true) => AccountVaultDelta::from_iters([asset], []),
605 Some(false) => AccountVaultDelta::from_iters([], [asset]),
606 None => AccountVaultDelta::default(),
607 }
608 }
609
610 let account_id = NonFungibleAsset::mock_issuer().prefix();
611
612 let mut delta_x = create_delta_with_non_fungible(account_id, x);
613 let delta_y = create_delta_with_non_fungible(account_id, y);
614
615 let result = delta_x.merge(delta_y);
616
617 if let Ok(expected) = expected {
618 let expected = create_delta_with_non_fungible(account_id, expected);
619 assert_eq!(result.map(|_| delta_x).unwrap(), expected);
620 } else {
621 assert!(result.is_err());
622 }
623 }
624}