Skip to main content

miden_protocol/account/delta/
vault.rs

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::AccountType;
15use crate::asset::{Asset, AssetVaultKey, FungibleAsset, NonFungibleAsset};
16use crate::{Felt, ONE, ZERO};
17
18// ACCOUNT VAULT DELTA
19// ================================================================================================
20
21/// The domain for the assets in the delta commitment.
22const DOMAIN_ASSET: Felt = Felt::new(1);
23
24/// [AccountVaultDelta] stores the difference between the initial and final account vault states.
25///
26/// The difference is represented as follows:
27/// - fungible: a binary tree map of fungible asset balance changes in the account vault.
28/// - non_fungible: a binary tree map of non-fungible assets that were added to or removed from the
29///   account vault.
30#[derive(Clone, Debug, Default, PartialEq, Eq)]
31pub struct AccountVaultDelta {
32    fungible: FungibleAssetDelta,
33    non_fungible: NonFungibleAssetDelta,
34}
35
36impl AccountVaultDelta {
37    /// Validates and creates an [AccountVaultDelta] with the given fungible and non-fungible asset
38    /// deltas.
39    ///
40    /// # Errors
41    /// Returns an error if the delta does not pass the validation.
42    pub const fn new(fungible: FungibleAssetDelta, non_fungible: NonFungibleAssetDelta) -> Self {
43        Self { fungible, non_fungible }
44    }
45
46    /// Returns a reference to the fungible asset delta.
47    pub fn fungible(&self) -> &FungibleAssetDelta {
48        &self.fungible
49    }
50
51    /// Returns a reference to the non-fungible asset delta.
52    pub fn non_fungible(&self) -> &NonFungibleAssetDelta {
53        &self.non_fungible
54    }
55
56    /// Returns true if this vault delta contains no updates.
57    pub fn is_empty(&self) -> bool {
58        self.fungible.is_empty() && self.non_fungible.is_empty()
59    }
60
61    /// Tracks asset addition.
62    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    /// Tracks asset removal.
70    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    /// Merges another delta into this one, overwriting any existing values.
78    ///
79    /// The result is validated as part of the merge.
80    ///
81    /// # Errors
82    /// Returns an error if the resulted delta does not pass the validation.
83    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    /// Appends the vault delta to the given `elements` from which the delta commitment will be
89    /// computed.
90    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    /// Creates an [AccountVaultDelta] from the given iterators.
99    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    /// Returns an iterator over the added assets in this delta.
132    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(|(vault_key, &diff)| {
138                Asset::Fungible(
139                    FungibleAsset::new(vault_key.faucet_id(), diff.unsigned_abs())
140                        .unwrap()
141                        .with_callbacks(vault_key.callback_flag()),
142                )
143            })
144            .chain(
145                self.non_fungible
146                    .filter_by_action(NonFungibleDeltaAction::Add)
147                    .map(Asset::NonFungible),
148            )
149    }
150
151    /// Returns an iterator over the removed assets in this delta.
152    pub fn removed_assets(&self) -> impl Iterator<Item = crate::asset::Asset> + '_ {
153        self.fungible
154            .0
155            .iter()
156            .filter(|&(_, &value)| value < 0)
157            .map(|(vault_key, &diff)| {
158                Asset::Fungible(
159                    FungibleAsset::new(vault_key.faucet_id(), diff.unsigned_abs())
160                        .unwrap()
161                        .with_callbacks(vault_key.callback_flag()),
162                )
163            })
164            .chain(
165                self.non_fungible
166                    .filter_by_action(NonFungibleDeltaAction::Remove)
167                    .map(Asset::NonFungible),
168            )
169    }
170}
171
172impl Serializable for AccountVaultDelta {
173    fn write_into<W: ByteWriter>(&self, target: &mut W) {
174        target.write(&self.fungible);
175        target.write(&self.non_fungible);
176    }
177
178    fn get_size_hint(&self) -> usize {
179        self.fungible.get_size_hint() + self.non_fungible.get_size_hint()
180    }
181}
182
183impl Deserializable for AccountVaultDelta {
184    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
185        let fungible = source.read()?;
186        let non_fungible = source.read()?;
187
188        Ok(Self::new(fungible, non_fungible))
189    }
190}
191
192// FUNGIBLE ASSET DELTA
193// ================================================================================================
194
195/// A binary tree map of fungible asset balance changes in the account vault.
196///
197/// The [`AssetVaultKey`] orders the assets in the same way as the in-kernel account delta which
198/// uses a link map.
199#[derive(Clone, Debug, Default, PartialEq, Eq)]
200pub struct FungibleAssetDelta(BTreeMap<AssetVaultKey, i64>);
201
202impl FungibleAssetDelta {
203    /// Validates and creates a new fungible asset delta.
204    ///
205    /// # Errors
206    /// Returns an error if the delta does not pass the validation.
207    pub fn new(map: BTreeMap<AssetVaultKey, i64>) -> Result<Self, AccountDeltaError> {
208        let delta = Self(map);
209        delta.validate()?;
210
211        Ok(delta)
212    }
213
214    /// Adds a new fungible asset to the delta.
215    ///
216    /// # Errors
217    /// Returns an error if the delta would overflow.
218    pub fn add(&mut self, asset: FungibleAsset) -> Result<(), AccountDeltaError> {
219        let amount: i64 = asset.amount().try_into().expect("Amount it too high");
220        self.add_delta(asset.vault_key(), amount)
221    }
222
223    /// Removes a fungible asset from the delta.
224    ///
225    /// # Errors
226    /// Returns an error if the delta would overflow.
227    pub fn remove(&mut self, asset: FungibleAsset) -> Result<(), AccountDeltaError> {
228        let amount: i64 = asset.amount().try_into().expect("Amount it too high");
229        self.add_delta(asset.vault_key(), -amount)
230    }
231
232    /// Returns the amount of the fungible asset with the given vault key.
233    pub fn amount(&self, vault_key: &AssetVaultKey) -> Option<i64> {
234        self.0.get(vault_key).copied()
235    }
236
237    /// Returns the number of fungible assets affected in the delta.
238    pub fn num_assets(&self) -> usize {
239        self.0.len()
240    }
241
242    /// Returns true if this vault delta contains no updates.
243    pub fn is_empty(&self) -> bool {
244        self.0.is_empty()
245    }
246
247    /// Returns an iterator over the (key, value) pairs of the map.
248    pub fn iter(&self) -> impl Iterator<Item = (&AssetVaultKey, &i64)> {
249        self.0.iter()
250    }
251
252    /// Merges another delta into this one, overwriting any existing values.
253    ///
254    /// The result is validated as part of the merge.
255    ///
256    /// # Errors
257    /// Returns an error if the result did not pass validation.
258    pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
259        // Merge fungible assets.
260        //
261        // Track fungible asset amounts - positive and negative. `i64` is not lossy while
262        // fungibles are restricted to 2^63-1. Overflow is still possible but we check for that.
263
264        for (&vault_key, &amount) in other.0.iter() {
265            self.add_delta(vault_key, amount)?;
266        }
267
268        Ok(())
269    }
270
271    // HELPER FUNCTIONS
272    // ---------------------------------------------------------------------------------------------
273
274    /// Updates the provided map with the provided key and amount. If the final amount is 0,
275    /// the entry is removed.
276    ///
277    /// # Errors
278    /// Returns an error if the delta would overflow.
279    fn add_delta(&mut self, vault_key: AssetVaultKey, delta: i64) -> Result<(), AccountDeltaError> {
280        match self.0.entry(vault_key) {
281            Entry::Vacant(entry) => {
282                // Only track non-zero amounts.
283                if delta != 0 {
284                    entry.insert(delta);
285                }
286            },
287            Entry::Occupied(mut entry) => {
288                let old = *entry.get();
289                let new = old.checked_add(delta).ok_or(
290                    AccountDeltaError::FungibleAssetDeltaOverflow {
291                        faucet_id: vault_key.faucet_id(),
292                        current: old,
293                        delta,
294                    },
295                )?;
296
297                if new == 0 {
298                    entry.remove();
299                } else {
300                    *entry.get_mut() = new;
301                }
302            },
303        }
304
305        Ok(())
306    }
307
308    /// Checks whether this vault delta is valid.
309    ///
310    /// # Errors
311    /// Returns an error if one or more fungible assets' faucet IDs are invalid.
312    fn validate(&self) -> Result<(), AccountDeltaError> {
313        for vault_key in self.0.keys() {
314            if !matches!(vault_key.faucet_id().account_type(), AccountType::FungibleFaucet) {
315                return Err(AccountDeltaError::NotAFungibleFaucetId(vault_key.faucet_id()));
316            }
317        }
318
319        Ok(())
320    }
321
322    /// Appends the fungible asset vault delta to the given `elements` from which the delta
323    /// commitment will be computed.
324    ///
325    /// Note that the order in which elements are appended should be the link map key ordering. This
326    /// is fulfilled here because the link map key's most significant element takes precedence over
327    /// less significant ones. The most significant element in the fungible asset delta is the
328    /// faucet ID prefix and the delta happens to be sorted by vault keys. Since the faucet ID
329    /// prefix is unique, it will always decide on the ordering of a link map key, so less
330    /// significant elements are unimportant. This implicit sort should therefore always match the
331    /// link map key ordering, however this is subtle and fragile.
332    pub(super) fn append_delta_elements(&self, elements: &mut Vec<Felt>) {
333        for (vault_key, amount_delta) in self.iter() {
334            // Note that this iterator is guaranteed to never yield zero amounts, so we don't have
335            // to exclude those explicitly.
336            debug_assert_ne!(
337                *amount_delta, 0,
338                "fungible asset iterator should never yield amount deltas of 0"
339            );
340
341            let was_added = if *amount_delta > 0 { ONE } else { ZERO };
342            let amount_delta = Felt::try_from(amount_delta.unsigned_abs())
343                .expect("amount delta should be less than i64::MAX");
344
345            let key_word = vault_key.to_word();
346            elements.extend_from_slice(&[
347                DOMAIN_ASSET,
348                was_added,
349                key_word[2], // faucet_id_suffix_and_metadata
350                key_word[3], // faucet_id_prefix
351            ]);
352            elements.extend_from_slice(&[amount_delta, ZERO, ZERO, ZERO]);
353        }
354    }
355}
356
357impl Serializable for FungibleAssetDelta {
358    fn write_into<W: ByteWriter>(&self, target: &mut W) {
359        target.write_usize(self.0.len());
360        // TODO: We save `i64` as `u64` since winter utils only supports unsigned integers for now.
361        //   We should update this code (and deserialization as well) once it supports signed
362        //   integers.
363        // TODO: If we keep this code, optimize by not serializing asset ID (which is always 0).
364        target.write_many(self.0.iter().map(|(vault_key, &delta)| (*vault_key, delta as u64)));
365    }
366
367    fn get_size_hint(&self) -> usize {
368        const ENTRY_SIZE: usize = AssetVaultKey::SERIALIZED_SIZE + core::mem::size_of::<u64>();
369        self.0.len().get_size_hint() + self.0.len() * ENTRY_SIZE
370    }
371}
372
373impl Deserializable for FungibleAssetDelta {
374    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
375        let num_fungible_assets = source.read_usize()?;
376        // TODO: We save `i64` as `u64` since winter utils only supports unsigned integers for now.
377        //   We should update this code (and serialization as well) once it supports signed
378        //   integers.
379        let map = source
380            .read_many_iter::<(AssetVaultKey, u64)>(num_fungible_assets)?
381            .map(|result| result.map(|(vault_key, delta_as_u64)| (vault_key, delta_as_u64 as i64)))
382            .collect::<Result<_, _>>()?;
383
384        Self::new(map).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
385    }
386}
387
388// NON-FUNGIBLE ASSET DELTA
389// ================================================================================================
390
391/// A binary tree map of non-fungible asset changes (addition and removal) in the account vault.
392///
393/// The [`AssetVaultKey`] orders the assets in the same way as the in-kernel account delta which
394/// uses a link map.
395#[derive(Clone, Debug, Default, PartialEq, Eq)]
396pub struct NonFungibleAssetDelta(
397    BTreeMap<AssetVaultKey, (NonFungibleAsset, NonFungibleDeltaAction)>,
398);
399
400impl NonFungibleAssetDelta {
401    /// Creates a new non-fungible asset delta.
402    pub const fn new(
403        map: BTreeMap<AssetVaultKey, (NonFungibleAsset, NonFungibleDeltaAction)>,
404    ) -> Self {
405        Self(map)
406    }
407
408    /// Adds a new non-fungible asset to the delta.
409    ///
410    /// # Errors
411    /// Returns an error if the delta already contains the asset addition.
412    pub fn add(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> {
413        self.apply_action(asset, NonFungibleDeltaAction::Add)
414    }
415
416    /// Removes a non-fungible asset from the delta.
417    ///
418    /// # Errors
419    /// Returns an error if the delta already contains the asset removal.
420    pub fn remove(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> {
421        self.apply_action(asset, NonFungibleDeltaAction::Remove)
422    }
423
424    /// Returns the number of non-fungible assets affected in the delta.
425    pub fn num_assets(&self) -> usize {
426        self.0.len()
427    }
428
429    /// Returns true if this vault delta contains no updates.
430    pub fn is_empty(&self) -> bool {
431        self.0.is_empty()
432    }
433
434    /// Returns an iterator over the (key, value) pairs of the map.
435    pub fn iter(&self) -> impl Iterator<Item = (&NonFungibleAsset, &NonFungibleDeltaAction)> {
436        self.0
437            .iter()
438            .map(|(_key, (non_fungible_asset, delta_action))| (non_fungible_asset, delta_action))
439    }
440
441    /// Merges another delta into this one, overwriting any existing values.
442    ///
443    /// The result is validated as part of the merge.
444    ///
445    /// # Errors
446    /// Returns an error if duplicate non-fungible assets are added or removed.
447    pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
448        // Merge non-fungible assets. Each non-fungible asset can cancel others out.
449        for (&asset, &action) in other.iter() {
450            self.apply_action(asset, action)?;
451        }
452
453        Ok(())
454    }
455
456    // HELPER FUNCTIONS
457    // ---------------------------------------------------------------------------------------------
458
459    /// Updates the provided map with the provided key and action.
460    /// If the action is the opposite to the previous one, the entry is removed.
461    ///
462    /// # Errors
463    /// Returns an error if the delta already contains the provided key and action.
464    fn apply_action(
465        &mut self,
466        asset: NonFungibleAsset,
467        action: NonFungibleDeltaAction,
468    ) -> Result<(), AccountDeltaError> {
469        match self.0.entry(asset.vault_key()) {
470            Entry::Vacant(entry) => {
471                entry.insert((asset, action));
472            },
473            Entry::Occupied(entry) => {
474                let (_prev_asset, previous_action) = *entry.get();
475                if previous_action == action {
476                    // Asset cannot be added nor removed twice.
477                    return Err(AccountDeltaError::DuplicateNonFungibleVaultUpdate(asset));
478                }
479                // Otherwise they cancel out.
480                entry.remove();
481            },
482        }
483
484        Ok(())
485    }
486
487    /// Returns an iterator over all keys that have the provided action.
488    fn filter_by_action(
489        &self,
490        action: NonFungibleDeltaAction,
491    ) -> impl Iterator<Item = NonFungibleAsset> + '_ {
492        self.0
493            .iter()
494            .filter(move |&(_, (_asset, cur_action))| cur_action == &action)
495            .map(|(_key, (asset, _action))| *asset)
496    }
497
498    /// Appends the non-fungible asset vault delta to the given `elements` from which the delta
499    /// commitment will be computed.
500    pub(super) fn append_delta_elements(&self, elements: &mut Vec<Felt>) {
501        for (asset, action) in self.iter() {
502            let was_added = match action {
503                NonFungibleDeltaAction::Remove => ZERO,
504                NonFungibleDeltaAction::Add => ONE,
505            };
506
507            let key_word = asset.vault_key().to_word();
508            elements.extend_from_slice(&[
509                DOMAIN_ASSET,
510                was_added,
511                key_word[2], // faucet_id_suffix_and_metadata
512                key_word[3], // faucet_id_prefix
513            ]);
514            elements.extend_from_slice(asset.to_value_word().as_elements());
515        }
516    }
517}
518
519impl Serializable for NonFungibleAssetDelta {
520    fn write_into<W: ByteWriter>(&self, target: &mut W) {
521        let added: Vec<_> = self.filter_by_action(NonFungibleDeltaAction::Add).collect();
522        let removed: Vec<_> = self.filter_by_action(NonFungibleDeltaAction::Remove).collect();
523
524        target.write_usize(added.len());
525        target.write_many(added.iter());
526
527        target.write_usize(removed.len());
528        target.write_many(removed.iter());
529    }
530
531    fn get_size_hint(&self) -> usize {
532        let added = self.filter_by_action(NonFungibleDeltaAction::Add).count();
533        let removed = self.filter_by_action(NonFungibleDeltaAction::Remove).count();
534
535        added.get_size_hint()
536            + removed.get_size_hint()
537            + added * NonFungibleAsset::SERIALIZED_SIZE
538            + removed * NonFungibleAsset::SERIALIZED_SIZE
539    }
540}
541
542impl Deserializable for NonFungibleAssetDelta {
543    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
544        let mut map = BTreeMap::new();
545
546        let num_added = source.read_usize()?;
547        for _ in 0..num_added {
548            let added_asset: NonFungibleAsset = source.read()?;
549            map.insert(added_asset.vault_key(), (added_asset, NonFungibleDeltaAction::Add));
550        }
551
552        let num_removed = source.read_usize()?;
553        for _ in 0..num_removed {
554            let removed_asset: NonFungibleAsset = source.read()?;
555            map.insert(removed_asset.vault_key(), (removed_asset, NonFungibleDeltaAction::Remove));
556        }
557
558        Ok(Self::new(map))
559    }
560}
561
562#[derive(Clone, Copy, Debug, PartialEq, Eq)]
563pub enum NonFungibleDeltaAction {
564    Add,
565    Remove,
566}
567
568// TESTS
569// ================================================================================================
570
571#[cfg(test)]
572mod tests {
573    use super::{AccountVaultDelta, Deserializable, Serializable};
574    use crate::account::AccountId;
575    use crate::asset::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails};
576    use crate::testing::account_id::{
577        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
578        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
579    };
580
581    #[test]
582    fn test_serde_account_vault() {
583        let asset_0 = FungibleAsset::mock(100);
584        let asset_1 = NonFungibleAsset::mock(&[10, 21, 32, 43]);
585        let delta = AccountVaultDelta::from_iters([asset_0], [asset_1]);
586
587        let serialized = delta.to_bytes();
588        let deserialized = AccountVaultDelta::read_from_bytes(&serialized).unwrap();
589        assert_eq!(deserialized, delta);
590    }
591
592    #[test]
593    fn test_is_empty_account_vault() {
594        let faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
595        let asset: Asset = FungibleAsset::new(faucet, 123).unwrap().into();
596
597        assert!(AccountVaultDelta::default().is_empty());
598        assert!(!AccountVaultDelta::from_iters([asset], []).is_empty());
599        assert!(!AccountVaultDelta::from_iters([], [asset]).is_empty());
600    }
601
602    #[rstest::rstest]
603    #[case::pos_pos(50, 50, Some(100))]
604    #[case::neg_neg(-50, -50, Some(-100))]
605    #[case::empty_pos(0, 50, Some(50))]
606    #[case::empty_neg(0, -50, Some(-50))]
607    #[case::nullify_pos_neg(100, -100, Some(0))]
608    #[case::nullify_neg_pos(-100, 100, Some(0))]
609    #[case::overflow(FungibleAsset::MAX_AMOUNT as i64, FungibleAsset::MAX_AMOUNT as i64, None)]
610    #[case::underflow(-(FungibleAsset::MAX_AMOUNT as i64), -(FungibleAsset::MAX_AMOUNT as i64), None)]
611    #[test]
612    fn merge_fungible_aggregation(#[case] x: i64, #[case] y: i64, #[case] expected: Option<i64>) {
613        /// Creates an [AccountVaultDelta] with a single [FungibleAsset] delta. This delta will
614        /// be added if `amount > 0`, removed if `amount < 0` or entirely missing if `amount == 0`.
615        fn create_delta_with_fungible(account_id: AccountId, amount: i64) -> AccountVaultDelta {
616            let asset = FungibleAsset::new(account_id, amount.unsigned_abs()).unwrap().into();
617            match amount {
618                0 => AccountVaultDelta::default(),
619                x if x.is_positive() => AccountVaultDelta::from_iters([asset], []),
620                _ => AccountVaultDelta::from_iters([], [asset]),
621            }
622        }
623
624        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
625
626        let mut delta_x = create_delta_with_fungible(account_id, x);
627        let delta_y = create_delta_with_fungible(account_id, y);
628
629        let result = delta_x.merge(delta_y);
630
631        // None is used to indicate an error is expected.
632        if let Some(expected) = expected {
633            let expected = create_delta_with_fungible(account_id, expected);
634            assert_eq!(result.map(|_| delta_x).unwrap(), expected);
635        } else {
636            assert!(result.is_err());
637        }
638    }
639
640    #[rstest::rstest]
641    #[case::empty_removed(None, Some(false), Ok(Some(false)))]
642    #[case::empty_added(None, Some(true), Ok(Some(true)))]
643    #[case::add_remove(Some(true), Some(false), Ok(None))]
644    #[case::remove_add(Some(false), Some(true), Ok(None))]
645    #[case::double_add(Some(true), Some(true), Err(()))]
646    #[case::double_remove(Some(false), Some(false), Err(()))]
647    #[test]
648    fn merge_non_fungible_aggregation(
649        #[case] x: Option<bool>,
650        #[case] y: Option<bool>,
651        #[case] expected: Result<Option<bool>, ()>,
652    ) {
653        /// Creates an [AccountVaultDelta] with an optional [NonFungibleAsset] delta. This delta
654        /// will be added if `Some(true)`, removed for `Some(false)` and missing for `None`.
655        fn create_delta_with_non_fungible(
656            account_id: AccountId,
657            added: Option<bool>,
658        ) -> AccountVaultDelta {
659            let asset: Asset = NonFungibleAsset::new(
660                &NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]).unwrap(),
661            )
662            .unwrap()
663            .into();
664
665            match added {
666                Some(true) => AccountVaultDelta::from_iters([asset], []),
667                Some(false) => AccountVaultDelta::from_iters([], [asset]),
668                None => AccountVaultDelta::default(),
669            }
670        }
671
672        let account_id = NonFungibleAsset::mock_issuer();
673
674        let mut delta_x = create_delta_with_non_fungible(account_id, x);
675        let delta_y = create_delta_with_non_fungible(account_id, y);
676
677        let result = delta_x.merge(delta_y);
678
679        if let Ok(expected) = expected {
680            let expected = create_delta_with_non_fungible(account_id, expected);
681            assert_eq!(result.map(|_| delta_x).unwrap(), expected);
682        } else {
683            assert!(result.is_err());
684        }
685    }
686}