miden_objects/account/delta/
vault.rs

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// 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 amount of the fungible asset with the given faucet ID.
237    pub fn amount(&self, faucet_id: &AccountId) -> Option<i64> {
238        self.0.get(faucet_id).copied()
239    }
240
241    /// Returns the number of fungible assets affected in the delta.
242    pub fn num_assets(&self) -> usize {
243        self.0.len()
244    }
245
246    /// Returns true if this vault delta contains no updates.
247    pub fn is_empty(&self) -> bool {
248        self.0.is_empty()
249    }
250
251    /// Returns an iterator over the (key, value) pairs of the map.
252    pub fn iter(&self) -> impl Iterator<Item = (&AccountId, &i64)> {
253        self.0.iter()
254    }
255
256    /// Merges another delta into this one, overwriting any existing values.
257    ///
258    /// The result is validated as part of the merge.
259    ///
260    /// # Errors
261    /// Returns an error if the result did not pass validation.
262    pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
263        // Merge fungible assets.
264        //
265        // Track fungible asset amounts - positive and negative. `i64` is not lossy while
266        // fungibles are restricted to 2^63-1. Overflow is still possible but we check for that.
267
268        for (&faucet_id, &amount) in other.0.iter() {
269            self.add_delta(faucet_id, amount)?;
270        }
271
272        Ok(())
273    }
274
275    // HELPER FUNCTIONS
276    // ---------------------------------------------------------------------------------------------
277
278    /// Updates the provided map with the provided key and amount. If the final amount is 0,
279    /// the entry is removed.
280    ///
281    /// # Errors
282    /// Returns an error if the delta would overflow.
283    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    /// Checks whether this vault delta is valid.
310    ///
311    /// # Errors
312    /// Returns an error if one or more fungible assets' faucet IDs are invalid.
313    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        // TODO: We save `i64` as `u64` since winter utils only supports unsigned integers for now.
328        //   We should update this code (and deserialization as well) once it supports signed
329        //   integers.
330        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        // TODO: We save `i64` as `u64` since winter utils only supports unsigned integers for now.
342        //   We should update this code (and serialization as well) once it support signeds
343        // integers.
344        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// NON-FUNGIBLE ASSET DELTA
355// ================================================================================================
356
357/// A binary tree map of non-fungible asset changes (addition and removal) in the account vault.
358#[derive(Clone, Debug, Default, PartialEq, Eq)]
359pub struct NonFungibleAssetDelta(BTreeMap<NonFungibleAsset, NonFungibleDeltaAction>);
360
361impl NonFungibleAssetDelta {
362    /// Creates a new non-fungible asset delta.
363    pub const fn new(map: BTreeMap<NonFungibleAsset, NonFungibleDeltaAction>) -> Self {
364        Self(map)
365    }
366
367    /// Adds a new non-fungible asset to the delta.
368    ///
369    /// # Errors
370    /// Returns an error if the delta already contains the asset addition.
371    pub fn add(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> {
372        self.apply_action(asset, NonFungibleDeltaAction::Add)
373    }
374
375    /// Removes a non-fungible asset from the delta.
376    ///
377    /// # Errors
378    /// Returns an error if the delta already contains the asset removal.
379    pub fn remove(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> {
380        self.apply_action(asset, NonFungibleDeltaAction::Remove)
381    }
382
383    /// Returns the number of non-fungible assets affected in the delta.
384    pub fn num_assets(&self) -> usize {
385        self.0.len()
386    }
387
388    /// Returns true if this vault delta contains no updates.
389    pub fn is_empty(&self) -> bool {
390        self.0.is_empty()
391    }
392
393    /// Returns an iterator over the (key, value) pairs of the map.
394    pub fn iter(&self) -> impl Iterator<Item = (&NonFungibleAsset, &NonFungibleDeltaAction)> {
395        self.0.iter()
396    }
397
398    /// Merges another delta into this one, overwriting any existing values.
399    ///
400    /// The result is validated as part of the merge.
401    ///
402    /// # Errors
403    /// Returns an error if duplicate non-fungible assets are added or removed.
404    pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
405        // Merge non-fungible assets. Each non-fungible asset can cancel others out.
406        for (&key, &action) in other.0.iter() {
407            self.apply_action(key, action)?;
408        }
409
410        Ok(())
411    }
412
413    // HELPER FUNCTIONS
414    // ---------------------------------------------------------------------------------------------
415
416    /// Updates the provided map with the provided key and action.
417    /// If the action is the opposite to the previous one, the entry is removed.
418    ///
419    /// # Errors
420    /// Returns an error if the delta already contains the provided key and action.
421    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                    // Asset cannot be added nor removed twice.
434                    return Err(AccountDeltaError::DuplicateNonFungibleVaultUpdate(asset));
435                }
436                // Otherwise they cancel out.
437                entry.remove();
438            },
439        }
440
441        Ok(())
442    }
443
444    /// Returns an iterator over all keys that have the provided action.
445    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// TESTS
506// ================================================================================================
507
508#[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        /// Creates an [AccountVaultDelta] with a single [FungibleAsset] delta. This delta will
552        /// be added if `amount > 0`, removed if `amount < 0` or entirely missing if `amount == 0`.
553        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        // None is used to indicate an error is expected.
570        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        /// Creates an [AccountVaultDelta] with an optional [NonFungibleAsset] delta. This delta
592        /// will be added if `Some(true)`, removed for `Some(false)` and missing for `None`.
593        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}