miden_objects/account/delta/
vault.rs

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// ACCOUNT VAULT DELTA
15// ================================================================================================
16
17/// [AccountVaultDelta] stores the difference between the initial and final account vault states.
18///
19/// The difference is represented as follows:
20/// - fungible: a binary tree map of fungible asset balance changes in the account vault.
21/// - non_fungible: a binary tree map of non-fungible assets that were added to or removed from the
22///   account vault.
23#[derive(Clone, Debug, Default, PartialEq, Eq)]
24pub struct AccountVaultDelta {
25    fungible: FungibleAssetDelta,
26    non_fungible: NonFungibleAssetDelta,
27}
28
29impl AccountVaultDelta {
30    /// Validates and creates an [AccountVaultDelta] with the given fungible and non-fungible asset
31    /// deltas.
32    ///
33    /// # Errors
34    /// Returns an error if the delta does not pass the validation.
35    pub const fn new(fungible: FungibleAssetDelta, non_fungible: NonFungibleAssetDelta) -> Self {
36        Self { fungible, non_fungible }
37    }
38
39    /// Returns a reference to the fungible asset delta.
40    pub fn fungible(&self) -> &FungibleAssetDelta {
41        &self.fungible
42    }
43
44    /// Returns a reference to the non-fungible asset delta.
45    pub fn non_fungible(&self) -> &NonFungibleAssetDelta {
46        &self.non_fungible
47    }
48
49    /// Returns true if this vault delta contains no updates.
50    pub fn is_empty(&self) -> bool {
51        self.fungible.is_empty() && self.non_fungible.is_empty()
52    }
53
54    /// Tracks asset addition.
55    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    /// Tracks asset removal.
63    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    /// Merges another delta into this one, overwriting any existing values.
71    ///
72    /// The result is validated as part of the merge.
73    ///
74    /// # Errors
75    /// Returns an error if the resulted delta does not pass the validation.
76    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    /// Creates an [AccountVaultDelta] from the given iterators.
85    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    /// Returns an iterator over the added assets in this delta.
120    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    /// Returns an iterator over the removed assets in this delta.
135    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// FUNGIBLE ASSET DELTA
200// ================================================================================================
201
202/// A binary tree map of fungible asset balance changes in the account vault.
203#[derive(Clone, Debug, Default, PartialEq, Eq)]
204pub struct FungibleAssetDelta(BTreeMap<AccountId, i64>);
205
206impl FungibleAssetDelta {
207    /// Validates and creates a new fungible asset delta.
208    ///
209    /// # Errors
210    /// Returns an error if the delta does not pass the validation.
211    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    /// Adds a new fungible asset to the delta.
219    ///
220    /// # Errors
221    /// Returns an error if the delta would overflow.
222    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    /// Removes a fungible asset from the delta.
228    ///
229    /// # Errors
230    /// Returns an error if the delta would overflow.
231    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    /// Returns the number of fungible assets affected in the delta.
237    pub fn num_assets(&self) -> usize {
238        self.0.len()
239    }
240
241    /// Returns true if this vault delta contains no updates.
242    pub fn is_empty(&self) -> bool {
243        self.0.is_empty()
244    }
245
246    /// Returns an iterator over the (key, value) pairs of the map.
247    pub fn iter(&self) -> impl Iterator<Item = (&AccountId, &i64)> {
248        self.0.iter()
249    }
250
251    /// Merges another delta into this one, overwriting any existing values.
252    ///
253    /// The result is validated as part of the merge.
254    ///
255    /// # Errors
256    /// Returns an error if the result did not pass validation.
257    pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
258        // Merge fungible assets.
259        //
260        // Track fungible asset amounts - positive and negative. `i64` is not lossy while
261        // fungibles are restricted to 2^63-1. Overflow is still possible but we check for that.
262
263        for (&faucet_id, &amount) in other.0.iter() {
264            self.add_delta(faucet_id, amount)?;
265        }
266
267        Ok(())
268    }
269
270    // HELPER FUNCTIONS
271    // ---------------------------------------------------------------------------------------------
272
273    /// Updates the provided map with the provided key and amount. If the final amount is 0,
274    /// the entry is removed.
275    ///
276    /// # Errors
277    /// Returns an error if the delta would overflow.
278    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    /// Checks whether this vault delta is valid.
305    ///
306    /// # Errors
307    /// Returns an error if one or more fungible assets' faucet IDs are invalid.
308    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        // TODO: We save `i64` as `u64` since winter utils only supports unsigned integers for now.
323        //   We should update this code (and deserialization as well) once it supports signed
324        //   integers.
325        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        // TODO: We save `i64` as `u64` since winter utils only supports unsigned integers for now.
337        //   We should update this code (and serialization as well) once it support signeds
338        // integers.
339        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// NON-FUNGIBLE ASSET DELTA
350// ================================================================================================
351
352/// A binary tree map of non-fungible asset changes (addition and removal) in the account vault.
353#[derive(Clone, Debug, Default, PartialEq, Eq)]
354pub struct NonFungibleAssetDelta(BTreeMap<NonFungibleAsset, NonFungibleDeltaAction>);
355
356impl NonFungibleAssetDelta {
357    /// Creates a new non-fungible asset delta.
358    pub const fn new(map: BTreeMap<NonFungibleAsset, NonFungibleDeltaAction>) -> Self {
359        Self(map)
360    }
361
362    /// Adds a new non-fungible asset to the delta.
363    ///
364    /// # Errors
365    /// Returns an error if the delta already contains the asset addition.
366    pub fn add(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> {
367        self.apply_action(asset, NonFungibleDeltaAction::Add)
368    }
369
370    /// Removes a non-fungible asset from the delta.
371    ///
372    /// # Errors
373    /// Returns an error if the delta already contains the asset removal.
374    pub fn remove(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> {
375        self.apply_action(asset, NonFungibleDeltaAction::Remove)
376    }
377
378    /// Returns the number of non-fungible assets affected in the delta.
379    pub fn num_assets(&self) -> usize {
380        self.0.len()
381    }
382
383    /// Returns true if this vault delta contains no updates.
384    pub fn is_empty(&self) -> bool {
385        self.0.is_empty()
386    }
387
388    /// Returns an iterator over the (key, value) pairs of the map.
389    pub fn iter(&self) -> impl Iterator<Item = (&NonFungibleAsset, &NonFungibleDeltaAction)> {
390        self.0.iter()
391    }
392
393    /// Merges another delta into this one, overwriting any existing values.
394    ///
395    /// The result is validated as part of the merge.
396    ///
397    /// # Errors
398    /// Returns an error if duplicate non-fungible assets are added or removed.
399    pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
400        // Merge non-fungible assets. Each non-fungible asset can cancel others out.
401        for (&key, &action) in other.0.iter() {
402            self.apply_action(key, action)?;
403        }
404
405        Ok(())
406    }
407
408    // HELPER FUNCTIONS
409    // ---------------------------------------------------------------------------------------------
410
411    /// Updates the provided map with the provided key and action.
412    /// If the action is the opposite to the previous one, the entry is removed.
413    ///
414    /// # Errors
415    /// Returns an error if the delta already contains the provided key and action.
416    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                    // Asset cannot be added nor removed twice.
429                    return Err(AccountDeltaError::DuplicateNonFungibleVaultUpdate(asset));
430                }
431                // Otherwise they cancel out.
432                entry.remove();
433            },
434        }
435
436        Ok(())
437    }
438
439    /// Returns an iterator over all keys that have the provided action.
440    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// TESTS
501// ================================================================================================
502
503#[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        /// Creates an [AccountVaultDelta] with a single [FungibleAsset] delta. This delta will
547        /// be added if `amount > 0`, removed if `amount < 0` or entirely missing if `amount == 0`.
548        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        // None is used to indicate an error is expected.
565        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        /// Creates an [AccountVaultDelta] with an optional [NonFungibleAsset] delta. This delta
587        /// will be added if `Some(true)`, removed for `Some(false)` and missing for `None`.
588        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}