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